# Khora documentation Source: https://khoralang.com/docs/v0.4/index.md ## Where Khora actually is **Khora is `0.x` and pre-1.0, with one maintainer.** What that means in practice, stated here so nobody has to discover it: - The language and `std` can break between releases. Every breaking change is in the changelog under a **Breaking** heading first, and pinning `[toolchain]` makes a build reproducible — but there is no source-compatibility promise across releases yet. [Compatibility and stability](/docs/v0.4/reference/compatibility/) is the policy, including the four things 1.0 is waiting for. - Recent bug-hunting sessions still turn up **silent-wrongness bugs**, most recently in trait dispatch and structured concurrency. That is the specific reason 1.0 has not been called. - There is **no public package registry** yet; dependencies are paths and pinned git revisions. - Structured concurrency is the least finished part of the language. There is no `timeout`, no `race` and no `select`, a bounded nursery admits one more child than its limit, and a child's failure does not reliably cancel its siblings. [Known limitations](/docs/v0.4/limitations/) has the measurements. - Nobody who did not write it has shipped with it. If you are evaluating Khora for something load-bearing, read [Known limitations](/docs/v0.4/limitations/) and [Performance](/docs/v0.4/performance/) before anything else on this site; both are written to be falsifiable, not persuasive. ## Why this rather than Rust, OCaml or Gleam Khora's bet is that three things belong in the type system together — what a function can *fail* with, what external *authority* it needs, and how its concurrent children are *scoped* — and that a native language can have all three without a borrow checker and without a tracing garbage collector. That combination is the argument; every piece of it exists somewhere else already. **Against Rust.** Khora has no lifetimes, no borrow checker and no source-level ownership syntax. Memory is reference-counted, with the compiler eliding retain/release pairs and reusing uniquely-owned storage where it cannot change what the program means; [Memory and resources](/docs/v0.4/reference/memory-and-resources/) says exactly what is promised. Failures are a `raises` row the compiler propagates rather than a `Result` you thread by hand, and capabilities are a `with` row, not a convention. What Rust has that Khora does not: a stable language, a decade of libraries, a registry, and predictable performance without a reference-count cost. Khora's HTTP server answers about 174,000 requests a second in 8.4 MB — mid-table on rate and first on memory in [the comparison](/docs/v0.4/performance/) — which is a real number but not a Rust-beating one. **Against OCaml.** OCaml 5 also has effect handlers, a fast native compiler and a far more mature implementation. The differences are that Khora's rows carry failure *and* capability *and* are inferred through call chains, that memory is reference-counted rather than traced (which is where the 8.4 MB comes from), and that concurrency is structured in the language itself. OCaml wins on maturity, on tooling that has been used in anger for thirty years, and on a real ecosystem. **Against Gleam.** Gleam is a friendlier and much more finished language, and it inherits the BEAM: OTP, supervision trees, and a preemptive scheduler, none of which Khora's nurseries match today (see the nursery measurements in [Known limitations](/docs/v0.4/limitations/)). Khora compiles to native code through LLVM with no VM underneath, which is the case for it where a runtime is not available or a few megabytes of RSS is the constraint, and it has effects and capabilities, which Gleam does not. If none of those trades is one you want to make, the honest answer is to use the mature thing. Khora is worth your time if failure rows, capabilities and structured concurrency in one native language is a combination you cannot get elsewhere, and you can tolerate a pre-1.0 implementation while it is proved. ## Start here - **[Getting started](/docs/v0.4/getting-started/)** — install Khora, create a project, build it, run it, and test it. - **[Language reference](/docs/v0.4/reference/)** — every construct in one place: values, functions, algebraic data types, pattern matching, pipelines, generics, traits, effects, capabilities, resources, and fibers. It opens with a reading order for a first pass, and is the lookup-oriented page for an exact answer. - **[Standard library](/docs/v0.4/stdlib/)** — curated overview plus generated API reference kept in sync with the source by `khora doc`. ## Build real applications - **[Cookbook](/docs/v0.4/cookbook/)** — worked patterns for HTTP services, database access, tracing, cancellation, bounded concurrency, and testing. - **[Packages](/docs/v0.4/packages/)** — what ships alongside the language rather than inside it, and the manifest line each is depended on with. - **[Deployment](/docs/v0.4/deployment/)** — supported targets and how Khora applications are built and deployed. - **[Migration guides](/docs/v0.4/migration/)** — mental-model bridges for developers coming from Effect TypeScript, Go, and Rust. - **[Performance](/docs/v0.4/performance/)** — what the HTTP server answers, measured against a load generator that is not the bottleneck, with the conditions each figure had to satisfy and an account of why every number published before September 2026 was too high. - **[Internals](/docs/v0.4/internals/)** — how Khora works underneath: what a reference count costs, what `with` compiles to, what a fiber is made of, and what makes a release build reproducible. Nothing here is needed to write Khora; it is for debugging something strange or judging whether Khora suits a problem. - **[Limitations](/docs/v0.4/limitations/)** — functionality that is intentionally incomplete, unsupported, or still evolving, with the measurements behind each entry. Reading this as an AI agent? [/llms.txt](/llms.txt) is the index and [/llms-full.txt](/llms-full.txt) is the whole documentation in one file. ## Tooling The Khora toolchain is one binary. These are the commands a project uses day to day; `khora --help` lists all of them. ```text khora new start a package khora build compile it khora run compile and run it khora check parse, type check and lint, without building khora test run its tests khora bench run its benchmarks khora fmt format its source khora doc generate API pages from its `///` comments khora std search the standard library from the terminal khora lsp the language server, for editors khora mcp the same knowledge, for coding agents ``` `khora std search ` is the fastest way to find out whether something exists. It reads the compiler's own view of the `std` beside it — signatures sliced from the declarations, descriptions taken from their `///` comments — so it is never out of step with the toolchain you have, which is more than these pages can promise. There is no `khora lint`: the lints run inside `khora check`, because a separate command is a second thing to run and a second answer to disagree with the first. `khora sbom`, `khora toolchain`, `khora update`, `khora cache`, `khora why`, `khora graph` and `khora release` cover distribution and diagnosis. If you are new to Khora, start with **Getting started**, then read the **Language reference** in the order its first section gives. Use the **Standard library** for what ships with the toolchain, and the **Cookbook** for a whole task working end to end. # Getting Started Source: https://khoralang.com/docs/v0.4/getting-started/index.md You can go from an empty directory to a checked, tested, native Khora executable in a few minutes. Two things worth knowing before you start. `khora std search ` finds anything in the standard library from the terminal — it reads the compiler's own view of the `std` beside it, so it is never out of step with the toolchain you have. And the compiler's error messages name the fix, not just the problem; reading them is usually faster than searching these pages. ## 1. Install Khora The ordinary installer selects the latest **published stable** version. To follow this 0.4 walkthrough before the stable release, first use the [0.4 release-candidate installer](/docs/v0.4/getting-started/installation/#release-candidates) once that candidate is published. Check `khora --version` before continuing: `khora new` pins that exact version, and a plain install during the RC period still creates a 0.3 project. The manifest examples below show stable 0.4.0; for the candidate, use `0.4.0-rc.2` in every `[toolchain]` pin. For the current published stable version on macOS or Linux: ```bash curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh | sh ``` For the 0.4 candidate, once it is published, **use this instead**: ```bash curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh | sh -s -- --version 0.4.0-rc.2 ``` On Windows, the ordinary stable installer is: ```powershell irm https://raw.githubusercontent.com/codyspate/khoralang/main/install.ps1 | iex ``` For the published 0.4 candidate on Windows, run the prerelease installer instead (PowerShell cannot pass a flag through `iex`): ```powershell irm https://raw.githubusercontent.com/codyspate/khoralang/main/installrc.ps1 | iex ``` Verify that `khora --version` reports `0.4.0-rc.2` before proceeding. Verify the toolchain is available: ```bash khora --version ``` If the installer asks for a system linker, follow the platform-specific instructions in [Installation](/docs/v0.4/getting-started/installation/). ## 2. Create a package ```bash khora new hello_khora cd hello_khora ``` That writes the whole package: ```text hello_khora/ ├── .gitignore ├── khora.toml └── src/ └── main.kh ``` `khora.toml` names the package and pins the compiler that builds it: ```toml [package] name = "hello_khora" version = "0.1.0" [toolchain] version = "0.4.0" ``` The example pin is for the stable 0.4.0 release. On the candidate path above, `khora new` writes `version = "0.4.0-rc.2"` instead; keep it, and use a package dependency tagged `v0.4.0-rc.2` if the project adds one. Both tables are required. `[toolchain]` is what makes "this project builds the same way on your machine" true by default rather than by convention, so a project without one stops with the two lines to add. `khora new` writes it for you with the version you are running. and `src/main.kh` is a program that compiles: ```khora module hello_khora::main; pub fn main() -> Int { 0 } ``` A package name is a module path segment, so it is letters, digits and underscores — `hello_khora`, not `hello-khora`. `khora new` says so rather than creating something that will not compile. Add `khora new --lib` for a library, which writes `src/lib.kh` and no `main`. Make it print something: ```khora module hello_khora::main; import std::core::{print}; pub fn main() -> Int { print("Hello, Khora!"); 0 } ``` ## 3. Run it ```bash khora run . ``` ```text Hello, Khora! ``` `khora run` compiles and starts the program, and gives you its exit status as its own — so it behaves in a script the way running the executable would. The build is cached, so the second run starts immediately. ### Arguments Anything after `--` goes to the program rather than to `khora`: ```bash khora run . -- --quiet report.txt ``` Without the `--`, `khora` reads `--quiet` as one of its own options and stops with `error: unexpected argument '--quiet' found`. The program reads them through `Env`, which is a capability like any other: ```khora module hello_khora::main; import std::core::{Iterator, List, Step, print}; import std::env::{Env}; pub fn main() { let env = Env::real(); match env.arguments() { List::Nil => print("no arguments"), List::Cons(program, rest) => { print("program: ${program}"); for arg in rest { print("arg: ${arg}"); } }, } } ``` ```text program: ./build/hello_khora arg: --quiet arg: report.txt ``` `arguments` is an operation on `Env`, so it is called through the capability — `env.arguments()`, not an imported free function. The program's own name comes first, the same convention C, Rust and Go follow, which is why the example splits it off before walking the rest. Reading an environment *variable* needs a grant in `khora.toml`; the argument list does not, because the person typing the command is already the one who chose it. To get an executable you can hand to somebody: ```bash khora check . khora build . ./build/hello_khora ``` A build writes into the package's own `build/` directory, named after the package -- `build/hello_khora` here, and `build\hello_khora.exe` on Windows, where the same two commands run it as `.\build\hello_khora.exe`. Nothing lands among your sources, and `build/` is the one directory a package does not track. `--out` overrides the path, and adds the platform's extension if you did not write one. `khora check` is the fast feedback command: it parses and resolves your package, checks types and exhaustiveness, and reports diagnostics without producing an executable. `khora build` continues through native code generation and linking. ## 4. Format and test as you work Khora ships the formatter and test runner in the same toolchain: ```bash khora fmt . khora test . ``` `khora fmt . --check` writes nothing and fails if anything is unformatted, which is the form for CI. ## 5. Depend on another package A package that needs another names it under `[dependencies]`, by path: ```toml [package] name = "hello_khora" version = "0.1.0" [toolchain] version = "0.4.0" [dependencies] postgres = { path = "../packages/postgres" } ``` The name on the left is what your `import` lines say — `import postgres::pool::{Pool}` — and it need not match the directory. A dependency's own dependencies come with it. Continue with [Your first Khora project](/docs/v0.4/getting-started/first-project/) to add a function and a test and learn the everyday check-format-test-build loop. ## 6. Set up your editor Khora's language server is part of the compiler toolchain. Your editor starts `khora lsp` for you and gets compiler-backed diagnostics, navigation, formatting, and symbol information. See [Editor setup](/docs/v0.4/getting-started/editor/) for configuration and the compiler-backed MCP integration for coding agents. ## Where to go next - [Installation](/docs/v0.4/getting-started/installation/) — toolchains, version pinning, source builds, and optional release candidates. - [Your first Khora project](/docs/v0.4/getting-started/first-project/) — work through the normal package workflow. - [Language Reference](/docs/v0.4/reference/) — every construct, one page per topic. Its first section is a reading order, from values and functions through effects and structured concurrency. - [Standard Library](/docs/v0.4/stdlib/) — find the APIs available once you start building real programs. # Installation Source: https://khoralang.com/docs/v0.4/getting-started/installation/index.md Install the Khora toolchain once, then use `khora` to manage compiler versions for all of your projects. There is no separate version manager to install. These `next` pages describe 0.4.0. Until its stable release is published, the ordinary installer still selects the latest published stable version. For 0.4 candidate testing, use the [release-candidate instructions](#release-candidates) *after* the candidate appears on the release page; examples pinned to 0.4.0 require the stable artifact and do not install an unpublished release. Reading this as an AI agent? [/llms.txt](/llms.txt) is the index and [/llms-full.txt](/llms-full.txt) is the whole documentation in one file. ## Install Khora On macOS or Linux: ```bash curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh | sh ``` The script needs `curl` or `wget`, `tar`, and CA certificates. A desktop or a CI runner has them; a slim container image often does not — on `debian:*-slim`, `apt-get install -y curl ca-certificates` first. Running it as `root`, in a `Dockerfile` or a CI step, is supported. On Windows, in PowerShell: ```powershell irm https://raw.githubusercontent.com/codyspate/khoralang/main/install.ps1 | iex ``` The installer downloads the build for your platform, verifies it against the published checksum, and installs it under `~/.khora` — or under `$KHORA_HOME` when that variable is set, which is also where every later `khora` command looks for toolchains. The installer prints the directory it used. It does not compile Khora from source and does not require administrator access. Verify the installation: ```bash khora --version ``` Then continue with [Getting Started](/docs/v0.4/getting-started/) or [Your first Khora project](/docs/v0.4/getting-started/first-project/). ## Verify what you downloaded Every archive is published with a `.sha256` beside it, and the installer checks it. If you fetched an archive by hand, the release page lists both, at ```text https://github.com/codyspate/khoralang/releases/download/v/khora--.tar.gz https://github.com/codyspate/khoralang/releases/download/v/khora--.tar.gz.sha256 ``` (`.zip` on Windows), and you check it the same way: ```bash sha256sum -c khora-0.4.0-x86_64-unknown-linux-gnu.tar.gz.sha256 ``` A checksum says the bytes are the ones that were published. It does not say who published them, and anybody who can replace the archive can replace the checksum next to it. For that, every archive also carries **build provenance**: a signed statement, made by GitHub during the release run, of which workflow in which repository at which commit produced that exact file. Checking it needs the GitHub CLI signed in to any account (`gh auth login`): ```bash gh attestation verify khora-0.4.0-x86_64-unknown-linux-gnu.tar.gz --repo codyspate/khoralang ``` There is no maintainer key to trust and none to leak. The signing identity is the release workflow's own, which is also why nothing is published from anybody's workstation. ## What is in the toolchain Each release includes a **bill of materials** as `khora-.cdx.json`, in CycloneDX 1.5, listing every Rust crate compiled into the toolchain with its version and license, the pinned LLVM, and the Rust toolchain that built it. Dependency scanners read it directly. That document is about the *compiler*. For the other question — what does the program I am building pull in — `khora sbom` renders the same format from your package's own resolution: ```bash khora sbom --out my-app.cdx.json ``` It is rendered from the resolution rather than from a lockfile read off disk, so it describes what a build here would use. Pass `--locked` to refuse a stale lockfile instead of absorbing the difference. ## Linux system requirements The published Linux build is dynamically linked against glibc, and a glibc program cannot run against a library older than the one it was compiled against. The release is built on Ubuntu 24.04, so the published `x86_64-unknown-linux-gnu` build requires **glibc 2.39 or newer**. That is a real constraint and not a rounding error. Distributions in wide use today are on both sides of it: | distribution | glibc | the published build | | --- | --- | --- | | Ubuntu 24.04 LTS | 2.39 | runs | | Debian 13 (trixie) | 2.41 | runs | | Debian 12 (bookworm) | 2.36 | does not run | | Ubuntu 22.04 LTS | 2.35 | does not run | | RHEL 9 and rebuilds | 2.34 | does not run | | Alpine, and anything on musl | not glibc | does not run | `install.sh` checks this before it downloads anything, and refuses with the two numbers rather than installing a toolchain that cannot start. If you are on a distribution below the line, the options are a newer distribution — including a container built on one — or [building from source](#build-the-compiler-from-source), which links the compiler against the glibc you have. Check yours with: ```bash ldd --version | head -1 ``` This is a property of the *toolchain*, not of the programs you compile with it: a program built by Khora is linked against the machine that built it, so building on Ubuntu 24.04 and deploying to Debian 12 has the same problem for the same reason. [Containers](/docs/v0.4/deployment/containers/) says to use the same base image in both stages, which is that advice. ## System linker requirement Khora compiles to native object code, so producing an executable requires the platform's linker, C runtime, and system libraries. You do **not** need to install LLVM separately; LLVM is linked into the Khora compiler rather than invoked as an external program. | Platform | Linker/toolchain | | --- | --- | | macOS | `xcode-select --install` | | Debian / Ubuntu | `apt install clang` or `apt install build-essential` | | Fedora / RHEL | `dnf install clang` or `dnf install gcc` | | Windows | [LLVM](https://releases.llvm.org), or the Visual Studio Build Tools with the **C++ Clang tools for Windows** component | On Linux and macOS any of `clang`, `cc` or `gcc` will do as the driver. Windows needs `clang` specifically: the **Desktop development with C++** workload does not install it by itself — its **C++ Clang tools for Windows** component is separate — and MSVC's own `cl.exe` is not a substitute, because the driver is what locates the C runtime and system libraries. Installing LLVM is the shorter route. The installer checks for a usable linker and tells you if one is missing. ## Update and manage toolchains Use `khora` itself to install and switch compiler versions: ```bash khora update # install the newest release and use it khora toolchain install 0.4.0 # install a particular release khora toolchain default 0.1.0 # select an installed release as the default khora toolchain list # list installed toolchains khora toolchain which # show the version selected here and why ``` Updating does not remove the version it replaces, so rolling back is a toolchain selection rather than a reinstall. ## Pin a project to a compiler version Every project says which compiler builds it, in `khora.toml`: ```toml [toolchain] version = "0.4.0" ``` **The field is required.** A project without it stops with a message carrying the two lines to add — the version, the quoting and the table name are not things anybody should have to guess. A project pin takes precedence over your machine default. If the pinned version is not installed, Khora stops rather than silently building the project with a different compiler. `latest` and `latest.rc` are accepted in place of a version, and mean the newest toolchain installed on this machine, release candidates excluded or included. They resolve whenever they are read, so they are not reproducible — write a version for a project you want built the same way twice. That makes the compiler version part of the reproducible project configuration: ```bash khora toolchain which ``` shows which version the current directory selects. ## Uninstall The default installation lives under `~/.khora`. If you no longer want Khora installed, remove that directory and remove any Khora path entry you added manually. ## Build the compiler from source Building from source is for contributing to Khora itself, testing compiler changes, or working on a platform without a published toolchain artifact. Application developers normally use the installer above. Requirements are Git, a current Rust toolchain, and LLVM 22.1.8 for native code generation. ```bash git clone https://github.com/codyspate/khoralang.git cd khoralang sh scripts/setup-llvm.sh cargo build -p khora-cli --features llvm cargo build -p khora-rt ``` `scripts/setup-llvm.sh` installs or locates the pinned LLVM version and writes the machine-specific Cargo configuration needed by the backend. You can register a compiler you built locally as a Khora toolchain: ```bash khora toolchain link 0.4.0 target/debug/khora ``` ## Release candidates If you intentionally want to test a release candidate, opt into prerelease versions explicitly. **These `next` documentation pages describe 0.4.0, but candidate artifacts are published only after the release page lists them.** Check that page for the current candidate before using a pinned command. On macOS or Linux, choose the newest published version including candidates, or select the 0.4 candidate by name once published: ```bash curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh | sh -s -- --pre curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh | sh -s -- --version 0.4.0-rc.2 ``` The 0.4 candidate has its own version and pin, `0.4.0-rc.2`: replace `version = "0.4.0"` with `version = "0.4.0-rc.2"` in an application manifest for candidate testing, and pin packages from the same release tag. `--version` takes the version without the leading `v`, and works for any published release, candidate or not. On Windows PowerShell, `installrc.ps1` is `install.ps1` with prereleases allowed — PowerShell cannot pass an argument to a script piped into `iex`, so the option is a second file rather than a flag: ```powershell irm https://raw.githubusercontent.com/codyspate/khoralang/main/installrc.ps1 | iex ``` After a Khora toolchain is installed, the normal toolchain commands can also include prereleases: ```bash khora toolchain install --pre khora update --pre ``` `--pre` means prereleases are eligible; it does not prevent the tool from selecting a newer stable release. Release candidates have their own immutable version numbers such as `0.2.0-rc.1` and are not promoted in place. A project built with a candidate pins it by its full name — `khora new` writes the pin for whichever compiler ran it: ```toml [toolchain] version = "0.4.0-rc.2" ``` When you are ready to write code, continue with [Your first Khora project](/docs/v0.4/getting-started/first-project/). # Your first Khora project Source: https://khoralang.com/docs/v0.4/getting-started/first-project/index.md A Khora package has a `khora.toml` manifest and source files, conventionally under `src/`. This walkthrough adds a small function and a test, then formats, checks, tests, builds, and runs the package with the installed `khora` toolchain. If `khora --version` does not work yet, start with [Installation](/docs/v0.4/getting-started/installation/). For the 0.4 release candidate, use its [explicit candidate install](/docs/v0.4/getting-started/installation/#release-candidates) after publication rather than the plain stable installer. `khora new` pins whichever compiler ran it: keep `0.4.0-rc.2` in a candidate project, and pin Khora-repository dependencies to `v0.4.0-rc.2`. The manifest below shows the stable 0.4.0 release, which is installable only after it is published. ## Package structure Create one with `khora new`: ```bash khora new hello_khora cd hello_khora ``` which writes this: ```text hello_khora/ ├── .gitignore ├── khora.toml └── src/ └── main.kh ``` `khora new --lib` writes a library instead. Creating the same files by hand works too; `khora new` only saves the typing and writes the `[toolchain]` pin for the compiler that ran it. `src/` is the convention, but **the package is the manifest's directory**, not `src/` alone: every `.kh` file beside `khora.toml` or under any directory below it is compiled. A scratch file dropped at the package root is part of the package and its errors are reported against it. Three directories are left out of that walk: `target/`, `.git/`, and any directory with a `khora.toml` of its own — so a nested package or a vendored copy stays its own package rather than being absorbed into its parent. `src/bin/` is left out too, but only of the package's own compilation: each file there is built as a program of its own against the package's modules, and is still checked. Delete a scratch file or move it under a directory with its own `khora.toml`; there is no ignore list. The manifest names the package and says which Khora builds it: ```toml [package] name = "hello_khora" version = "0.1.0" # Which Khora builds this project. Required. [toolchain] version = "0.4.0" ``` The `[toolchain]` version is required, and it is what makes a project build the same way on the next machine: run any command here with a different Khora on your path and it hands the command over to the one this project asks for. Every key is in [the manifest reference](/docs/v0.4/reference/manifest/). ## Write some Khora Put this in `src/main.kh`: ```khora module hello_khora::main; import std::core::{assert, print}; fn double(value: Int) -> Int { value * 2 } test "double returns twice its input" { assert(double(21) == 42); } pub fn main() -> Int { print("Hello, Khora!"); 0 } ``` The [Language Reference](/docs/v0.4/reference/) goes deeper on each construct once the workflow is running. ## Check before building From the package root: ```bash khora check . ``` Use `check` constantly while you work. It validates the package without paying the cost of native code generation and linking. ## Format Khora has one canonical formatter: ```bash khora fmt . ``` It keeps the line breaks you chose and normalizes everything else: indentation, spacing, sorted import lists, and a comma after the last element of any list broken across lines, so adding an element changes one line. A tuple or a parenthesized expression never gets one, because `(e,)` is a one-tuple. The formatter changes no other token. In CI, check formatting without changing files: ```bash khora fmt . --check ``` See [Values and functions](/docs/v0.4/reference/expressions/) for the core expression and function model. ## Run the tests ```bash khora test . ``` The test runner discovers `test` blocks in the package and reports failures through the CLI. For testing patterns around capabilities, typed failure, and cancellation, see [Testing](/docs/v0.4/reference/testing/). ## Document what you wrote ```bash khora doc ``` Run inside a package, this reads the `///` and `//!` comments from its `src` and writes a page per module into `docs/api` beside the `khora.toml`. Name sources or a destination when you want different ones, as in `khora doc src --out reference`. It keeps a `.khora-doc` record beside the pages listing the ones it generated. That is what lets a later run delete the page of a module you removed without touching anything it did not write, so a hand-written file in the same directory is safe and is reported rather than removed. `khora doc --check` writes nothing and fails when a checked-in page no longer matches its source, which is how a project keeps generated documentation honest in CI. ## Build a native executable ```bash khora build . ``` The program goes in `build/`, named after the package, so nothing a build makes lands among your sources. `khora new` writes a `.gitignore` with `build/` in it for exactly that reason. Run it on macOS or Linux: ```bash ./build/hello_khora ``` Or on Windows: ```powershell .\build\hello_khora.exe ``` `--out` puts it somewhere else, under whatever name you give it: ```bash khora build . --out dist/hello ``` You should see: ```text Hello, Khora! ``` For an optimized build, add `--release`: ```bash khora build . --release ``` ## In an editor The language server uses the same compiler queries as `khora check`, so editor diagnostics and command-line diagnostics stay aligned. Continue with [Editor setup](/docs/v0.4/getting-started/editor/). ## What to learn next A useful path from here is: 1. [Expressions](/docs/v0.4/reference/expressions/) — values, functions, lambdas, pipelines 2. [Types](/docs/v0.4/reference/types/) — records, variants, wrappers, generics 3. [Patterns](/docs/v0.4/reference/patterns/) — matching and destructuring 4. [Failures](/docs/v0.4/reference/failures/) — `raises`, `!`, `catch`, `attempt` 5. [Capabilities](/docs/v0.4/reference/capabilities/) — `effect`, `handler`, `with`, `context` 6. [Concurrency](/docs/v0.4/reference/concurrency/) — fibers, nurseries, cancellation For exact declarations while you work, browse the [Standard Library](/docs/v0.4/stdlib/) reference. # Editor setup Source: https://khoralang.com/docs/v0.4/getting-started/editor/index.md Khora ships one language server as part of the compiler toolchain: `khora lsp`. It is backed by the same compiler queries used by `khora check`, so the diagnostics and type information you see in your editor come from the compiler itself. Your editor should launch the server for `.kh` files; you normally do not need to run it in a terminal yourself. ## VS Code The extension is not on the Marketplace yet. Install it from a release: 1. Download `khora-vscode-.vsix` from the newest `vscode-v*` release on GitHub — the `vscode-v0.3.0` release publishes `khora-vscode-0.3.0.vsix`. The extension is versioned separately from the compiler, so that number is not a Khora toolchain version. 2. Run `code --install-extension khora-vscode-.vsix`, or use **Extensions: Install from VSIX** in the command palette. It needs `khora` on your `PATH`, which both installers arrange. If it cannot find one it says so and offers to open the `khora.server.path` setting rather than failing quietly. Format-on-save is turned on for `.kh` files only. The status bar shows which toolchain answered, and turns yellow when a project pins a version that is not installed. ## Language server command Configure an LSP client with: ```text command: khora args: lsp ``` The server currently provides: - compiler diagnostics and hover information; - formatting; - completion and signature help; - go-to-definition and find-references; - document and workspace symbols; - semantic tokens; - code actions and code lenses; - highlighting every mention of the name under the cursor; - go to the *type* of an expression, and to every `impl` of a type or trait; - folding, and expand-selection through the tree; - inlay hints: the capability and failure rows a call needs, and the inferred type of a binding that does not say its own; - rename, across every file that names the thing, when the declaration and its uses belong to the opened project. Files in the package store or another project's dependency directory cannot be edited this way. Quick fixes are offered only where a diagnostic's own message names one edit and there is nothing to choose, because an action is applied by somebody who read four words of it. Seven qualify today: adding the `!` a call needs; writing out the trait members an impl has not, with their signatures copied from the trait and `Self` swapped for the type being implemented; writing every missing `match` arm at once, qualified the way the arms already there are, with `todo()` for a body; removing an unused import together with one separating comma; renaming an unused binding to `_name`; taking the spelling a "did you mean" suggests; and adding a record's missing field, again with `todo()`. The message that says a call needs a capability the function does not require offers the signature edit and nothing beside it, because propagating the requirement outwards is one of two answers and only that one is spelled out. Inside a `with { .. }`, completion offers whole handlers: typing an effect's name inserts `clock: handler for Clock { unix_seconds: fn () => todo(), unix_millis: fn () => todo(), monotonic_millis: fn () => todo(), sleep: fn a => todo() }`, with a closure of the right arity for every operation the effect declares. Where the code inside the block still needs a capability, the entry is labeled the way the requirement asked for it and sorted first. A signature's `with` clause is a different row and offers types instead. Completion offers every public name in the workspace, not only what the file has imported, and accepting one that is not in scope writes the `import` with it: merged into an existing import of that module, or placed in sorted order among the others. Names from elsewhere sort below everything already in scope. The documentation for one is fetched when you highlight it rather than for the whole list, which is what keeps the list fast against a workspace the size of `std`. Assists are the other half, and answer a different question: not what is wrong, but what you want done where the cursor is. A `let` with no annotation offers to write its inferred type down as text, which an inlay hint can only draw. A selected expression offers to become a `let` above the statement it was in. That one refuses where lifting it would cross something conditional, an `if` branch, a `match` arm, a lambda body, or the far side of `&&`, because running code the program said to skip is not a refactoring. A selection also offers to become a **function**, and that one is offered in all the places the `let` refuses: hoisting an expression runs it earlier, and a call left where the expression was runs at exactly the same moment. Blocks count here too, so selecting a run of statements and naming them is one keystroke. What it writes is the interesting part. Parameters are the bindings the selection uses and does not declare, typed from the checker; the return type is the selection's own; and the `with` and `raises` clauses are written from what the calls inside actually demanded. So extracting three lines that reach a database and can fail produces ```khora fn extracted(id: Int) -> Row with { db: Db } raises DbError { one_row(db.query("select .. where id = $1", [Cell::Number(id)])!)! } ``` with the call site given its `!`. Nothing about the selection said `Db` — the checker recorded what each call needed while it was type-checking, and the assist reads it back rather than working it out again. It refuses a selection containing a `with` block or a `catch`, because those *answer* a row and the signature would then over-state what escapes; and a block that assigns to a binding it did not declare, because a parameter is a value and the write would land on a copy. "Apply idiomatic fixes" (`source.fixAll.khora`) rewrites everything the `idiomatic` lint group reports in the file, exactly as `khora check --fix` does, and only when the project has switched the group on. It is a source action, so an editor runs it on save when asked to, and it is not offered in the lightbulb menu. In VS Code: ```json "editor.codeActionsOnSave": { "source.fixAll": "explicit" } ``` The rewrites are listed in [Lints](/docs/v0.4/reference/lints/#fixing-what-they-find). A code lens marks what a function absorbs rather than passes on: `installs { db } · catches DbError` above a function whose signature mentions neither. Rows are transitive, so a lens repeating a signature would be noise; a `with` block and a `catch` are the two places that stops being true, and they are what the type system deliberately hides. Rename edits the declaration, every use, and the import that brings the name into each file when those files belong to the opened project. This includes path dependencies inside the opened folder. The server refuses renames that would edit files in the shared package store or another project's dependency directory, including private modules outside a package's published API. Where a file imports under an alias, the import's original name is renamed and the alias is left alone, because the alias is that file's own word for it. Renaming a function's parameter also renames every [labeled argument](/docs/v0.4/reference/expressions/#labeled-arguments) written against it, in every file; for a trait method that happens when the parameter is renamed in the trait's declaration, and a parameter renamed in an `impl` changes only that body, because callers label with the trait's names. A rename whose new name is not an identifier, is `_`, `self` or a reserved word, or is already bound in the same function is refused with a message saying which, and no file is changed: a name already bound there would be captured, so the renamed uses would read the other binding and the program would still compile. A trait member and a constructor are still refused, each with a sentence saying why: a trait member's name belongs to the trait and to every impl of it, and a constructor has no recorded range to edit. The server loads the opened project's source files, the standard library, and the dependencies declared in its manifest: git dependencies from the lockfile and the local package store, path dependencies from their directories. It never fetches a package or changes the manifest or lockfile. If a dependency is not installed, it shows a message naming the command to run (`khora check` or `khora install`) and keeps checking everything that does not need it. Restart the server after installing a dependency. Files under the folder you opened are yours, including path packages vendored inside it, and get diagnostics and edits as usual. Dependency code is not: files in the package store, and path dependencies outside the opened folder, get no diagnostics and no edits. If the opened folder's `khora.toml` cannot be parsed, the server cannot tell which outside files are dependencies, so it treats every file outside the folder as read-only until the manifest is fixed and the server restarted. The server asks for incremental synchronization, so an edit sends the edit rather than the file. The installed toolchain is the only server installation you need. Updating or pinning Khora also selects the compiler behavior your editor sees for that project. ## Formatting Use Khora's formatter rather than editor-specific formatting rules: ```bash khora fmt . ``` For CI: ```bash khora fmt . --check ``` Editors that support LSP formatting can delegate format-on-save to `khora lsp`. ## Editor clients Any editor with a Language Server Protocol client can launch `khora lsp`. The repository ships working configuration for VS Code, Helix and Neovim. Emacs and Sublime Text have LSP snippets in `editors/README.md` that nobody has run; they are a starting point rather than a supported setup. Zed needs an extension compiled to WebAssembly and has none. The important part is always the same: the project root contains `khora.toml`, `.kh` files are associated with Khora, and the editor starts `khora lsp`. ## AI coding tools Khora also exposes a compiler-backed MCP server. A coding agent can use it to ask the real Khora compiler about source instead of guessing syntax or type behavior. The same standard-library index is on the command line as `khora std search `, so it is not an agent-only facility. Both read the compiler's own view of the `std` beside them rather than a checked-in list, which is why neither can describe a version you do not have. It is optional. Nothing about writing, building or testing correct Khora requires it, and the compiler and language server behave identically whether or not an agent is connected. For documentation rather than compiler queries, use [/llms.txt](/llms.txt) to find pages and [/llms-full.txt](/llms-full.txt) to read them together. These files cover the current stable release; [next](/docs/next/llms.txt) covers the unreleased compiler. A typical MCP client configuration is: ```json { "mcpServers": { "khora": { "command": "khora", "args": ["mcp"] } } } ``` As with the language server, the client starts the process for you. You normally do not run `khora mcp` interactively. ## Next - [Your first Khora project](/docs/v0.4/getting-started/first-project/) shows the command-line workflow the editor complements. - The [Language reference](/docs/v0.4/reference/) covers the language features the server is checking, and is the lookup-oriented companion when you need exact syntax or semantics. # Language reference Source: https://khoralang.com/docs/v0.4/reference/index.md Every Khora construct, one page per topic. If you know the name of the thing you are looking for, this section should answer "what does it look like, and what are the rules?" without reading compiler source or a design document. Every construct has concrete Khora code here — a construct should never exist only in prose or only in the parser. ## Reading it the first time The pages are lookup-oriented, but they are ordered so that a straight read works. Each opens with what the construct is for before it lists the shapes it accepts. **The syntax of ordinary programs** — [Expressions](/docs/v0.4/reference/expressions/), [Control flow](/docs/v0.4/reference/control-flow/), [Types](/docs/v0.4/reference/types/), [Patterns](/docs/v0.4/reference/patterns/), [Generics](/docs/v0.4/reference/generics/) and [Traits](/docs/v0.4/reference/traits/). **What makes Khora different** — [Failures](/docs/v0.4/reference/failures/): what may go wrong is in the type. Then [Effects and rows](/docs/v0.4/reference/effects/) and [Capabilities](/docs/v0.4/reference/capabilities/): what authority a function needs is in the type too, and the two rows are independent. **Lifetimes and concurrent work** — [Memory and resources](/docs/v0.4/reference/memory-and-resources/), [Concurrency](/docs/v0.4/reference/concurrency/), [Sharing](/docs/v0.4/reference/sharing/). If you have not built a Khora program yet, start with [Getting Started](/docs/v0.4/getting-started/) and come back once the toolchain workflow works. Arriving from TypeScript + Effect, Go or Rust, the [migration pages](/docs/v0.4/migration/) map familiar concepts onto these. The rest of this page is the full index, by topic. ## Source and declarations - [Lexical structure](/docs/v0.4/reference/lexical-structure/) — identifiers, row variables, numeric/string literals, interpolation, comments, keywords, and punctuation. - [Declarations](/docs/v0.4/reference/declarations/) — `module`, `import`, `pub`, `const`, `type`, `derive`, `fn`, `extern fn`, `effect`, `context`, `trait`, `impl`, `test`, and `bench`. - [Grammar and precedence](/docs/v0.4/reference/grammar/) — compact grammar shapes, operator precedence, and parsing rules that disambiguate similar forms. ## Expressions and control flow - [Expressions](/docs/v0.4/reference/expressions/) — literals, calls, fields, records, record update, tuples, lists, blocks, `let`, assignment, lambdas, `|>`, `||>`, postfix `!`, `catch`, handlers, and `with`. - [Control flow](/docs/v0.4/reference/control-flow/) — `if`, `match`, guards, `while`, `for`, `loop`, `break`, `continue`, `return`, and typed-failure exits. - [Patterns](/docs/v0.4/reference/patterns/) — wildcard, binding, literal, constructor, record, tuple, guarded, `let`, `for`, and `catch` patterns. ## Types and abstraction - [Types](/docs/v0.4/reference/types/) — path, unit, tuple, record, mutable-field, variant, function, row, generic, literal, opaque, wrapper, and `forall` types. - [Generics](/docs/v0.4/reference/generics/) — type parameters, bounds, const generics, row variables, higher-kinded use, explicit `forall`, and variance. - [Traits](/docs/v0.4/reference/traits/) — trait declarations, supertraits, associated types, trait `impl`, inherent `impl`, bounds, and `derive(...)`. ## Effects, authority, and failure - [Effects and rows](/docs/v0.4/reference/effects/) — `effect` declarations and the relationship between `with` and `raises` rows. - [Capabilities](/docs/v0.4/reference/capabilities/) — handlers, capability rows, postfix/block `with`, named `context`, sequential composition, and overrides. - [Failures](/docs/v0.4/reference/failures/) — `raises`, `raise`, postfix `!`, pattern-based `catch`, failure translation, `attempt`, and collecting every reason rather than the first. ## Runtime-facing language rules - [Memory and resources](/docs/v0.4/reference/memory-and-resources/) — `Region`, `Scope`, `scoped`, `acquire`, finalizer order, and cleanup on structured exits. - [Sharing](/docs/v0.4/reference/sharing/) — `Share`, `Shared`, `Changed`, `Channel`, `SharedFn`, critical-section rules, and cross-fiber values. - [Concurrency](/docs/v0.4/reference/concurrency/) — `Fiber`, `Nursery`, `nursery`, `bounded_nursery`, cancellation, suspension, and structured ownership. - [Traps](/docs/v0.4/reference/traps/) — checked overflow, bounds failures, process-fatal behavior, backtraces, and exported-call containment. - [FFI](/docs/v0.4/reference/ffi/) — importing with `extern fn`, exporting with `pub extern fn`, `Ptr`, borrowed buffers, and the C ABI boundary. ## Working on a program - [Modules and packages](/docs/v0.4/reference/modules-and-packages/) — `src/bin`, dependencies, the lockfile, publishing, and package boundaries. - [The manifest](/docs/v0.4/reference/manifest/) — every table in `khora.toml`: the required `[toolchain]` pin, `[package]`, workspaces, permissions, dependencies, lints, and tasks. - [Testing and benchmarks](/docs/v0.4/reference/testing/) — `khora test` and `khora bench`, supplying capabilities to a test, build profiles, and CI. - [Lints](/docs/v0.4/reference/lints/) — the 21 checks `khora check` runs, their default levels, and how to set them in `[lints]`. - [Debugging a program](/docs/v0.4/reference/debugging/) — backtraces, debug information, what a debugger can and cannot be relied on for. - [Compatibility and stability](/docs/v0.4/reference/compatibility/) — what `0.x` promises, what counts as a breaking change, and what 1.0 is waiting for. - [Environment variables](/docs/v0.4/reference/environment/) — every `KHORA_*` you may set: where Khora keeps its cache and toolchains, which profile a build uses, and the runtime's fiber backend. ## Where the other sections start The Reference is the language. Two other sections answer different questions about the same program: - The [Standard Library](/docs/v0.4/stdlib/) is what ships with the toolchain — prose for the modules that need it, and generated pages carrying every exported declaration. The Reference gives the syntax of a capability or a fiber; the library reference gives the complete operations of `Random`, `Clock`, `Shared`, `Channel`, `List`, the HTTP types and the rest. - The [Cookbook](/docs/v0.4/cookbook/) is a whole task, working, end to end — bounded concurrency, a database transaction, decoding untrusted input. # Lexical structure Source: https://khoralang.com/docs/v0.4/reference/lexical-structure/index.md Khora source is UTF-8 text. Whitespace and comments separate tokens where needed; the syntax tree retains trivia so formatting and documentation tools can preserve source faithfully. ## Identifiers Ordinary identifiers begin with an ASCII letter or `_` and continue with ASCII letters, digits, or `_`: ```khora user user_id HTTP2 _internal ``` Hard keywords cannot be used as identifiers. ## Character literals One Unicode scalar value between apostrophes: ```khora 'a' 'é' '\n' '\u{1F600}' ``` The escapes are the string ones: `\n`, `\r`, `\t`, `\0`, `\\`, `\'`, `\"`, and `\u{...}` for a code point in hexadecimal. **Exactly one character.** `''` is not a literal and neither is `'ab'` — a `String` is what holds any number of characters. The apostrophe also begins a row variable, and the two are told apart by the closing quote: `'a'` is a character and `'a` is a row variable. A generic list such as `<'a, 'b>` is two row variables, not one character containing a comma. ## Row variables A row variable begins with `'` followed by an identifier: ```khora 'ef 'raises 'capabilities ``` The spelling after `'` has no built-in semantic meaning. ## Integer literals ```khora 0 42 1_000_000 ``` Underscores may be used as visual separators. ## Floating-point literals ```khora 0.5 3.14159 6.02e23 1.0E-6 ``` A fractional literal without the decimal suffix is an IEEE `Float`. **An exponent needs a fractional part in front of it.** `6.02e23` and `1.0E-6` are literals; `1e19` is not, and is reported as ``expected `;` `` — the lexer stops at the `1` and the `e19` that follows is not part of the number. Write `1.0e19`. The same rule applies to a `d` suffix: `1.25e3d` is a `Decimal` literal and `1e19d` is a parse error. ## Decimal literals Append `d` directly to the number for an exact `Decimal` literal: ```khora 0d 0.01d 19.99d 1.25e3d ``` `d` is Khora's literal suffix; no whitespace may appear between the number and suffix. The module needs `import std::decimal::{Decimal};` for the literal to have a type. A fractional literal *without* the suffix is a `Float`: ```khora let approximate = 0.1; let exact = 0.1d; ``` The distinction is visible in the source so a reader can tell whether an API does approximate binary arithmetic or exact decimal arithmetic. [Decimals](/docs/v0.4/stdlib/decimal/) is the rest of it. ## Boolean literals ```khora true false ``` ## Quoted strings ```khora "Khora" "first\nsecond" "say \"hello\"" ``` Strings contain Unicode text. The escapes are `\n`, `\r`, `\t`, `\0`, `\\`, `\'`, `\"`, and `\u{...}` for a code point in hexadecimal: ```khora "caf\u{e9}" // café "snowman \u{2603}" // snowman ☃ ``` `\u{...}` takes one to six hexadecimal digits. A code point that is not a character — a surrogate, or anything above `10FFFF` — is refused at compile time rather than producing a string that is not UTF-8. ## String interpolation `${...}` evaluates an expression inside a string: ```khora "user ${user.id}" "count = ${count}" "the point is ${point} and the list is ${items}" ``` Interpolation is expression syntax, not a separate formatting language. A hole that already holds a `String` is used as it stands. Anything else is shown through [`Show`](/docs/v0.4/reference/traits/), so a value whose type has no `Show` impl is a compile error naming the type: ``` error: `Color` has no `Show`, so it cannot go in a `${..}` hole. Write `derive(Show)` on it, or `impl Show for Color` ``` `Show` does not have to be imported to interpolate. The hole is the use, and the trait is never named in the source. The value's *type* does have to be imported, though: a hole over a `List` in a module that never wrote `List` reports the same "has no `Show`" error even though `List` implements it. **A quote inside a hole is written unescaped.** `${...}` is scanned as source, not as string content, so the inner literal's quotes stand as they are: ```khora "len = ${String::byte_length("abc")}" // correct "len = ${String::byte_length(\"abc\")}" // refused ``` The escaped form is refused by name: ``` error: a `${..}` hole is scanned as source, not as string text, so its quotes stand as they are: write `"` here, not `\"` ``` A hole that fails to parse for some other reason gets the general message, ``error: this `${..}` does not contain an expression``, with the hole underlined. Binding the value to a `let` on the line before is the way out of one that will not parse. Interpolation is for text a person will read. Where another program consumes the output, reach for a structured encoder such as `std::json` instead — a value that renders one way for a human is not a serialization format. ## Backtick strings Backticks delimit multiline strings: ```khora let query = ` select id, name from users where active = true `; ``` `${...}` interpolation and backslash escapes work in backtick strings too. A literal backtick is escaped with `\``. When a multiline backtick literal opens on its own line, common source indentation is stripped so the surrounding code can remain indented without changing the text's intended layout. Three things are removed, and the last two are the ones that surprise people: - the **common indentation**, measured over the whole body and taken off each line, so the deepest-indented line keeps whatever it has beyond the shallowest; - the newline **immediately after the opening backtick**; - the newline and indentation **before the closing backtick**. So this literal is thirty bytes and three lines, with no leading or trailing newline: ```khora fn block() -> String { ` line one indented line three ` } ``` It is `"line one\n indented\nline three"`. `indented` keeps two spaces because the common prefix was two, not four. A literal that should *end* with a newline needs a blank line before the closing backtick, and one that should start with a newline needs a blank line after the opening one. None of it applies when the content starts on the opening line, which is the form to reach for when the text must be taken exactly as written: ```khora let raw = `first second`; ``` ## Line comments ```khora // ordinary comment ``` API documentation uses three slashes: ```khora /// Returns the user for `id`. pub fn load_user(id: Id) -> User; ``` Module documentation uses `//!`: ```khora //! User-domain types and operations. module app::users; ``` `khora doc` reads `///` and `//!` comments as Markdown documentation. ## Block comments ```khora /* a block comment */ ``` Block comments may nest: ```khora /* outer /* inner */ outer again */ ``` ## Hard keywords These words are reserved: ```text module import type trait impl fn match let mut pub as if else forall const effect with raises raise catch while loop break continue for return true false ``` ## Contextual keywords These words are keywords only in their grammatical position and remain usable as ordinary identifiers elsewhere: ```text handler in capability row test bench derive extern ``` Examples of their keyword positions: ```khora handler for Clock { now: fn () => fixed_instant } for item in items { process(item); } context Production { clock: live_clock } test "works" { assert(true); } bench "parse" { parse(fixture); } derive(Eq, Show) type Point = { x: Int, y: Int }; extern fn strlen(ptr: Ptr) -> U64; ``` `in` is contextual in the `for pattern in expression` form. ## Reserved words These words are refused as identifiers and mean nothing yet: ```text where yield macro unsafe unstable ``` Using one as a name is an error naming the word, not a syntax error: ```text error: `yield` is reserved for a future version of Khora and cannot be used as a name. Nothing uses it yet -- it is held back because Khora has no editions, so a word claimed after 1.0 would break every program that had named something with it ``` They are held back because Khora has no editions and no `unstable` marker, so a keyword introduced after 1.0 would break every program that had used the word as a name, with no mechanism to migrate one. Reserving a word now costs one name; reserving it afterwards is not possible at all. **A reservation is an option, not a promise.** None of these words is scheduled, and the spellings are not fixed — if generators arrive written some other way, `yield` will have been held back for nothing. The list is short for that reason. They are also **not keywords**: an editor will not highlight them, because there is no construct to highlight. ## Punctuation with language meaning Common multi-character tokens include: ```text :: compile-time path separator -> function/type arrow => match arm or lambda separator |> pipeline ||> flow lambda == != <= >= && || ``` Single-character punctuation includes `; , . : | = + - * / % ! < > ( ) { } [ ] _`. See [Expressions](/docs/v0.4/reference/expressions/) for operator precedence and [Declarations](/docs/v0.4/reference/declarations/) for the positions in which the declaration-oriented keywords appear. # Declarations Source: https://khoralang.com/docs/v0.4/reference/declarations/index.md This page lists the declaration forms accepted at module scope. Local bindings and expression forms are covered in [Expressions](/docs/v0.4/reference/expressions/). ## Module declaration ```khora module app::users; ``` A module path uses `::` between segments. The module declaration belongs at the beginning of the source file. ## Imports Selected names: ```khora import std::core::{List, Option, print}; ``` Aliased name: ```khora 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};`. ### 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 = 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](/docs/v0.4/reference/traits/#trait-scope-and-resolution). 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 = Option::None;` does not. `F32`, `F64`, `I128` and `U128` are not among them and are not types Khora has. ## Visibility `pub` makes a declaration visible outside its declaring module: ```khora 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 ```khora const NAME = "khora"; pub const DEFAULT_PORT: Int = 8080; ``` General form: ```text 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 Named definition over an existing type, and record definition: ```khora 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](/docs/v0.4/reference/types/#field-visibility) has the whole rule. Variant type: ```khora pub type Result = | 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](/docs/v0.4/reference/types/#wrappers-and-named-data): 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: ```khora pub type Handle; ``` General form: ```text derive(...)? pub? type Name? (= 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. ## `derive(...)` A derive clause appears immediately before the type it applies to: ```khora 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` 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` has `Show` — it prints `` — 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. ## Function declarations Definition: ```khora pub fn add(left: Int, right: Int) -> Int { left + right } ``` Generic and effectful definition: ```khora pub fn load(id: Id) -> A with { store: Store } raises StoreError { store.load(id)! } ``` Signature without a body: ```khora fn intrinsic(value: Int) -> Int; ``` General form: ```text pub? fn Name? (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(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](/docs/v0.4/reference/capabilities/) and [Failures](/docs/v0.4/reference/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 Declare a C symbol with contextual `extern` before `fn`: ```khora extern fn strlen(ptr: Ptr) -> U64; ``` A public external function can form part of a generated C-compatible library surface: ```khora pub extern fn khora_add(left: Int, right: Int) -> Int; ``` See [FFI](/docs/v0.4/reference/ffi/) for ABI rules and supported boundary types. ## Effect declarations ```khora pub effect Store { load: Id -> User raises StoreError, save: User -> () raises StoreError, } ``` General form: ```text pub? effect Name? { operation: FunctionType, ... } ``` Effect members are named function types separated by commas. ## Context declarations ```khora pub context Production { scope: Scope::root(), db: postgres_db()!, store: sql_store(), } ``` General form: ```text pub? context Name { label: Expr, ... } ``` Bindings are evaluated in order; a later binding may use capabilities introduced above it. ## Trait declarations ```khora pub trait Named { fn name(self) -> String; } ``` With supertraits and an associated type: ```khora pub trait Stored: Eq + Show { type Id; fn id(self) -> Self::Id; } ``` Trait functions may be signatures or have default bodies. ## Implementations Trait implementation: ```khora impl Named for User { fn name(self) -> String { self.name } } ``` Inherent implementation: ```khora impl User { pub fn display_name(self) -> String { self.name } } ``` Generic implementations may put type parameters after `impl`: ```khora impl Show for Box { fn show(self) -> String { Show::show(self.value) } } ``` ## Associated type definitions Inside a trait: ```khora type Item; type Item: Show; ``` Inside an implementation: ```khora type Item = User; ``` ## Tests ```khora test "addition works" { assert(add(20, 22) == 42); } ``` General form: ```text 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. ## Benchmarks ```khora bench "parse payload" { parse_payload(fixture)!; } ``` General form: ```text bench "name" Block ``` `test`, `bench`, `context`, `extern`, and `derive` are contextual keywords in their declaration positions rather than globally reserved identifiers. # Grammar and precedence Source: https://khoralang.com/docs/v0.4/reference/grammar/index.md 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. ## Declaration starts A source file is a sequence of declarations. Common forms are: ```text module Path ; import Path::{...} ; pub? const Pattern (: Type)? = Expr ; derive(...)? pub? type Name? (= pub? TypeDef)? ; pub? fn Name? (Params) (-> Type)? (with Row)? (raises Type)? (Block | ;) pub? effect Name? { Field, ... } pub? context Name { name: Expr, ... } pub? trait Name? (: Bounds)? { TraitItem* } impl? 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](/docs/v0.4/reference/types/#field-visibility). ## Paths and fields Compile-time path: ```khora app::model::User Result::Ok ``` Runtime projection: ```khora user.name response.status ``` `::` and `.` are intentionally separate tokens with separate roles. ## Type grammar shapes Named/generic type: ```khora User List ``` Tuple and unit: ```khora () (Int, String) ``` Record: ```khora { id: Int, name: String } ``` Record update, in expression position — the base first, then the fields that replace its own: ```khora { ..base } { ..base, id: 7 } { ..base, id: 7, name: "Grace" } ``` Open row: ```khora { db: Db | 'ef } ``` Function type: ```khora Request -> Response with { db: Db } raises DbError + HttpError ``` Explicit polymorphism: ```khora forall. A -> A ``` Variant type: ```khora | None | Some(value: A) ``` ## Generic parameter grammar Ordinary parameter: ```khora ``` Bound: ```khora ``` Const parameter: ```khora ``` Row parameter: ```khora <'ef> ``` Variance: ```khora <+A> <-A> ``` These forms may be mixed in one parameter list. ## Primary expression forms ```khora 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 ``` ## Postfix forms Starting from an expression, postfix forms bind tightly and may chain: ```khora 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. ## Lambda grammar Single parameter: ```khora fn value => expression ``` Ignored parameter: ```khora fn _ => expression ``` Parameter list: ```khora fn (left: Int, right: Int) => left + right ``` Block body: ```khora fn value => { let next = value + 1; next * 2 } ``` ## Pipeline grammar First argument: ```khora value |> f(a) ``` Explicit insertion point: ```khora value |> f(a, _, b) ``` A stage may contain one `_` placeholder. Flow lambda: ```khora ||> normalize |> validate! |> persist! ``` `||>` starts a unary lambda; following `|>` stages belong to that flow expression until grouping ends. ## Operator precedence 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: ```khora x = value + 1 |> double ``` parses as assigning the result of the full pipeline to `x`. ## Blocks versus record literals These braces are a record because they begin with fields: ```khora { id: 1, name: "Ada" } ``` These braces are a block: ```khora { 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. ## Match arms ```text Pattern (if Expr)? => Expr ,? ``` Examples: ```khora Result::Ok(value) => value, value if value > 0 => value, _ => 0, ``` ## Pattern shapes ```text _ Literal identifier Path Path(Pattern, ...) Path { field, field: Pattern, ... } (Pattern, ...) ``` See [Patterns](/docs/v0.4/reference/patterns/) for binding and exhaustiveness rules. ## Numeric literals ```khora 1_000 0.25 6.02e23 19.99d ``` The `d` suffix creates an exact `Decimal`; a fractional literal without it is `Float`. ## Semicolons A semicolon terminates declarations such as `const`, `type`, imports, and signature-only functions, and turns an expression inside a block into a statement: ```khora 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 `};`. # Expressions Source: https://khoralang.com/docs/v0.4/reference/expressions/index.md Khora is expression-oriented. Literals, calls, blocks, conditionals, matches, pipelines, lambdas, handlers, and control-flow forms all appear in expression position according to their type. ## Literals ```khora 42 1_000_000 3.14 6.02e23 19.99d true false "hello" `multiline text` ``` See [Lexical structure](/docs/v0.4/reference/lexical-structure/) for exact literal and interpolation forms. ## Paths and names ```khora value app::model::User Result::Ok ``` `::` is compile-time namespacing for modules, types, constructors, and associated items. ## Calls ```khora parse(input) connect(host, port) ``` Arguments are positional and may have a trailing comma. ### Labeled arguments At a call to a named function, an argument may be written `name: value`, where `name` is the name the function's declaration gives that parameter. The label is checked and changes nothing else: arguments are still matched to parameters by position, and still evaluated left to right in the order written. ```khora fn reply(connection: Connection, response: String, keep_alive: Bool) -> String { response } reply(connection, "ok", keep_alive: false) reply(connection: c, response: "ok", keep_alive: true) ``` - **Any argument may be labeled, or none.** Labeled and unlabeled arguments mix freely in any combination, because a label never moves anything. A label is never required. - **A label must name the parameter at its position.** `reply(c, keep_alive: true, response: "ok")` is refused even though both names exist, and the error says which position each name has. - A label naming no parameter is refused. So is a label written twice, since it cannot match both positions, and a label on a parameter declared `_`. `_:` is not a label. - **Methods.** In `Type::f(x, a)` the receiver is parameter 1 and is not labeled; `a` is parameter 2 and may be. The same holds in the second form, `x.f(a)`. `self:` is refused: the receiver is written one way. - **Pipes.** `x |> f(b: 1)` puts `x` in parameter 1, or in the `_` slot, and `b:` is checked against the parameter it lands in. - **Traits.** A trait method's labels are the trait's parameter names, however the method is reached: `Trait::m(x, ..)`, `Type::m(x, ..)` or `x.m(..)`. An `impl` may name its parameters differently; those names are local to its body. - **Constructors.** A case with a named payload, `| A(v: Int, w: Bool)`, takes labels exactly as a function does. A positional payload, `| B(Int)`, takes none. Patterns are unchanged. - **Calls through a value take no labels.** A function type such as `(Int, Bool) -> ()` names no parameters, so a label at a call through a local, a parameter, a field, a closure or an effect operation is refused. - `extern fn` declarations are labeled by their declared names. Labels do not reach C. There are no default arguments, and a label cannot skip or reorder one. A parameter's name is part of a public function's interface once any caller labels it: see [Compatibility](/docs/v0.4/reference/compatibility/). ## Field projection and method calls ```khora user.name response.status User::display_name(user) ``` `.` reads a field of a runtime value. It is distinct from `::` path lookup. **A method is called through its owner:** `User::display_name(user)`, with the receiver as the first argument. The owner is the type that declares the method, or the trait for a trait's method: `Show::show(n)`, and inside `fn f(x: A)`, `Show::show(x)`, since a type parameter has no `A::` to write. The qualified call names where the function lives, it is the form `|>` composes with (`name |> String::trim`), and it is the one that settles a name two traits share. `user.display_name()` is the second form of the same call, and it compiles: the receiver is found first and the method looked up on its type, a field holding a function winning over a method of the same name. Both forms evaluate the receiver first and then the arguments, left to right. The `idiomatic` lint group reports the second form as `method-call`, and `khora check --fix` rewrites it to the first; see [Lints](/docs/v0.4/reference/lints/#fixing-what-they-find). ## Record literals ```khora { id: 42, name: "Ada", } ``` A braced form beginning with `name:` is a record literal; an ordinary braced sequence is a block. **`{}` in expression position is a record literal, not an empty block.** It is the value of a record type declared with no fields — `pub type Nothing = {};` — and it only checks where such a type is expected: ```khora pub type Nothing = {}; let nothing: Nothing = {}; ``` Everywhere else it is refused, and the message says so and says what to write instead: ```text error: `{}` is an empty record literal, and no record type here is declared with no fields. Write `()` for a block that does nothing --> src/main.kh:9:21 | 9 | Option::None => {}, | ^^ ``` The place this bites is a `match` arm that should do nothing. `=>` is followed by an *expression*, so `Option::None => {}` is a record literal and is refused. Write `Option::None => ()` for an arm that produces unit. A braced block after `=>` is fine as long as it is not empty: `Option::None => { retire(); }`. ## Record update `{ ..base, field: value }` builds a new record from an existing one. Every field not named comes from `base`: ```khora let renamed = { ..user, name: "Grace" }; ``` It is a **new record**: `user` is unchanged. What the syntax saves is writing out the fields that do not change, which starts to matter at more than a few: ```khora fn applied(counts: Counts, event: Event) -> Counts { match event { Event::Created => { ..counts, created: counts.created + 1 }, Event::Deleted => { ..counts, deleted: counts.deleted + 1 }, Event::Expired => { ..counts, expired: counts.expired + 1 }, } } ``` The base comes first and appears once. A field named twice is an error rather than last-one-wins, so is a field the base's type does not have, and so is a base that is not a record. `{ ..base }` with nothing after it is `base`. **The base's type has to be a record the checker can name.** A type parameter is not one, whatever a caller instantiates it at, so `fn f(x: A) -> A { { ..x, n: 1 } }` is refused. A lambda's parameter can be the base without an annotation when a call fixes its type: `let g = fn c => { ..c, created: 0 };` followed by `g(counts)` is checked as an update of `Counts`. When nothing ever fixes it, the update asks for an annotation, because there is no record to check the fields against. From outside a record's module, an update is subject to [field visibility](/docs/v0.4/reference/types/#field-visibility) like any other way of building one. A [`mut` field](/docs/v0.4/reference/types/#record-types) is the other way to do this. The update produces a new value; assigning a `mut` field changes the one already held. Reach for the update where the old value still matters, and for `mut` where it does not. ## Tuple and unit expressions ```khora () (1, "one") (1,) ``` Parentheses without a comma group an expression: ```khora (value + 1) ``` ## List literals ```khora [] [1, 2, 3] [ "Ada", "Grace", ] ``` **Elements that come and go.** Inside `[..]`, three forms stand for any number of elements: ```khora [header, if debug => trace, for r in rows => render(r), ..footer] ``` `if c => x` is `x` when `c` holds, and nothing otherwise; `else` gives the other case, and `else if` chains. `for p in xs => e` is one `e` per item. `..xs` is every element of the list `xs`. They nest, as in `[for x in xs => if x != 2 => x]`, and they are evaluated left to right, each condition and iterable once. An `else` belongs to the nearest `if`, so `[if a => if b => x else y]` is `y` when `a` holds and `b` does not. `=>` is what makes them elements. `[if c { x } else { y }]` is a one-element list holding the value of an `if` expression, and `[if c { x }]` is a type error unless `x` is `()`: "an `if` without `else` must produce `()`", followed by "inside `[..]`, an element that is only sometimes there is written `if c => x`". A `for` element needs `Step` and `Iterator` in scope, as a `for` loop does, and it is a cancellation point like one. It cannot be left with `break` or `continue`; filter with `if` instead. `return` and `!` work inside elements as they do anywhere in the function, and leave it with the elements made so far freed. Only `List` has a literal. A `match` that makes zero or more elements is written as a spread: `..match o { Option::Some(v) => [v], Option::None => [] }`. **Cost.** The literal is built front to back and reversed once, so it is linear in the elements it makes, and allocates what a `for` that pushes and then calls `reverse` would. A trailing `..xs` is shared rather than copied; a spread anywhere else walks its list. ## Integer division and remainder `/` **truncates toward zero** and `%` **takes the sign of the dividend**, which is what C, Rust, Go and the hardware instruction all do: ```khora (0 - 7) / 2 // -3, not -4 7 / (0 - 2) // -3 (0 - 7) / (0 - 2) // 3 (0 - 7) % 2 // -1 7 % (0 - 2) // 1 (0 - 7) % (0 - 2) // -1 ``` The two agree, so `a == (a / b) * b + (a % b)` holds for every pair that does not trap. `Float::to_int` truncates toward zero for the same reason, and says so. Both trap on a zero divisor rather than answering anything — see [Traps](/docs/v0.4/reference/traps/). For a quotient that rounds rather than truncating, do the rounding in `Decimal`, where the mode is a parameter and not a convention. ## Blocks ```khora { let subtotal = 40; let tax = 2; subtotal + tax } ``` The final expression without a semicolon is the block value. Statements before it are evaluated in order. ## Local bindings ```khora let value = compute(); let value: Int = compute(); let mut count = 0; let (left, right) = pair; ``` General form: ```text let mut? Pattern (: Type)? = Expr ; ``` A binding is immutable unless it says `mut`, and a plain `let` cannot be assigned to later. `mut` is fiber-local mutation only: state that several fibers evolve is a `Shared` boundary instead, and [Sharing](/docs/v0.4/reference/sharing/) says why. `let` is local. Module-level named expressions use `const`; see [Declarations](/docs/v0.4/reference/declarations/#constants). ## Assignment ```khora count = count + 1 ``` Assignment is an expression of type `()` and is right-associative. The target must be writable, such as a `let mut` binding or a mutable record field. ## Lambdas Single parameter: ```khora fn value => value * 2 ``` Ignored parameter: ```khora fn _ => fixed_value ``` Several or annotated parameters: ```khora fn (left: Int, right: Int) => left + right ``` Block body: ```khora fn value => { let doubled = value * 2; doubled + 1 } ``` ## Pipeline `|>` First-argument insertion: ```khora value |> transform(a, b) ``` is equivalent to: ```khora transform(value, a, b) ``` One `_` placeholder selects another argument position: ```khora value |> transform(a, _, b) ``` A stage may contain at most one placeholder. Bare unary function: ```khora value |> normalize |> validate ``` Fallible stage: ```khora value |> parse! |> validate(config)! ``` A pipeline introduces no second error model: `!` still marks the exact call where a typed failure may leave the function, and `catch` still applies either to one stage or to the parenthesized pipeline as a whole. ```khora let user = (raw |> parse! |> validate!) catch { ParseError::Invalid(message) => User::invalid(message), }; ``` ## Flow lambda `||>` `||>` starts a unary anonymous pipeline: ```khora ||> normalize |> validate! |> persist! ``` For example: ```khora items |> List::map( ||> normalize |> validate! ) ``` It is equivalent in shape to `fn value => value |> ...`, and infers its effects, failures and captures the same way. Following `|>` stages belong to the flow lambda until grouping ends, so piping the function value itself takes parentheses: ```khora (||> normalize) |> apply_twice ``` `||>` is always unary. An anonymous function of several parameters is `fn`. A named function needs neither: `items |> List::map(normalize)`. Reach for a pipeline when the value moving through the expression is the thing to follow, and for an ordinary call when the operation itself is the point of the line. ## Operators and precedence From loosest to tightest: 1. assignment `=` — right-associative 2. pipeline `|>` — left-associative 3. logical OR `||` 4. logical AND `&&` 5. comparisons `== != < > <= >=` 6. addition/subtraction `+ -` — `+` also joins two `String`s, which is the only concatenation operator there is; there is no `++` 7. multiplication/division/remainder `* / %` 8. prefix negation `-` and boolean not `!` 9. postfix call, field access, failure `!`, `catch`, and `with` Examples: ```khora value + 1 |> double ready && count > 0 !enabled -total ``` Prefix `!value` is boolean negation. Postfix `call()!` marks failure propagation; position disambiguates them. ## Failure postfix `!` ```khora load_user(id)! ``` The inner call keeps its normal value type while its declared failure row is allowed to leave the current computation. See [Failures](/docs/v0.4/reference/failures/). ## `catch` ```khora load_user(id)! catch { UserError::NotFound(_) => User::guest(), UserError::Unavailable(reason) => User::offline(reason), } ``` `catch` is postfix on the expression whose typed failures it handles, and that expression still needs its `!`. The mark is what lets the failure leave the call; `catch` only says what to do with it once it has. Omitting it is refused: ``` error: `load_user` can leave this function, so the call needs `!`: write `load_user(..)!` ``` ## Postfix capability installation ```khora load_user(id)! with { store: test_store, } ``` A named context can be supplied the same way: ```khora load_user(id)! with Production ``` with overrides: ```khora load_user(id)! with Production { store: test_store, } ``` See [Capabilities](/docs/v0.4/reference/capabilities/). ## Handler expressions ```khora handler for Clock { now: fn () => fixed_instant, } ``` A handler expression produces a value implementing the named effect. ## `with` blocks ```khora with { clock: fixed_clock, store: test_store, } { run_job()! } ``` Named context: ```khora with Production { run_server()! } ``` ## Control-flow expressions The following forms are expressions or block-like expressions and have dedicated rules in [Control flow](/docs/v0.4/reference/control-flow/): ```khora if condition { a } else { b } match value { Pattern => result, } while condition { body; } for pattern in iterable { body; } loop { body; } break break value continue return return value raise error ``` Patterns used by `match`, `for`, destructuring `let`, and `catch` are listed in [Patterns](/docs/v0.4/reference/patterns/). # Control flow Source: https://khoralang.com/docs/v0.4/reference/control-flow/index.md Khora's control-flow forms are expressions or block-like expressions. This page records their accepted shapes and result rules. ## Blocks ```khora { let value = compute(); value + 1 } ``` General shape: ```text { Statement* Expr? } ``` The final expression, when present without a semicolon, is the block's value. A block with no final value produces `()`. ## `if` ```khora if condition { when_true } else { when_false } ``` `else if` is nested `if` syntax: ```khora if first { a } else if second { b } else { c } ``` The condition must be `Bool`. When an `if` is used as a value, its branches must agree on a result type. An `if` without `else` must produce `()`. ## `match` ```khora match value { Pattern => Expr, Pattern if guard => Expr, } ``` An arm body may be a block: ```khora match value { Result::Ok(item) => { log(item); item }, Result::Err(error) => fallback(error), } ``` Patterns are checked for exhaustiveness and reachability. See [Patterns](/docs/v0.4/reference/patterns/). ## Match guards ```khora match score { value if value >= 90 => "high", value if value >= 50 => "medium", _ => "low", } ``` The guard runs only after its pattern matches and must produce `Bool`. ## `while` ```khora while condition { body; } ``` The condition must be `Bool`. ## `for` ```khora for pattern in iterable { body; } ``` The left side is a pattern. Khora has a tuple literal — see [Tuple and unit expressions](/docs/v0.4/reference/expressions/) — but no standard iterator yields one: an iterator over pairs yields a `Pair` record, so the loop binds it by name and reads its fields. ```khora for entry in Dict::entries(table) { print("${entry.key}: ${entry.value}"); } ``` **`Pair` has the fields `key` and `value`**, and `String::split_once` answers a `Split` with the fields `head` and `rest`. Both are ordinary records: they are read with `.`, and written as record literals — `let p: Pair = { key: "a", value: 1 };`. A tuple pattern — `for (key, value) in ...` — is only valid against a value whose type really is a tuple, and is refused against a `Pair`: ``` error: this pattern takes a value apart into 2 pieces, but `Pair` is not a tuple ``` `for` desugars to `Iterator::next`, so both `Iterator` and `Step` must be in scope in the module that writes the loop: ```khora import std::core::{Iterator, Step}; ``` A tuple is taken apart by pattern and never by position: there is no `pair.0`. Destructure it in the `for`, in a `let`, or in a match arm. ```khora let point = (3, 4); let (x, y) = point; ``` Reach for a transform such as `List::map` when the result is another collection, and for `for` when the point is the body run once per element. ## `loop` ```khora loop { body; } ``` A loop exits through `break`, `return`, `raise`, failure propagation, cancellation, or other non-local control flow. ## `break` Without a value: ```khora break; ``` With a value: ```khora break result; ``` A `loop` with value-carrying `break` expressions produces the common type of those values: ```khora let found = loop { let candidate = next(); if candidate.valid { break candidate; } }; ``` ## `continue` ```khora continue; ``` `continue` skips the remainder of the current loop iteration. ## `return` Return a value: ```khora return value; ``` Return unit: ```khora return; ``` A function normally returns its body's final expression; `return` is the explicit early-exit form. Prefer the final expression where it stays clear, and `return` where an early exit is what makes the control flow readable. ## Typed failure control flow Explicit raise: ```khora raise UserError::NotFound(id) ``` Propagate a fallible call: ```khora load_user(id)! ``` Handle a typed failure: ```khora load_user(id)! catch { UserError::NotFound(_) => User::guest(), UserError::Unavailable(reason) => User::offline(reason), } ``` `raise` is not `return`. It leaves through the function's typed failure channel, which is a second exit the signature declares separately; `!` propagates that channel and `catch` handles it before it reaches the caller. See [Failures](/docs/v0.4/reference/failures/). ## Capability installation as control scope Postfix installation serves one expression: ```khora load_user(id)! with { store: test_store, } ``` Block installation serves a lexical region: ```khora with Production { run_server()! } ``` See [Capabilities](/docs/v0.4/reference/capabilities/) for capability rows and overrides. # Patterns Source: https://khoralang.com/docs/v0.4/reference/patterns/index.md Patterns appear in `match` and `catch` arms, local destructuring, `for` bindings, and other positions that bind or inspect a value. ## Wildcard ```khora _ ``` The wildcard matches a value without binding it. Prefer an explicit arm per variant where the cases mean different things: a wildcard earns its place when the remaining cases genuinely share a behavior, and costs something when they do not, because it is the one arm a newly added variant will silently fall into. ## Binding identifier ```khora value ``` **A bare identifier binds the matched value.** `value => ..` matches anything and names it `value`. **A bare identifier that is the name of one of the value's cases is an error.** Over a `Color`, `Red => ..` would bind every color, not match `Color::Red`, so the compiler refuses it and names the pattern to write. This applies in every pattern (`match`, `catch`, `let`, `for`, and nested inside others) and to cases with a payload as well: `NotFound => ..` over an `FsError` is refused too. **Write a constructor qualified** (`Color::Red`, `FsError::NotFound(path)`), and bind with a name that is not a case. The check is against the value's type, not against what the file imports, so it applies to a value whose type the file never names. If a case is added to a type later, a binding that shares its name stops compiling. It does not silently start matching only that case. **A capitalized bare name is an error too**, when it is no case of the value's type. A name in a pattern that starts with a capital letter must be a case, so `Gren => ..` over a `Color` is refused, and the message offers the nearest case, `Color::Green`, when one is within two edits (or a third of the name's length, if that is more). A `const` written as a pattern, `FAVORITE => ..`, would bind every value rather than compare against the constant; compare with a guard instead: ```khora match n { n if n == FAVORITE => "lucky", _ => "ordinary", } ``` Bind with a lower-case name. ## Literal patterns ```khora 0 3.14 "ready" true false ``` Integer, floating-point, string, and boolean literals can be used as literal patterns. ## Nullary constructor path ```khora Option::None Status::Ready ``` A qualified path selects the named constructor. ## Constructor payload pattern ```khora Option::Some(value) Result::Err(error) Message::Move(x, y) ``` The patterns inside parentheses correspond to the constructor's payload positions. ## Record pattern Shorthand field binding: ```khora User { id, name } ``` Explicit nested pattern: ```khora User { id: user_id, name: "admin", } ``` General shape: ```text Path { field, field: Pattern, ... } ``` A record pattern begins with a path and may bind fields by shorthand or supply another pattern after `:`. ## Tuple pattern ```khora (left, right) (x, y, z) ``` Tuple patterns may nest other patterns: ```khora (Result::Ok(value), _) ``` ## Patterns in `match` ```khora match result { Result::Ok(value) => use_value(value), Result::Err(error) => handle(error), } ``` The compiler checks exhaustiveness and unreachable arms. Adding a variant to a type therefore turns every exhaustive match on it into a compile error, which is the list of places that need a decision about the new case. ## Match guards A guard belongs to the arm after the pattern: ```khora match score { value if value >= 90 => "high", value if value >= 50 => "medium", _ => "low", } ``` General form: ```text Pattern if BoolExpr => Expr ``` The guard runs only after the pattern itself matches. ## Patterns in `let` ```khora let (left, right) = pair; let User { id, name } = user; ``` A pattern used directly by `let` must be valid for the value's type without requiring a missing-case branch. Refutable alternatives belong in `match`. ## Patterns in `for` ```khora for entry in Dict::entries(table) { print("${entry.key}: ${entry.value}"); } ``` The pattern binds each yielded item, under the same rules as `let`: valid for the item's type, and irrefutable. `Dict::entries` yields a `Pair`, which is a record — a tuple pattern such as `(key, value)` is only valid where the item's type is a tuple, and is refused otherwise. `for` needs `Step` and `Iterator` in scope, and `Pair` too if the body reads the item's fields: `import std::core::{Dict, Iterator, Pair, Step, print};`. See [Control flow](/docs/v0.4/reference/control-flow/#for). ## Patterns in `catch` ```khora load_user(id)! catch { UserError::NotFound(missing_id) => fallback(missing_id), UserError::Unavailable(reason) => offline(reason), } ``` `catch` reuses the pattern syntax but adds failure-row semantics: exhaustively handling a failure type removes that type from the failures that can leave the expression. ## Nesting Patterns compose recursively: ```khora match value { Envelope { payload: Result::Ok(User { id, name }), } => use_user(id, name), _ => fallback(), } ``` Use nesting when it makes the shape clearer; split deeply nested business decisions into smaller matches when a single arm becomes difficult to read. See [Control flow](/docs/v0.4/reference/control-flow/) for `match` result rules and [Failures](/docs/v0.4/reference/failures/) for `catch` semantics. # Types Source: https://khoralang.com/docs/v0.4/reference/types/index.md Khora is statically typed with inference for ordinary local expressions and explicit annotations where a boundary or ambiguity needs one. ## Type annotations Bindings: ```khora let count: Int = 42; let names: List = []; ``` **`List` has to be imported.** There is no prelude: `List`, `Dict`, `Option`, `Result`, `print` and everything else in `std::core` must be named in an `import` in every file that writes them, including the types that look built in. The snippets on this page show the annotation under discussion and not the `import std::core::{List};` above it; [Declarations](/docs/v0.4/reference/declarations/#names-that-need-no-import) has the rule. Only `Int`, `Float`, `Bool`, `Char`, `String` and the fixed-width numerics are keywords of the language rather than library names. Parameters and returns: ```khora fn add(left: Int, right: Int) -> Int { left + right } ``` ## Path types A named type is a path, optionally with generic arguments: ```khora Int String app::model::User List Result ``` Compile-time path segments use `::`. ## Unit ```khora () ``` `()` is both the unit type and unit value. ## Tuple types ```khora (Int, String) (Int, String, Bool) (Int,) ``` Parentheses without a comma group a type: ```khora (Int) ``` ## Record types ```khora { id: Int, name: String, } ``` Record fields may be explicitly mutable: ```khora { mut count: Int, name: String, } ``` A `mut` field can be assigned through a value of that record type; an ordinary field cannot. ```khora let seen: Tally = { name: "hits", count: 0 }; seen.count = seen.count + 1; ``` **The annotation is load-bearing, and there are no anonymous records.** A record literal is checked *against* a declared type, so `{ name: "hits", count: 0 }` is a `Tally` because something said so — the annotation here, a parameter's type at a call, or a function's return type. A literal with nowhere to get a type from is refused: ``` error: no record type has exactly the fields `hit`, `value` ``` which most often means a `let` with no annotation, or an accumulator handed to `List::fold` whose shape was never declared. Declaring the type is the fix: ```khora pub type Pick = { take: Bool, value: String }; ``` This is what in-place aggregation is written out of, and the fast shape for grouping by a small key — counters updated where they sit, rather than a new value per event: ```khora let buckets: Array = Array::from_fn(3, fn i => { name: "b${i}", count: 0 }); Array::get(buckets, 1).count = Array::get(buckets, 1).count + 5; ``` `Array::from_fn` and not `Array::new`: `new` puts the *same* value in every cell, which is invisible for a record nobody can change and is one counter with three names the moment a field is `mut`. `from_fn` calls its function per cell. A record with a `mut` field cannot cross into a fiber. Two fibers writing one record is a data race, and the type says so; [Sharing](/docs/v0.4/reference/sharing/) is what does cross. ### Field visibility A field of a declared record is private to the module that declares the type, unless it is marked `pub`: ```khora pub type Pair = { pub key: K, pub value: V }; pub type Tally = { pub name: String, pub mut count: Int }; pub type Email = { text: String }; ``` `pub` comes before `mut`, as it comes before `fn`. **Inside the declaring module nothing changes.** Everywhere else, a private field cannot be read (`e.text`), assigned (`e.text = ..`), named in a literal (`{ text: .. }`) or an update (`{ ..e, text: .. }`), or bound in a pattern (`Email { text }`, `let Email { text } = e`). **A value with any private field can only be built by its own module.** A literal or an update from outside is refused even when it names only `pub` fields, because the private ones are what the module's functions keep true. That is what makes a checking constructor the only way to get a value: ```khora module email::address; import std::core::{Option, String}; /// An address that was checked once, here, and never again. pub type Email = { text: String }; impl Email { pub fn parse(text: String) -> Option { match String::index_of(text, "@") { Option::None => Option::None, Option::Some(_) => Option::Some({ text: text }), } } pub fn text(self) -> String { self.text } } ``` Another module calls `Email::parse` and reads `Email::text(e)`. Writing `{ text: "nope" }` there is refused: ```text error: cannot build `Email` here: `text` is private to `email::address`, so only `email::address` can make one. Call one of its functions that returns `Email`, or, if `email::address` is yours, mark every field `pub` ``` **A private field does not hide a method.** `e.text()`, the second form of `Email::text(e)`, calls the method from outside even though `Email` has a private field of the same name. **A case's payload is not a field and is always public**, because matching on it is how a variant is used. To hide a variant's shape, wrap it in a newtype, which is private by default (see [Wrappers and named data](#wrappers-and-named-data)). **`derive`** writes code inside the declaring module, so a derived `Show`, `Eq`, `Ord`, `Hash` or `Encode` reads private fields. `derive(Decode)` is refused on a `pub` type with a private field, because it would build one from input without calling anything that checks. Write `impl Decode` with `Schema::try_map` over the function that checks, or mark every field `pub` if nothing needs checking: ```khora impl Decode for Email { fn schema() -> Schema { Schema::try_map(string(), "an email address", fn s => Email::parse(s)) } } ``` **Tests:** a `test` block is inside the module it is written in. A separate `module email::address_test;` is outside `email::address`, as it is for that module's private functions. `pub` means something only on a field of a `type` declaration. In a row, on a case's payload and on an effect's operation it is refused. `mut` is refused in a row too: a row's entries belong to no value, so there is nothing to assign. ## Variant types A value that is exactly one of several named cases. Other languages call this a **discriminated union**, a **tagged union**, or a **sum type**; Rust and Swift spell it `enum`. A `match` on one is checked for exhaustiveness, so a case you forget is a compile error rather than a surprise. ```khora | None | Some(value: A) ``` A normal named declaration uses a variant type on the right-hand side: ```khora pub type Option = | Some(value: A) | None; ``` Payloads may be named: ```khora | Point(x: Int, y: Int) ``` or positional: ```khora | Pair(Int, String) ``` ## Function types Unary function: ```khora A -> B ``` Several parameters are represented by the parameter tuple on the left: ```khora (A, B) -> C ``` Function arrows are right-associative: ```khora A -> B -> C ``` means a function from `A` to a function from `B` to `C`. ## Capability clauses on function types ```khora Request -> Response with { db: Db } ``` Open capability row: ```khora A -> B with 'ef ``` A function type can carry both requirements and failures: ```khora Request -> Response with { db: Db, clock: Clock } raises DbError + ValidationError ``` The clauses belong to the arrow they follow. ## `Char` One Unicode scalar value, thirty-two bits wide. ```khora fn initial(name: String) -> Option { String::char_at(name, 0) } ``` **Not a `String` of length one.** A `String` can be empty or hold a thousand characters, so a function taking one has to decide what those mean; a `Char` cannot be either. It lives in a register, so scanning a string a character at a time allocates nothing. **A scalar value is not every 32-bit number.** The range stops at `0x10FFFF`, and the surrogates `0xD800` to `0xDFFF` are a hole in the middle of it — they exist only to encode a pair in UTF-16 and are not characters. `Char::from_code` stops the program on either, the way `U8::of` does on a number that does not fit. `Char::code` goes the other way and cannot fail. ## Failure rows `+` joins the failure types of a `raises` row: ```khora DbError + ValidationError + HttpError ``` For a declaration: ```khora fn serve() -> Response raises DbError + HttpError { // ... } ``` **Only in a `raises` or `with` clause.** `+` builds a row, and a row is what those two clauses take. Written anywhere else it is refused: ```khora fn hold(r: Result) -> Int // error ``` A `Result` holds one error type. Handle a wider row with [`catch`](/docs/v0.4/reference/failures/#handle-failures-with-catch), which matches per type and never has to name a combined type. There is no *anonymous* union type — no way to write "an `Int` or a `String`" inline, without declaring anything. A named [variant type](#variant-types), which other languages call a discriminated union or tagged union, is how "one of several" is expressed, and the compiler checks a `match` on one for exhaustiveness. `+` in a bound (`T: Eq + Show`) is the *other* meaning of the symbol and means the parameter implements both. ## Generic type arguments ```khora List Map Matrix ``` Type arguments can include integer literal types for const-generic parameters. ## Row variables A row variable starts with `'`: ```khora 'ef 'er ``` They are used where an API is polymorphic over capabilities or failures: ```khora A -> B with 'ef raises 'er ``` The name after `'` has no built-in meaning. `'ef` for capabilities and `'er` for errors are conventions, spelled with two letters so they cannot be read backwards: `'e` alone looks like "errors" to anybody arriving from a library that calls them that. ## Record rows and open tails A closed capability-shaped row: ```khora { db: Db, clock: Clock } ``` A row with an open tail: ```khora { db: Db | 'ef } ``` Rows may merge additional row values in the tail position: ```khora { 'left | 'right | clock: Clock } ``` An **failure row** names failure types rather than capabilities, and its entries are bare: ```khora { Oops } { Timeout | Refused } ``` The bare spelling above is the one a `raises` clause takes. In **type-argument** position — the one place a row has to be written down rather than inferred — an failure row's entries are *labeled*, and an failure row labels each type with its own name. A `Fiber`'s second parameter is a row, so the form that works everywhere is: ```khora fn stop(f: Fiber<(), { Oops: Oops }>) -> () { Fiber::cancel(f) } let f: Fiber<(), { Oops: Oops }> = Fiber::spawn(work); ``` `Fiber<(), Oops>` is a type where a row belongs; it is refused, and the compiler prints the shape it wanted: ``` error: this argument: expected `() -> () raises { | Oops }`, found `() -> () raises { Oops: Oops }`; `Oops` is a type and a row belongs here. A row's entries are labeled, and an failure row labels each type with its own name — write it `{ Oops: Oops }`. The bare name is right in a `raises` clause and only a type argument needs the braces ``` **`{ Oops }` is not the same thing as `{ Oops: Oops }`.** In a `let` annotation it happens to check, because the annotation is unified against an inferred type. In a parameter or return type it is read as `{ | Oops }` — an open-tail row *variable* named `Oops` — the declaration is accepted silently, and the mismatch surfaces at the call site with the caret on the argument rather than on the signature. Write the labeled form and it is right in both places. Most code never writes one, because a signature's `raises` clause takes the types directly (`raises Oops`) and everything else infers. `Fiber` in the standard library and `Fiber<(), 'er>` in the concurrency reference are row *variables*, which need no braces — which is why the concrete form is easy to miss. Rows are structural; ordinary declared ADTs remain nominal. ## Explicit polymorphic types with `forall` ```khora forall. A -> A ``` Several parameters, including const parameters, use the normal generic parameter list: ```khora forall. Vector ``` Most named generic functions do not need explicit `forall`; the declaration's own type parameter list introduces their parameters. **A `forall` value cannot be called yet.** The type is accepted as a parameter or an annotation, and a polymorphic function can be passed as one, but applying it inside the body is a gap in the compiler — [Generics](/docs/v0.4/reference/generics/#a-forall-value-cannot-be-called-yet) has the error and what to write instead. ## Opaque types A type declaration may omit its definition: ```khora pub type Handle; ``` The representation is not available to ordinary source using the type. ## Wrappers and named data A type declaration over an existing type gives a domain name to a value, and gives it a type of its own: ```khora pub type UserId = pub Int; pub type OrderId = pub Int; ``` **These are distinct types, not other spellings of `Int`.** A `UserId` is not accepted where an `Int` is wanted, an `Int` is not accepted where a `UserId` is, and a `UserId` is not an `OrderId` — which is the reason to write one: ```text error: this argument: expected `UserId`, found `OrderId` ``` Build one by calling the type's name, and open it by matching on it, the shape a Rust tuple struct has: ```khora let id = UserId(1); fn number(id: UserId) -> Int { match id { UserId(value) => value } } ``` **The value inside is private unless the declaration says `pub`**, the same rule a record's fields follow: a wrapper is a record with one unnamed field. `type UserId = pub Int;` is an identifier anyone may make and open, and `UserId(1)` is then the spelling everywhere, including modules that import the type rather than declare it. The qualified `UserId::UserId(1)` also resolves. Without the `pub`, only the declaring module can call `Port(..)` or match `Port(n)`. That is what a refinement needs: a `Port` built only by a function that checks the range is a port every other function can trust. ```khora module net::port; import std::core::{Option}; /// A TCP port, 1 to 65535. `Port::of` is the only way to get one. pub type Port = Int; impl Port { pub fn of(n: Int) -> Option { if n < 1 || n > 65535 { Option::None } else { Option::Some(Port(n)) } } } ``` From another module, `Port(99999)` is refused: ```text error: cannot build `Port` here: its value is private to `net::port`, so only `net::port` can make one. Call one of its functions that returns `Port`, or, if `net::port` is yours, declare it `type Port = pub ..` ``` `derive` applies as it does to anything else, and is usually wanted: without `Eq` and `Ord` a `UserId` cannot be a `Dict` key, and without `Show` it cannot go in a `${..}` hole. ```khora derive(Eq, Ord, Show) pub type UserId = pub Int; ``` `Show` prints `UserId(1)`, not `UserId::UserId(1)` — the one case is the type. The underlying type may be anything, a generic one included, which is how a long type gets a short name: ```khora pub type Books = pub Dict; ``` Khora has **no transparent alias**: no form meaning "another spelling of the same type". Where that is what is wanted, write the type out, or take the wrapper and the one `match` it costs. The other two ways a declaration names data, for comparison — a record: ```khora pub type User = { pub id: UserId, pub name: String, }; ``` and a variant: ```khora pub type Lookup = | Found(user: User) | Missing; ``` See [Declarations](/docs/v0.4/reference/declarations/) for visibility, `derive`, and complete declaration forms; see [Generics](/docs/v0.4/reference/generics/) for bounds, const parameters, variance, and polymorphism. # Generics Source: https://khoralang.com/docs/v0.4/reference/generics/index.md A generic is code written once against a type it does not know, and a bound is what it is allowed to assume about that type. Everything below is one of those two things. ## Type parameters on functions ```khora fn identity(value: A) -> A { value } ``` Several parameters: ```khora fn pair(left: A, right: B) -> (A, B) { (left, right) } ``` General parameter-list shape: ```text ``` ## Type parameters on data types ```khora pub type Box = { value: A, }; pub type Result = | Ok(value: A) | Err(error: E); ``` ## Generic type arguments ```khora Box Result Map ``` `<...>` in type position is always a type-argument list; no turbofish syntax is required. ## Trait bounds Single bound: ```khora fn same(left: A, right: A) -> Bool { left == right } ``` Several bounds: ```khora fn render(value: A) -> String { Show::show(value) } ``` General form: ```text A: Trait + OtherTrait ``` Each bound is a trait path. Ask for the smallest useful one: a function that needs only equality should bound on `Eq` and not on something broader, because the bound is what a caller has to satisfy. ## Const generic parameters ```khora pub type Matrix; ``` General form: ```text const Name: Type ``` Integer literals can be supplied as type arguments: ```khora Matrix Embedding<1536> ``` A const generic is part of a type's compile-time identity; it is distinct from a module-level `const` declaration. ## Row-variable parameters ```khora fn call( value: A, f: A -> B with 'ef raises 'er, ) -> B with 'ef raises 'er { f(value)! } ``` A row variable is introduced directly in the generic parameter list: ```text 'ef 'er ``` Its spelling is arbitrary; the position where it is used determines whether it represents capability requirements, failures, or another row-shaped type. This is why an effectful or fallible function can be handed to an ordinary higher-order operation without a separate `traverse`-shaped API beside it: the combinator is polymorphic in the rows its argument carries, and passes them through to its own signature. ## Open record/capability rows ```khora { db: Db | 'ef } ``` The named entries are required and the row variable represents any additional entries. ## Higher-kinded use A generic parameter can be used as a type constructor when its use requires that kind: ```khora fn construct(value: A, make: A -> F) -> F { make(value) } ``` Here `F` is used with one type argument, so it is a type constructor rather than an inhabited concrete type. Kinds are checked from how parameters are used; Khora does not require a separate source-level kind annotation on this declaration. ## Explicit `forall` A type value can quantify its own parameters: ```khora forall. A -> A ``` Several parameters: ```khora forall. (A, B) -> (B, A) ``` Const parameter: ```khora forall. A -> Vector ``` General form: ```text forall. Type ``` Named generic declarations introduce their parameters directly and usually do not need explicit `forall`. It earns its place in an API that stores or accepts a polymorphic function *value*, where there is no declaration to hang the parameter on. ### A `forall` value cannot be called yet **This is a gap in the compiler, and it is the whole of what `forall` is for**, so read the paragraph above as describing the intent rather than what today's compiler does. Writing the type is accepted: ```khora fn takes(f: forall. A -> A) -> Int { 0 } ``` and so is passing a polymorphic function to it — `takes(fn x => x)` compiles. **Calling `f` inside the body does not:** ```text fn apply(f: forall. A -> A) -> Int { let r: Int = f(1); r } error: the type of this expression was never worked out, and nothing else was reported — so either it needs an annotation, or this is a gap in the compiler worth reporting ``` The annotation the message suggests does not help: `let r: Int` is already there, and instantiating by hand — `let g: Int -> Int = f;` — fails the same way at the assignment. Nothing in `std` uses `forall`, which is why this has not bitten anybody here. Until it is fixed, a polymorphic operation that has to be *used* is a named generic function or a trait method rather than a stored function value. ## Variance annotations Covariant parameter: ```khora pub type Source<+A>; ``` Contravariant parameter: ```khora pub type Sink<-A>; ``` Invariant parameter: ```khora pub type Cell; ``` General forms: ```text +A -A A ``` Variance is written on the parameter declaration itself. Reach for it when designing a reusable abstraction whose subtype relationship depends on the parameter; an ordinary data type normally leaves it unmarked. ## Generic implementations ```khora impl Show for Box { fn show(self) -> String { Show::show(self.value) } } ``` Type parameters immediately after `impl` are in scope for the implemented trait, target type, associated types, and methods in that block. ## Generic effects and traits Effects and traits may also take type parameters: ```khora pub effect Cache { get: K -> Option, } pub trait Convert { fn convert(self) -> A; } ``` The parameter grammar is shared across functions, types, traits, effects, and implementation blocks. See [Traits](/docs/v0.4/reference/traits/) for associated types and implementations and [Types](/docs/v0.4/reference/types/) for function types, rows, and `forall` in context. # Traits Source: https://khoralang.com/docs/v0.4/reference/traits/index.md A trait is what generic code is allowed to assume, and an impl is a type's answer to it. Both are checked where they are written. ## Declare a trait ```khora pub trait Named { fn name(self) -> String; } ``` General form: ```text pub? trait Name? (: Trait + Trait ...)? { TraitItem* } ``` A trait item is a function declaration or associated type declaration. ## Trait methods Required method: ```khora pub trait Named { fn name(self) -> String; } ``` Default method body: ```khora pub trait Named { fn name(self) -> String; fn greeting(self) -> String { "Hello, ${Named::name(self)}" } } ``` A semicolon declares a required signature. A block supplies a default implementation. ## Supertraits ```khora pub trait Ord: Eq { fn cmp(self, other: Self) -> Ordering; } ``` Several supertraits use `+`: ```khora pub trait Persisted: Eq + Show { fn id(self) -> Int; } ``` An implementation of the child trait must satisfy the supertrait requirements. ## Associated types Declare an associated type. `Iterator` in `std::core` declares two, and they answer different questions — what the iteration yields, and what pulling from it requires: ```khora pub trait Iterator { type Item; type Effects; fn next(self) -> Step with Self::Effects; } ``` With a bound: ```khora pub trait Indexed { type Key: Eq + Show; fn key(self) -> Self::Key; } ``` Supply them in an implementation. `Effects = {}` is an iterator over something already in memory, so `next` asks its caller for nothing: ```khora impl Iterator for Users { type Item = User; type Effects = {}; fn next(self) -> Step { // ... } } ``` ## Trait implementations ```khora impl Named for User { fn name(self) -> String { self.name } } ``` General form: ```text impl? TraitType for TargetType { TraitItem* } ``` A trait implementation provides the methods and associated types required by the trait. ### Coherence, and the orphan rule that is not there yet One impl of a trait for a type is the rule, and the compiler enforces it across the whole compilation — the package's own modules, the sources of the packages it depends on, and `std`. Two impls in one file are refused where they are written: ``` error: `Codec` is already implemented for `A`; there can be only one impl of a trait for a type ``` and a second `impl Codec for A` in another module names the module that already has one: ``` error: `Codec` is already implemented for `A` in `store`; there can be only one impl of a trait for a type in a program ``` The file whose path sorts first keeps the impl and the other is what gets reported, so which of two modules is refused does not depend on the order the files were read in. A type's identity is its name *and* the module that declared it, so two types both named `Entry` are two types and each may have its own `impl Codec`. **There is still no orphan rule**, with one exception: `Share`, which only the module declaring a type may implement for it. Nothing else stops you writing someone else's trait for someone else's type. What has changed is that a second impl is now an error you are shown rather than a silent choice of whichever one was merged first. The only impl the compiler still cannot see is one in a package this build does not include, so keep "the declaring package owns the impl" as a convention: implement a trait for a type you declare, or a type you declare for someone else's trait. ## Generic implementations ```khora impl Show for Box { fn show(self) -> String { Show::show(self.value) } } ``` The type parameters after `impl` are scoped to the implementation block. ## Inherent implementations An `impl` without `for` defines methods belonging directly to the target type: ```khora impl User { fn normalized_name(self) -> String { self.name |> String::trim } pub fn display_name(self) -> String { User::normalized_name(self) } } ``` General form: ```text impl? TargetType { fn ... pub fn ... } ``` A public inherent method uses `pub`. Trait methods are reached through the trait contract rather than separately exported implementation members. ## Trait bounds on generic parameters ```khora fn equal(left: A, right: A) -> Bool { left == right } ``` Several bounds: ```khora fn render(value: A) -> String { Show::show(value) } ``` Bounds are trait paths separated by `+`. ## `Self` Inside a trait, `Self` names the implementing type: ```khora pub trait Combine { fn combine(self, other: Self) -> Self; } ``` ## Trait scope and resolution **A trait's methods resolve on every type that implements it, imported or not.** An implementation is a property of the type, and `value.method(..)` asks the type — so this compiles in a module that imports neither `Show` nor `Eq`: ```khora module app::main; import std::core::{print}; pub fn main() -> Int { let same = 1 == 1; let text = 42.show(); if same { print(text); } 0 } ``` Operators follow the same rule: `==` is `Eq::eq`, and it works on any type that implements `Eq` without the name being brought in. What an import buys is the **name**. `Show` has to be imported wherever the word `Show` is written — as a bound in ``, in a `derive(Show)` clause, or as the prefix of a trait-qualified call. `for` needs `Iterator` and `Step` in scope for exactly that reason: it desugars to `Iterator::next`, and a path is a name being looked up. One consequence worth knowing before it surprises you: a file that imports `Eq` and then only ever writes `==` never writes `Eq`, so `unused-import` reports the import, correctly. ## Two traits declaring the same method A type may implement more than one trait that declares a given method name. When it does, method syntax has nothing left to choose with: ```text error: `map` is declared by `Functor` and `Iterator`, and `List` implements more than one ``` **A type's own method outranks a trait's, so this only arises where the type has none.** `List` implements both `Functor` and `Iterator` and both declare `map`, which is exactly the collision above — and `xs.map(f)` is not ambiguous, because `List` has a `map` of its own. It is the eager one: a list in, a list out. Adding a trait to a program must not silently change what an existing call does, and the same rule is what keeps an abstraction most programs never mention from making the most ordinary line in the language an error. Where a type genuinely has no method of its own, name the trait. A trait-qualified call passes the receiver as the first argument and settles which one is meant: ```khora module app::main; import std::core::{Functor, List, Show, print}; pub fn main() -> Int { let xs = List::Cons(1, List::Cons(2, List::Nil)); let ys = Functor::map(xs, fn (n) => n + 1); print(Show::show(ys)); 0 } ``` `Iterator::map(xs, fn (n) => n + 1)` picks the other one, and gives a lazy `Mapped` rather than a `List` — which is why the two cannot simply be merged, and why `List::map` had to choose. The trait has to be imported for this form, since this is the case where the name is written. Without the import the call is refused with ``cannot resolve `Functor::map` in this scope``. `Functor::map` is a *trait* path, not a module path. [Modules and packages](/docs/v0.4/reference/modules-and-packages/#paths-versus-fields) says a module path cannot prefix an expression, and that still holds — `std::core::map(..)` does not resolve. `::` after a trait or type name reaches an associated item, which is what this is. ## Deriving structural traits ```khora derive(Eq, Ord, Show, Hash, Decode, Encode) pub type User = { pub id: Int, pub name: String, }; ``` The compiler can derive those six structural traits when every field supports the requested behavior. A `derive(...)` clause appears immediately before its `type` declaration. `Decode` and `Encode` are `std::schema`'s: the declaration is the schema, and `User::schema()` reads one from untrusted input. ## Generic traits ```khora pub trait Convert { fn convert(self) -> A; } ``` Trait parameters use the normal generic parameter syntax, including bounds, const parameters, and variance where meaningful. See [Generics](/docs/v0.4/reference/generics/) for parameter forms and [Declarations](/docs/v0.4/reference/declarations/) for the top-level grammar shared by traits and implementations. # Effects and rows Source: https://khoralang.com/docs/v0.4/reference/effects/index.md Khora functions can carry two orthogonal row-polymorphic dimensions in addition to parameters and return values: ```text with capabilities required by the computation raises recoverable failures that may leave the computation ``` They are part of the function type and are checked by the compiler. ## Effect declaration ```khora pub effect Clock { now: () -> Instant, } ``` An effect with fallible operations: ```khora pub effect Store { load: Id -> User raises StoreError, save: User -> () raises StoreError, } ``` General form: ```text pub? effect Name? { operation: FunctionType, ... } ``` An effect name is the type of handlers implementing that effect. The `Clock` above is a stand-in cut down to one operation. The real one is [`std::clock::Clock`](/docs/v0.4/stdlib/api/clock/): it lives in its own module rather than in `std::env`, and it has four operations, `sleep` among them. Waiting is an operation on the capability on purpose — a fake clock is an ordinary handler, so a test that exercises a retry loop finishes instantly: ```khora handler for Clock { sleep: fn _ms => (), unix_seconds: fn () => 0, unix_millis: fn () => 0, monotonic_millis: fn () => 0, } ``` A handler gives every operation of its effect. There is no `..` for the rest, and leaving one out is an error naming the operation that is missing, so the fake above writes all four even though only `sleep` is what the test is for. ### An operation may be generic in a row, but not in a type An operation's `raises` row may mention a row variable the operation itself binds. It is quantified per call, not per handler, so one handler serves callers whose failures are unrelated: ```khora pub effect Nursery { adopt: (Fiber<(), 'er>) -> (), } ``` An operation cannot introduce a *type* parameter the same way. The asymmetry follows from representation. A capability crosses as evidence and an error as a tag, so the handler's closure is the same machine code for every row — nothing in it depends on which failures the caller can raise. A type parameter decides a layout and must be monomorphized, and a handler's fields are closures, which have nowhere to put a per-layout instantiation. An effect may still take type parameters of its own on the `effect` declaration, as the general form above shows. Those are fixed when the handler is written; a row variable on an operation is not. ## Capability requirement on a function ```khora fn load_user(id: Id) -> User with { store: Store } raises StoreError { store.load(id)! } ``` `with { store: Store }` introduces the label `store` into the body with the operations of `Store`. ## Several capabilities ```khora fn build_report(id: Id) -> Report with { store: Store, clock: Clock } raises StoreError { // ... } ``` ## Open capability row ```khora fn use_clock(body: () -> A with { clock: Clock | 'ef }) -> A with { clock: Clock | 'ef } { body() } ``` A row variable preserves additional requirements not interpreted by the generic function. ## Failure row Single type: ```khora raises StoreError ``` Several failure types: ```khora raises StoreError + ValidationError + HttpError ``` Open failure row: ```khora raises 'er ``` ## A helper that waits needs no row A `raises` row says how a function can fail, and nothing else. A helper that waits on a clock, a channel or a fiber and cannot fail declares no row, and it is still a place its fiber can be canceled — every blocking operation is, whatever the row: ```khora fn nap(ms: Int) -> () with { clock: Clock } { clock.sleep(ms) } ``` It can be called from a function with any row, fallible or not, without a `!` and without widening the caller's row. A row variable (`raises 'er`) belongs where a failure passes *through* the function — most often a closure the caller supplies — not on a helper that has no failure of its own. ## Effects on function types ```khora Request -> Response with { db: Db } ``` ```khora Request -> Response raises HttpError ``` ```khora Request -> Response with { db: Db } raises DbError + HttpError ``` Clauses belong to the function arrow they follow. ## Handler expression ```khora handler for Clock { now: fn () => fixed_instant, } ``` Fallible operation implementation: ```khora handler for Store { load: fn id => load_from_disk(id)!, save: fn user => save_to_disk(user)!, } ``` The implementation must satisfy the effect's operation types. ## Install handlers One expression: ```khora job() with { clock: fixed_clock, } ``` Lexical block: ```khora with { clock: fixed_clock, store: memory_store, } { job()! } ``` Named contexts and overrides are specified in [Capabilities](/docs/v0.4/reference/capabilities/). ## Effect-polymorphic higher-order functions ```khora fn map( values: List, f: A -> B with 'ef raises 'er, ) -> List with 'ef raises 'er { // ... } ``` The higher-order function preserves the capabilities and failures of its callback rather than wrapping it in a separate effect value. ## Failure propagation and handling ```khora operation()! ``` ```khora operation()! catch { Error::Case(value) => recover(value), } ``` See [Failures](/docs/v0.4/reference/failures/) for `raise`, `!`, `catch`, and `attempt` semantics. ## Capabilities and failures are independent A capability operation may be infallible, and a pure function may still raise a typed domain failure. `with` records authority; `raises` records recoverable non-local control flow. Neither implies the other. # Capabilities Source: https://khoralang.com/docs/v0.4/reference/capabilities/index.md A function that needs to touch the outside world says so in its signature, and the caller has to hand it the means: ```khora fn load_user(id: Id) -> User with { store: Store } raises StoreError ``` `with { store: Store }` is the requirement. Callers supply it with a `with` block. Nothing reaches a database, a clock or the network without a caller passing it in. ## Capability row on a declaration ```khora fn load_user(id: Id) -> User with { store: Store } raises StoreError { store.load(id)! } ``` The row entry has a label and an effect type: ```text store: Store ``` The label `store` is in scope in the function body. The call mirrors the declaration. The function writes `with { store: Store }` to say what it needs; the caller writes `with { store: memory_store }` to supply it. Read the first as *this function requires a capability named `store` implementing the `Store` effect*. ## Several capabilities ```khora fn create_session(id: Id) -> Session with { store: Store, clock: Clock } raises StoreError { let user = store.load(id)!; Session::new(user, clock.now()) } ``` ## Open capability row ```khora { clock: Clock | 'ef } ``` The named capability is required and `'ef` represents any additional row entries. A function can preserve the open row: ```khora fn run(body: () -> A with 'ef) -> A with 'ef { body() } ``` ## Handler values ```khora let fixed_clock = handler for Clock { now: fn () => fixed_instant, }; ``` A handler's type is the effect it implements. ## Installing the real thing The handlers above are invented, because a page about syntax should not depend on which effects `std` happens to ship. Real programs install the ones it does, and each capability's own module provides them: ```khora import std::clock::{Clock}; import std::env::{Env}; import std::fs::{FsRead, FsWrite}; import std::log::{Severity, Log}; pub fn main() -> Int { with { env: Env::real(), reads: FsRead::real(), writes: FsWrite::real(), clock: Clock::real(), log: Log::json(Severity::Info), } { work() } } ``` **The label is chosen by the function you are calling, not by you.** A row entry named `fs:` will not satisfy a function that declared `reads: FsRead`; the error says so — ``needs `reads: FsRead`, which this function does not require`` — but it is worth knowing before you meet it, because the labels are effectively part of `std`'s public API: | Capability | Label | From | | --- | --- | --- | | `Env` | `env` | [`std::env`](/docs/v0.4/stdlib/api/env/) | | `FsRead` | `reads` | [`std::fs`](/docs/v0.4/stdlib/api/fs/) | | `FsWrite` | `writes` | [`std::fs`](/docs/v0.4/stdlib/api/fs/) | | `Clock` | `clock` | [`std::clock`](/docs/v0.4/stdlib/api/clock/) | | `Random` | `random` | [`std::random`](/docs/v0.4/stdlib/api/random/) | | `Log` | `log` | [`std::log`](/docs/v0.4/stdlib/api/log/) | | `Tracer` | `tracer` | [`std::trace`](/docs/v0.4/stdlib/api/trace/) | | `Db` | `db` | [`std::db`](/docs/v0.4/stdlib/api/db/) | | `HttpClient` | `client` | [`std::net::http`](/docs/v0.4/stdlib/api/net/http/) | | `Process` | `process` | [`std::process`](/docs/v0.4/stdlib/api/process/) | | `Nursery` | `nursery` | [`std::core`](/docs/v0.4/stdlib/api/core/) | | `Scope` | `scope` | [`std::core`](/docs/v0.4/stdlib/api/core/) | Every `real()` reaches the actual machine. A test installs a handler of its own instead — see [Testing capabilities](/docs/v0.4/cookbook/testing-capabilities/), which is what the seam is for. ## Postfix `with` Install handlers for one expression: ```khora let user = load_user(id)! with { store: memory_store, }; ``` General form: ```text Expr with { label: HandlerExpr, ... } ``` The installation is postfix, so it applies to the expression immediately before it. ## `with` block Install handlers for a lexical region: ```khora with { store: memory_store, clock: fixed_clock, } { let user = load_user(id)!; create_session(user) } ``` General form: ```text with ContextRow Block ``` Handlers lexically enclose the operations they serve. That is a real consequence of direct style rather than a syntactic detail: the operation runs when the call is evaluated, inside the block, rather than being collected into a description that something runs later. A closure written inside the block is the one case worth being exact about. It *captures* the capability, because a capability row is a block of bindings and a capability is an ordinary binding, so the closure keeps the handler it was written with even when it is called somewhere else -- including after the block has ended. That is what makes a callback that logs cost nothing in the type of whatever takes it. The handler stays alive as long as the closure does. ## Sequential bindings Bindings inside a capability row are sequential. A later expression may use handlers introduced above it: ```khora with { config: env_config(), scope: Scope::root(), db: postgres_db()!, store: sql_store(), } { run_server()! } ``` This allows service construction to remain flat rather than nesting one installation block per dependency. ## Named context declaration ```khora pub context Production { config: env_config(), scope: Scope::root(), db: postgres_db()!, store: sql_store(), } ``` General form: ```text pub? context Name { label: Expr, ... } ``` ## Use a named context Postfix: ```khora load_user(id)! with Production ``` Block: ```khora with Production { run_server()! } ``` ## Override named-context entries ```khora load_user(id)! with Production { store: test_store, } ``` or around a block: ```khora with Production { store: test_store, } { run_test_case()! } ``` Entries written at the use site replace or extend the corresponding capability row for that installation. This is what makes a named production context usable from a test: install `Production` and override the one capability the test wants to control. ## A capability as an ordinary parameter A `with` row is not the only way in. An effect value is a value, so a function can take one as a plain parameter and call its operations with the same method syntax: ```khora import std::clock::{Clock}; import std::core::{print}; /// The capability arrives as an argument. fn stamp(clock: Clock, message: String) -> String { "${clock.unix_millis()}: ${message}" } /// The same thing said with a row. fn stamped(message: String) -> String with { clock: Clock } { "${clock.unix_millis()}: ${message}" } pub fn main() { let clock = Clock::real(); print(stamp(clock, "started")); print(stamped("started") with { clock: clock }); } ``` The two forms differ in who chooses. A row is *requested*: the caller decides what to install, and every intermediate frame passes it along without naming it. A parameter is *given*: the caller has a particular handler in hand and hands that one over. Prefer the row — it is what makes an intermediate function say nothing about a capability it only passes through. **Reach for the parameter where a row cannot go.** The clearest case is a type that has no capability row to put one in. `SharedFn`, which `Router::get` takes, is `(A) -> B raises 'er` and nothing else — there is no `with` in it, so a handler needing a database or an HTTP client looks impossible to write. It is not: the handler takes the capability as a parameter, and the closure that is certified closes over it. ```khora import std::core::{Result, SharedFn}; import std::net::http::{Answer, Call, HttpClient, Request, Response, Router}; /// The capability arrives as a parameter, because `SharedFn` has no row. fn items(client: HttpClient, _request: Request) -> Response { match client.send(Call::get("http://inventory/items")) { Result::Ok(answer) => Response::text(answer.status, answer.body), Result::Err(_why) => Response::text(502, "inventory unreachable"), } } fn routes<'er>(client: HttpClient) -> Router<'er> { Router::new() |> Router::get("/items", SharedFn::of(fn request => items(client, request))) } ``` A handler is written the same way whether the capability came from a row or an argument, so a function can be moved from one to the other without its body changing. ## Capability rows on function values ```khora Request -> Response with { db: Db, clock: Clock } ``` Generic row: ```khora A -> B with 'ef ``` Capability rows are part of function types, so higher-order functions can preserve requirements without a runtime service locator. ## Capabilities do not imply failure ```khora fn choose_bucket() -> Int with { random: Random } { random.in_range(0, 10) } ``` A capability may be required by an operation that does not raise a recoverable failure. Conversely, a pure computation may use `raises` without any `with` requirement. ## Manifest permissions A capability row says a function reaches outside the program. `[permissions]` in `khora.toml` says how far it may reach, and the standard library's real handlers enforce it: ```toml [permissions] network = ["api.example.com:443", "*.internal"] env = ["PORT", "DATABASE_*"] [permissions.fs] read = ["./data", "./data/**"] write = ["./data/out.txt"] ``` **Both `read` entries above are needed**, and the reason is the one surprise in the glob dialect below: `./data/**` grants what is *inside* `data` and not `data` itself, so with only that line a program can read every file in the directory and cannot list it. `read_dir("data")` and `is_dir("data")` both raise `Denied` rather than answering `false`, for the reason given under the probes below. The grants are compiled into the binary rather than read at run time. A file the program consults for its own permissions is a file an attacker edits. **A missing table grants everything, and each category is independent.** Naming `network` says nothing about `env`. Tightening is opt-in. **`network` is a list of hosts the program may connect *out* to, and nothing else.** It is read in one place — `HttpClient::send`, once the URL is parsed and before anything is dialed — and it decides whether that host may be reached. Binding a port is the other direction and is not covered: `Router::listen`, `Router::listen_quietly` and `Router::listen_tls` never consult it, so a program with `default = "deny"` and no hosts granted still serves on any port it asks for. Write `network = ["127.0.0.1:8787"]` expecting it to authorize a listener and it will do nothing, in either direction. [Known limitations](/docs/v0.4/limitations/#inbound-connections-are-not-permissioned) has the reasoning. A denial is its own error case, separate from the ordinary failure, because the two send a reader to different files: | Reaching for | Denied as | Where the fix is | | --- | --- | --- | | an environment variable | `EnvError::Denied(name)` | `[permissions] env` | | a host over HTTP | `CallError::Denied(host)` | `[permissions] network` | | a path on disk | `IoError::Denied(path)` | `[permissions.fs]` | **Including the two probes.** `FsRead::exists` and `FsRead::is_dir` answer a `Bool` and raise `IoError` as well, so a path the manifest denies is `Denied` rather than `false`. They answered a plain `Bool` until recently, which made three situations one word — not there, there but unreadable, and there and readable and simply not granted — and the third is the one somebody debugs for an hour, because nothing in a `false` points at a manifest. The remaining `false` means one thing: the operating system would not open it. `Unreachable` is DNS or a firewall; `Denied` is a line you can copy out of the message into the manifest. `Env::variable` and `std::env::variable_or` therefore `raise EnvError`, so both need a `!` at the call site: ```khora let port = variable_or("PORT", "8080")!; ``` Globbing differs by category, and each one is the reading that costs the least surprise. For a path, `*` stops at a separator and `**` crosses one — and, as in `.gitignore`, **neither covers the directory being described**, which is why the `[permissions.fs]` example above names `./data` as well as `./data/**`. For a name — a variable, a command — there are no segments, so `*` spans everything. For a host, `*` spans dots, so `*.internal` covers `db.eu.internal`, and a grant with no port covers every port. See [Effects and rows](/docs/v0.4/reference/effects/) for effect declarations and [Failures](/docs/v0.4/reference/failures/) for typed failure. # Failures Source: https://khoralang.com/docs/v0.4/reference/failures/index.md Recoverable failure is part of a Khora function type. `raises` declares the channel, `raise` creates a failure, postfix `!` propagates one, `catch` handles typed cases, and `attempt` converts the channel into `Result` data. The four pieces, at a glance: ```text raise E create a typed failure foo()! allow E to propagate out of the current function foo()! catch { ... } handle E here attempt(fn () => foo()!) turn E into a Result value ``` ## Declare a failure type Failure types are ordinary ADTs: ```khora pub type UserError = | NotFound(id: Id) | Unavailable(reason: String); ``` ## `raises` on a function ```khora fn load_user(id: Id) -> User raises UserError { // ... } ``` Several failure types use `+`: ```khora fn handle_request() -> Response raises UserError + HttpError { // ... } ``` ## `raises` on a function type ```khora Id -> User raises UserError ``` Open generic row: ```khora A -> B raises 'er ``` ## Explicit `raise` ```khora raise UserError::NotFound(id) ``` `raise` is an expression of type `Never`: the current path does not produce the surrounding normal result. Example: ```khora fn require_user(found: Option, id: Id) -> User raises UserError { match found { Option::Some(user) => user, Option::None => raise UserError::NotFound(id), } } ``` ## Propagate with postfix `!` ```khora let user = load_user(id)!; ``` The call's normal value remains `User`; its failure is allowed to leave the current computation. The enclosing function must either permit that failure in its own `raises` row or handle it. ```khora fn load_profile(id: Id) -> Profile raises UserError { let user = load_user(id)!; Profile::from_user(user) } ``` ## `!` is a call-site marker Prefix and postfix `!` have different meanings: ```khora !enabled // boolean negation load_user(id)! // failure propagation ``` Position disambiguates them. ## Handle failures with `catch` ```khora let user = load_user(id)! catch { UserError::NotFound(_) => User::guest(), UserError::Unavailable(reason) => User::offline(reason), }; ``` **The `!` is still required.** `catch` handles a failure that has already been let out of the call, so `load_user(id) catch { .. }` — without the mark — is refused before the arms are looked at: ``` error: `load_user` can leave this function, so the call needs `!`: write `load_user(..)!` ``` A `catch` arm uses ordinary pattern syntax over the failure value. The success path keeps the value produced by the inner expression; a matching arm produces the replacement value. A bare name binds the whole failure, the way it does in a `match`: ```khora let user = load_user(id)! catch { trouble => User::unavailable(Show::show(trouble)) }; ``` That arm handles everything the operand can raise, so it needs no companion. It is the form to reach for when the answer does not depend on which variant arrived -- turning a nine-variant error into one `Refused` is one arm this way and nine identical ones by constructor. The binding is typed by the operand's failure row, so it works when that row names **one** type. Where an operand raises two, there is no single type to give the name, and the compiler says so: ```text this `catch` arm binds the failure, but the operand can raise more than one type (DbError, ModelError), so there is no single type to give the binding. Name a constructor, or use `_` to handle them all without looking ``` `_` remains the arm for "handle everything and do not look at it": it binds nothing, and the failure is released without being read. A failure type with a type parameter is handled at the instantiation that was raised. Over `fetch() raises Gx`, the arm `Gx::X(msg, v)` binds `v` as an `Int`, and an arm body that uses it as anything else is a type error. In a generic function, an arm over `Gx` handles whichever `A` that call was made at. A `raises` row holds each failure type at one instantiation. Where a `Gx` and a `Gx` would meet in one row -- the body of one closure, `raises E + F` with `E` and `F` both `Gx`, or one `catch` whose operand raises both -- the program is refused, because one arm reads one layout and nothing says which of the two arrived: ```text a `catch` arm handles one instantiation of a type, and this operand raises two; catch them with `_`, or in two `catch`es ``` ```text an error type appears in a `raises` row once, at one instantiation, and this raises `Gx` at two; catch one of them where it is raised ``` Put a `catch` around each call, or, directly around the two calls, use a `_` arm alone: `_` reads nothing, so it needs no layout. A `_` beside a `Gx::..` arm, or an arm that binds the failure, is refused like a named arm. A row is labeled by the failure type's name, so two *different* types of one name -- an `E` declared in `m::a` and another in `m::b` -- meet in a row the same way, and are refused the same way: ```text an error type appears in a `raises` row once, by name, and this raises `E` at two: two different types named `E`; catch one of them where it is raised ``` ## Exhaustiveness by failure type Handling a named failure type commits to handling all of that type's variants: ```khora operation()! catch { UserError::NotFound(id) => recover_missing(id), UserError::Unavailable(reason) => recover_unavailable(reason), } ``` When those arms exhaust `UserError`, that type is subtracted from the failures that can leave the `catch` expression. A wildcard does not mean “all possible failures in an open row.” ## Translate failure types Catch one type and raise another: ```khora fn load_for_api(id: Id) -> User raises ApiError { load_user(id)! catch { UserError::NotFound(_) => raise ApiError::NotFound, UserError::Unavailable(reason) => raise ApiError::ServiceUnavailable(reason), } } ``` `UserError` no longer escapes `load_for_api`; callers see `ApiError`. A layer should usually not expose the failures of the layer beneath it, which makes this the ordinary boundary shape: ```text infrastructure/domain failure | catch + raise application/API failure | catch response or other boundary value ``` ## Handle only part of a multi-type row ```khora fn analyze(id: Id) -> Report raises DbError { analyze_model(id)! catch { ModelError::RateLimited(_) => Report::deferred(id), ModelError::ContextLengthExceeded(_) => Report::unavailable(id), ModelError::InferenceEngineFailure(_) => Report::unavailable(id), ModelError::SchemaExtractionError(_) => Report::unavailable(id), } } ``` If the inner expression raises `DbError + ModelError`, exhaustively catching `ModelError` leaves `DbError` untouched. ## Convert failures to a boundary value ```khora fn handle(id: Id) -> Response { let user = load_for_api(id)! catch { ApiError::NotFound => return Response::text(404, "not found"), ApiError::ServiceUnavailable(reason) => return Response::text(503, reason), }; Response::json(200, user) } ``` The function has no `raises ApiError` because the boundary consumes every `ApiError` into an ordinary `Response`. ## `attempt` `attempt` converts a computation's failure channel into `Result`: ```khora let result = attempt(fn () => load_user(id)!); ``` Conceptual type: ```khora fn attempt(body: () -> A with 'ef raises E) -> Result with 'ef; ``` The returned `Result` is ordinary data: ```khora match result { Result::Ok(user) => use_user(user), Result::Err(error) => log_error(error), } ``` ### One error type `E` is a type, not a row, so `attempt` handles a body that raises exactly one thing. A body raising two has nowhere to go through it: ```text error: this argument: `Denied` is not accounted for here. This takes one error type and the body raises `Denied` and `IoError`; there is no type that means "either of these", so handle them with `catch` instead ``` This is a real limit rather than an oversight. `Result` needs one `E`, and Khora has no anonymous sum type to name "either of these two" — so there is nothing for a two-type row to collapse into. Naming the union would mean declaring a type for every pair of failures a program happens to combine. Use [`catch`](#handle-failures-with-catch) for a wider row. It matches per type and never has to name the union: Here `fetch` raises `std::fs`'s `IoError` and the program's own `Denied`, and one `catch` covers both. Every constructor is written with its type in front of it, and naming a type commits to all of its variants — `IoError` has three: ```khora let answer = fetch(url)! catch { IoError::NotFound(_path) => fallback(), IoError::Failed(_path) => fallback(), IoError::Denied(_path) => refuse(), Denied::Denied(_path) => refuse(), }; ``` ## Two marks, doing different jobs A fallible function can be mapped directly, because a function type carries its own capability and failure rows and the combinator carries the caller's: ```khora let users = ids |> List::map(fn id => load_user(id)!)!; ``` The inner `!` is `load_user` failing. The outer one is `List::map` passing that failure on, which it can do only because its own signature ends `raises 'er`. Leaving the outer one off is an error naming both halves: ```text error: `List::map` can leave this function, so the call needs `!`: write `List::map(..)!` error: `List::map` needs `Bad`, which this function does not raise ``` Every combinator on `List` that takes a function is written that way — `fold`, `filter`, `find`, `any`, `all`, `partition`, `flat_map`, and their counterparts on `Option` and `Result` — so a fallible step never means leaving the chain to write the walk by hand. Mapping a *pure* function needs neither mark: an empty row is what a row variable takes when nothing fills it. **`std::fs::fold_lines` and `fold_chunks` are the exception.** Their `step` parameter is `(A, String) -> A` with no `with` row and no `raises` row, so a step that fails or that needs a capability cannot be passed: ```text error: this argument: `Bad` is not accounted for here ``` The message does not say why. Until those signatures grow rows, a streaming walk whose step can fail has to be written as `read_text` plus `String::split`, which holds the whole file. ## Collect per-item failures Propagating from inside `List::map` stops at the first failure: ```khora items |> List::map(fn item => process(item)!)! ``` Convert each invocation with `attempt` to run all items and collect both successes and failures: ```khora let results = items |> List::map(fn item => attempt(fn () => process(item)!) ); ``` The result has the shape: ```khora List> ``` ## When you want every reason, not the first `raises` stops at the first failure, which is right when the next step needs the last one's value. Validation is the other shape: the fields of a form, the keys of a configuration, the columns of a row do not depend on each other, and reporting them one restart at a time makes somebody run the program five times to be told five things it knew the first time. `Validated` from `std::core` is that shape. `map2` runs its combiner only when both sides succeeded, and otherwise carries every error from both: ```khora let settings = Validated::map2( integer("PORT"), string("HOST"), fn (port, host) => { port: port, host: host }, ); ``` `and_then` is the fail-fast one, for a second step written in terms of the first's value — there is no second answer to collect when there is no first value to write it against. `to_result` collapses the whole thing into a `Result>` when it is time to rejoin a `raises` chain. It keeps the list rather than the first error, because throwing the rest away at the boundary would undo the collecting. [Load application configuration](/docs/v0.4/cookbook/configuration/) and [Schemas](/docs/v0.4/stdlib/schema/) are both built on it. ## `Result::map_err` Once a failure is already data, use normal `Result` operations rather than `catch`: ```khora let mapped = result |> Result::map_err(fn error => ApiError::from_user_error(error) ); ``` This maps `Result` to `Result`. By contrast, `catch { ... raise F ... }` maps the live failure channel from `E` to `F`. ## A failure that reaches `main` `main` is not called by anything, so there is nowhere to hand a failure that gets that far. A program whose entry point raises and does not handle it ends with **exit status 1**, and prints the error type's name to standard error: ``` khora: `IoError` reached the entry point and nothing handled it note: `main` has nowhere to hand an error, so the program ends here with status 1. Catch it in `main`, or return a `Result` ``` The *type* is named, not the value. Handle it where the program can say something better: ```khora pub fn main() -> Int { with { reads: FsRead::real() } { match attempt(fn () => read_text(path)!) { Result::Ok(text) => { print(text); 0 }, Result::Err(IoError::NotFound(missing)) => { eprint("no such file: ${missing}"); 1 }, Result::Err(other) => { eprint("could not read it"); 1 }, } } } ``` `eprint` and not `print`: the diagnostic goes to standard error, so that a program whose output somebody redirected still says why it stopped. It comes from `std::log`. A cancellation that reaches the entry point is a different outcome and exits **130** — 128 plus `SIGINT`, which is what a shell already means by "interrupted". It prints nothing, because stopping a program with a keystroke is not a failure to report. ## `main` installs capabilities rather than requiring them For the same reason, `main` may not carry a `with { .. }` requirement: nothing calls it, so nothing could supply one. Install them in the body instead — `with { reads: FsRead::real() } { .. }`, as above. A `with` clause on `main` is refused at compile time. ## Failure is part of the API A `raises` row is documentation the compiler checks. It answers, in the signature, a question many languages leave to prose: what normal failure conditions must a caller be prepared for? Keep the failure types meaningful at each boundary. A low-level package can expose precise operational failures, an application service can translate those into domain failures, and the outermost boundary can consume them into responses, exit codes or other protocol values. `with` and `raises` are the two halves of an effectful signature and are independent: `with` says what authority a computation needs, [Capabilities](/docs/v0.4/reference/capabilities/) covers it, and `raises` says how its normal result may fail. ## Traps are separate Arithmetic overflow, bounds failures, and other violated invariants are traps, not ordinary `raises` values. See [Traps](/docs/v0.4/reference/traps/). # Memory and resources Source: https://khoralang.com/docs/v0.4/reference/memory-and-resources/index.md Khora manages ordinary memory automatically. Resource lifetimes that have external meaning—files, sockets, transactions, foreign handles—use explicit structured cleanup. ## Ordinary memory There is no source-level borrow or lifetime syntax for ordinary Khora values. The runtime uses reference counting and the compiler may remove retain/release operations or reuse uniquely owned storage when doing so cannot change program meaning. The programmer-visible rules are: - optimization cannot make behavior depend on whether storage was reused; - writable state does not silently become cross-fiber shared state; - external resources require their own cleanup policy. ## Region syntax The primitive resource lifetime object is `Region`: ```khora pub type Region; impl Region { pub fn open() -> Region; pub fn root() -> Region; pub fn defer(self, finalizer: () -> ()) -> (); } ``` Open a region and register finalizers with `defer`: ```khora fn work() -> Int { let region = Region::open(); Region::defer(region, fn () => release_second()); Region::defer(region, fn () => release_first()); 42 } ``` Finalizers execute in **reverse registration order** when the region is released, and a region is released **when the last reference to it goes** -- which for the ordinary case is the end of the block its binding is in, on every way out of that block, including a cancellation. The exception is a reference that outlives the block: a closure that captured the region keeps it open until that closure goes too, so a captured region's finalizers run later than the block, not at it. So the example above runs its finalizers when `work` returns, and putting the `let` inside a smaller block ends the region there instead: ```khora { let region = Region::open(); Region::defer(region, fn () => release()); use_it(); }; // `release()` has already run here. ``` That is the difference between a lease that ends with the call and one that ends with the caller, and it is worth being deliberate about: a `Region::open()` at the top of a function holds everything until the function returns. `Region::root()` refers to the outer program region. Its finalizers run as the program exits. **A region stays on the fiber that opened it**, and so does a `Scope`: neither is shareable, so neither can be captured by a fiber's body, sent on a channel, or put in a `Shared` cell. Every finalizer therefore runs on the fiber that deferred it, which is what lets one capture a record with `mut` fields that the fiber goes on writing. A child fiber that acquires something is given a scope of its own, `Fiber::spawn(fn () => scoped(work))`, and what it acquires is released when that `scoped` ends. `Region::root()` and `Scope::root()` belong to the program's own fiber, and a spawned fiber that calls either stops with a fatal error; a `test` or `bench` block has a root region of its own, released when the block ends. [Sharing](/docs/v0.4/reference/sharing/#regions-stay-home) has the rule, which scope a lambda handed to `scoped` uses, and what it costs. ## Scope capability The standard structured-resource capability is: ```khora pub effect Scope { defer: (() -> ()) -> (), } ``` `scoped` creates a fresh region, installs a `Scope` handler over the body, and removes that capability from the caller's required row: ```khora pub fn scoped( body: () -> A with { 'ef | scope: Scope } raises 'er ) -> A with 'ef raises 'er ``` Example: ```khora fn inside() -> Int with { scope: Scope } { scope.defer(fn () => cleanup()); 7 } fn outside() -> Int { scoped(inside) } ``` A named function is the normal argument to `scoped` when that function requires the `scope` capability. ## Acquire and release `acquire` registers a release operation and returns the acquired value: ```khora pub fn acquire(value: A, release: (A) -> ()) -> A with { 'ef | scope: Scope } ``` Typical form: ```khora fn use_connection() -> ResultValue with { scope: Scope } { let connection = acquire(open_connection(), close_connection); query(connection) } ``` `release(value)` runs when the enclosing resource scope ends. ## Exit semantics A region is released on every structured path out of its owner: ```khora pub type WorkError = | Failed; fn work(fail: Bool) -> Int raises WorkError { let region = Region::open(); Region::defer(region, fn () => cleanup()); if fail { raise WorkError::Failed } return 1; } ``` The finalizer runs on the normal path, the explicit `return`, and when the `raise` leaves the function. Cancellation uses the same structured unwind path. A pending cancellation observed at a cancellation point releases intervening regions and runs their finalizers before the fiber terminates. A `catch` handles failures in a `raises` row. Cancellation is not a failure variant and is not consumed by `catch`. Cancellation is not an exception that can arrive between arbitrary instructions: a pending one is observed at a cancellation or failure propagation point, such as `!`. That is what keeps the code between two marked points readable as ordinary straight-line code, while still letting blocked or suspended work be woken so it can unwind and release what it holds. The invariant a resource abstraction should be built on is the short one: **if the scope ends, cleanup runs.** ## Resource APIs A resource-owning API should generally keep the lifetime inside one call: ```khora fn with_resource( body: (Resource) -> A with 'ef raises 'er ) -> A with 'ef raises 'er ``` rather than returning an unmanaged handle that callers must remember to close on every path. A database transaction is this shape with a richer finalizer policy, and it belongs in the transaction abstraction rather than in every caller: - completing normally commits; - a typed failure rolls back; - cancellation rolls back before the connection returns to its pool. [Database transactions](/docs/v0.4/cookbook/database-transactions/) is that policy written out. Foreign or thread-affine resources can impose rules beyond ordinary Khora values. See [FFI](/docs/v0.4/reference/ffi/) for pointer and suspension constraints, [Concurrency](/docs/v0.4/reference/concurrency/) for fiber lifetime rules, and [Sharing](/docs/v0.4/reference/sharing/) for cross-fiber values. # Sharing Source: https://khoralang.com/docs/v0.4/reference/sharing/index.md Khora keeps ordinary mutation fiber-local. A value may cross a fiber boundary only when its type is shareable, and coordinated mutation is expressed through synchronization types rather than by sharing an ordinary writable record or container. ## `Share` `Share` is the marker trait for values that two fibers may hold at the same time: ```khora pub trait Share {} ``` Structural values are shareable when all of their contents are shareable. For example, an immutable record containing only shareable fields needs no handwritten marker implementation. Opaque types are different: the compiler cannot inspect their representation, so they are not shareable unless their implementation explicitly promises that concurrent holders are safe: ```khora pub type SafeHandle; impl Share for SafeHandle {} ``` Writing that implementation is an assertion about the opaque implementation. Do not add it merely to silence a sharing diagnostic. Mutable fiber-local containers are not made safe by wrapping their type name in a shareable record. If the value itself permits unsynchronized writes, it must remain local or be represented through a synchronization primitive designed for that use. ## `Shared` `Shared` is a synchronized cell containing one shareable value: ```khora pub type Shared; impl Share for Shared {} impl Shared { pub fn of(value: A) -> Shared; pub fn get(self) -> A; pub fn set(self, value: A) -> (); pub fn update(self, change: (A) -> A) -> A; pub fn modify(self, change: (A) -> Changed) -> B; } ``` Create, read, and replace a value as follows: ```khora let count = Shared::of(0); let before = Shared::get(count); Shared::set(count, before + 1); ``` Use `update` when the read and write must be one serialized transition: ```khora let after = Shared::update(count, fn n => n + 1); ``` The `change` closure runs once while the cell is locked. Its type has no `raises` row. Fallible, blocking, or otherwise slow work belongs outside the critical section, which keeps it small and stops external latency from turning into lock contention: ```khora let refreshed = fetch_value()!; Shared::set(cache, refreshed); ``` Calling `update` or `modify` recursively on the **same cell** from inside its own change function would deadlock. Khora detects that case and traps instead of waiting forever. **Waiting for a cell.** While one fiber is inside a change function, any other `get`, `set`, `update` or `modify` on that cell waits. On the scheduler backend a waiting fiber gives its worker back, so the worker runs other fibers -- the one holding the cell among them -- and a cell held across a preemption or a channel `receive` does not stop the program, even on one CPU. Fibers waiting for a cell are woken in the order they asked for it; a fiber that is running when the cell is let go may take it first, but not for longer than about a millisecond before the longest waiter is given its turn. The program's `main`, and every fiber on the threads backend, waits on its own thread. Two fibers each inside a change function and each waiting for the other's cell wait for ever; Khora detects only a fiber waiting for itself. A canceled fiber waiting for a cell in `update` or `modify` stops waiting: its change function does not run, and it stops at the `update` as it would at any cancellation point. In cleanup only `Fiber::abort` (or a `cancel_within` deadline running out) ends that wait. `get` and `set` have no answer for having given up, so they wait for the cell. **What a cancellation does to a change function.** Nothing inside one stops at a cancellation point -- not a loop, not a call, not even for a fiber stopped with `Fiber::abort` -- because it holds the cell's lock. What a cancellation does instead: - A blocking call inside it -- a channel `receive`, a `clock.sleep`, a socket read -- gives up at once with its "gave up" answer (`None`, an early return, `-1`), the change function carries on with that answer and returns, and the fiber stops after the `update`. - A `Fiber::join`, `wait` or `outcome` inside it that comes back stopped -- because this fiber was canceled, or because the child was stopped by somebody else -- ends the change function. **The change does not happen**: the cell keeps the value it had, the lock is released, and the caller stops at the `update` as it would at any call to a stopped fiber. - **In cleanup, those three wait.** Inside a `Region::defer` finalizer or `scoped` cleanup, a plain cancel does not end a `join`, `wait` or `outcome` in a change function: it waits for the child with the lock held, the change happens, and the rest of the cleanup runs. Only `Fiber::abort` (or a `cancel_within` deadline running out) ends that wait, and then the cell keeps the value it had. - A change function that loops without blocking runs to its end. One that loops for ever holds the lock for ever, and nothing -- not `abort`, not `cancel_within` -- ends it but the process ending. ## `Changed` and `modify` `modify` is the atomic operation to use when a state transition also needs to return a value other than the new state. Its result record is: ```khora pub type Changed = { state: A, result: B, }; ``` Example: ```khora let issued = Shared::modify(next_id, fn current => { { state: current + 1, result: current, } }); ``` Both the state replacement and the returned result belong to the same locked transition. ## `Channel` A channel is a bounded hand-off queue, not another kind of shared cell: ```khora pub type Channel; impl Share for Channel {} impl Channel { pub fn bounded(capacity: Int) -> Channel; pub fn dropping(capacity: Int) -> Channel; pub fn sliding(capacity: Int) -> Channel; pub fn send<'er>(self, value: A) -> Bool raises 'er; pub fn receive<'er>(self) -> Option raises 'er; pub fn poll(self) -> Option; pub fn close(self) -> (); pub fn depth(self) -> Int; } ``` Typical use: ```khora let jobs = Channel::bounded(64); if Channel::send(jobs, job) { () } else { handle_closed_queue(job) } match Channel::receive(jobs) { Option::Some(next) => process(next), Option::None => (), } ``` A send to a full channel suspends until space becomes available. A receive from an empty open channel suspends until a value arrives. Suspension gives the scheduler worker back; it is not a busy wait or a blocked worker thread. Both are cancellation points, and neither takes a `!`: neither raises an error, and a fiber canceled while parked on one stops there whatever its `raises` row says. A canceled channel operation is always canceled **empty-handed**. The runtime looks at the cancellation flag only once it has established there is nothing to take and no room to send, so a value arriving at the same moment as the cancellation is still delivered rather than dropped. A send that gives up releases its value, the same as a send to a closed channel. `poll` never waits, so it is not a cancellation point. `send` returns `false` when the channel is closed. Closing wakes waiters. Receivers drain values already queued before `receive` begins returning `Option::None`. A capacity less than one is treated as one. `Channel::bounded(0)` is therefore **not** a zero-capacity rendezvous channel. ### What a full channel does The behavior belongs to the channel, not to the send. A queue is lossy or it is not, and two senders disagreeing about which is not a state a queue can be in. | Constructor | A send into a full one | `send` answers | What is left | | --- | --- | --- | --- | | `bounded` | waits | `true` | everything | | `dropping` | refuses the new value | `false` | the oldest | | `sliding` | evicts the oldest | `true` | the newest | `bounded` is the default because backpressure is: a queue nobody is draining is a producer that should slow down. `dropping` is for a producer that must not stall — the request path writing an audit event, a handler emitting a metric. The `false` makes the loss a value the caller can count and report. `sliding` is for a feed where only the newest value matters: a gauge, a progress indicator, a last-known position. It answers `true` because nothing was refused, so the loss is invisible at the call site. That is deliberate — nobody was going to act on it. Use `dropping` where somebody would. ### `poll` `poll` takes a value if one is already there and never waits: ```khora match Channel::poll(jobs) { Option::Some(next) => process(next), Option::None => do_something_else(), } ``` `None` means "not right now", not "not ever": a closed and drained channel and a live empty one both answer `None`, and telling them apart is what `receive` is for. Use `poll` in a loop that has other work between looks. A loop that only polls is a loop that spins. `depth` is observational: concurrent activity can make the returned count stale immediately, so it is appropriate for metrics and tests rather than synchronization decisions. ## `SharedFn` A closure's function type does not reveal what the closure captured. `SharedFn` records the fact that a closure was checked for safe sharing at the point where its captures were visible: ```khora pub type SharedFn; impl Share for SharedFn {} impl SharedFn { pub fn of(f: (A) -> B raises 'er) -> SharedFn; pub fn call(self, argument: A) -> B raises 'er; } ``` Construct one from a closure literal or named function: ```khora let callback = SharedFn::of(fn request => handle(request)); let response = SharedFn::call(callback, request); ``` Use `SharedFn` when a callback must be stored inside another shareable value, such as a router or callback table. Its error `'er` need not be `Share`. `SharedFn::call` runs the closure on the calling fiber, so an error it raises is raised and caught on that one fiber and reaches no other. A `Fiber`'s error is different, because the fiber that joins receives it. ## `Fiber` A fiber handle is shareable, so one fiber can hold another's and act on it: ```khora pub fn main() -> () { let worker = Fiber::spawn(fn () => slowly()); // The handle crosses, so a second fiber can stop the first. let watcher = Fiber::spawn(fn () => Fiber::cancel(worker)); Fiber::wait(watcher); Fiber::wait(worker); print("both settled") } ``` `wait` needs no `!` here because neither child can fail. The fiber doing the waiting can itself be canceled while it is parked, and it stops there whatever its own `raises` row. **This is what a supervisor is made of**, and what a deadline would be made of: there is no `timeout` or `race` in `std` (see [Concurrency](/docs/v0.4/reference/concurrency/)), so anything of that shape is written from a handle one fiber holds and another cancels. What constrains it is the answer rather than the handle. `A: Share` is on the whole `impl Fiber`, because a value computed on one fiber and read on another has to be safe to hold twice — so it is a condition on having a `Fiber` at all, and `wait` needs it as much as `join` does even though `wait` never hands the answer back. The error is held to the same rule. Every error in a spawned body's `raises` row must be `Share`, because `join` and `outcome` hand the raised error to whoever joins, and two fibers may join one handle. It is asked at the spawn, and again once the function's types are settled if the row was not known there: ```text error: `Oops` does not implement `Share`, which a spawned fiber's error requires: whoever joins the fiber holds the error it raised. `Oops` can be written, and two fibers writing one value is a race ``` A `SharedFn`'s error is not held to it: a certified closure is called on the caller's fiber, so what it raises stays there. It may still not raise a `Region` or `Scope`. ## Regions stay home A `Region` stays on the fiber that opened it, and so does a `Scope`. Neither is shareable: a fiber's body cannot capture one, a channel cannot carry one, a `Shared` cell cannot hold one, a fiber cannot answer or raise one, and a handler for another effect cannot capture one. So a region's finalizers run on the fiber that deferred them, which is what lets a finalizer capture a record with `mut` fields that this fiber goes on writing. A child fiber that acquires something is given a scope of its own: ```khora let worker = Fiber::spawn(fn () => scoped(work)); ``` A lambda works as well, `Fiber::spawn(fn () => scoped(fn () => serve(connection)))`, and so does a named function whose body calls `scoped`. What the child acquires is released when the child's `scoped` ends, on every way out, including a cancellation or an `abort` of the child. The scope `scoped` hands its body is the one the body uses, also inside a function that has a `scope` of its own: there, `work()` in `scoped(fn () => work())` uses the new scope, not the enclosing one. What the body names is another matter. A lambda that writes `scope.defer(..)`, or captures `scope` under another name, uses the enclosing function's scope, and inside a spawn that is refused: ```text error: `scope` cannot be handed to another fiber: a `Scope` stays on the fiber that opened it, so that its finalizers run on the fiber that deferred them. To release something the child acquires, give the child a scope of its own, `Fiber::spawn(fn () => scoped(work))` ``` `Region::root()` and `Scope::root()` belong to the program's own fiber. A spawned fiber that reaches for either is stopped with a fatal error (exit status 134), because the root region is reachable by name and no type rule can keep a child off it. A `test` or `bench` block is given a root region of its own, released when the block ends. A fiber that a test spawns is refused the test's root like any other, and because the fatal error ends the process, it ends the whole `khora test` run: the tests still running are not reported, and there is no summary line. A region whose last reference goes on a fiber that did not open it is stopped with the same kind of fatal error, rather than running its finalizers on that fiber. What this costs: a child cannot acquire something and have its parent's longer scope release it. A resource that honestly has to outlive the child is a shareable one — a pool, a channel, a `Shared` cell — and the parent acquires it first and hands the shareable value in. ## Choosing the boundary Use `Shared` when several fibers coordinate around **one evolving value**. Use `Channel` when a value or unit of work is **handed to one receiver**, especially when backpressure matters. Use `SharedFn` when a **callback itself** must cross the sharing boundary. A `Fiber` handle crosses too, which is what lets one fiber cancel or wait on another. See [Concurrency](/docs/v0.4/reference/concurrency/) for fiber and nursery lifetime rules and [Memory and resources](/docs/v0.4/reference/memory-and-resources/) for structured cleanup. # Concurrency Source: https://khoralang.com/docs/v0.4/reference/concurrency/index.md Start concurrent work with `Fiber::spawn`. A fiber cannot outlive the block that started it, so nothing leaks when you leave. **There is no `async` keyword and no `await`.** A function that suspends looks like one that does not, so any function can call any function. ```khora let child = Fiber::spawn(fn () => load(id)!); let row = Fiber::join(child)!; ``` ## Fibers A fiber handle carries both the answer type and the failure row: ```khora pub type Fiber; impl Fiber { pub fn spawn(body: () -> A raises 'er) -> Fiber; pub fn join(self) -> A raises 'er; pub fn cancel(self) -> (); pub fn detach(self) -> (); pub fn wait(self) -> (); } ``` `A` must be `Share`: the value is computed on one fiber and read on another. So must every error in `'er`, for the same reason: `join` hands the error the body raised to whoever joins, and a handle can be joined from more than one fiber. A record with a `mut` field, an `Array` or a `Map` in a spawned body's `raises` row is refused at the spawn, and a `Region` or `Scope` there is refused with the message that names `scoped`. `Fiber::outcome` and `nursery.adopt` take the same handle, so the spawn is where each is checked. The error of a `test` block is not held to this: the test runner reports it and releases it, and it reaches no other fiber of the program. `wait` waits without taking the answer, which is what you need after `cancel`: a canceled fiber has no answer, so `join` on one unwinds the joiner along with it. `join` waits and answers what the body answered. If the body raised, `join` re-raises with the same type, so the caller catches by name: ```khora fn run(id: Int) -> () { let child = Fiber::spawn(fn () => load(id)!); let row = Fiber::join(child)! catch { DbError::Timeout => Row::empty(), DbError::Missing(_id) => Row::empty(), }; print(Int::to_string(row.total)) } ``` A body with an empty failure row needs no `!` on the join. Joining twice is joining once, from either side, and answers twice. A spawned closure may capture values that satisfy the sharing rules: ```khora fn print_later(value: Int) -> () { let child = Fiber::spawn(fn () => print(Int::to_string(value))); Fiber::join(child) } ``` Releasing the final `Fiber` handle also waits for the child. This means a fiber cannot silently outlive the scope that still owns its handle. `Fiber::detach` is the exception, and the only one: it stops waiting and asks the fiber to stop. Both halves are asynchronous — `detach` signals and returns at once, so the fiber keeps running until it reaches its next cancellation point rather than stopping where it is. Its answer is discarded when it arrives, and a later failure is silent — the program said it was no longer listening. It cancels as well as detaching, because a detached fiber nobody asked to stop is a leak with a nicer name. **Canceling a fiber running `Router::listen` works.** It was a known gap until the runtime learned to check for cancellation in the poll loop a parked `accept` sits in: a canceled listener now unwinds and the process exits. Cancel it, wait for it, and the port is released. The shape still worth keeping is **drain first, stop last**: stop accepting work, wait for what is in flight, and only then cancel the listener — otherwise connections mid-request are dropped rather than finished. [Serve HTTP](/docs/v0.4/cookbook/http-service/#stopping-a-service) has the code. Without it, a bounded wait over a body with an uninterruptible tail could not be honored. That is the failure it exists for: every other way out of a handle waits, letting the binding go included, so one finalizer that never returns holds its nursery, which holds its parent, up to `main`. Reach for it when a bounded wait matters more than a clean one, and not otherwise. ## Nurseries A nursery owns a set of fibers. The capability installed in a nursery body is: ```khora pub effect Nursery { adopt: (Fiber<(), 'er>) -> (), } ``` The answer is fixed at `()` and the row is not, and each half has its own reason. The answer is fixed because a nursery has nothing to do with a result it cannot hand back: it holds children as bare handles and waits for them. A fiber whose result matters is one whose handle you keep and `join`. The row stays for failures: it is how a child that can fail is adopted, and how its failure reaches the nursery. It has nothing to do with stopping a child. A cancellation leaves every function on a return of its own, whatever its row, so a child whose row is empty is stopped exactly like one whose row is not. `'er` is quantified per call rather than per handler, so children raising unrelated failures are adopted by one nursery. A body that starts children declares the requirement and adopts each handle; the child's body may raise, and no `catch` is needed at the adoption site: ```khora fn fan_out() -> () with { nursery: Nursery } { nursery.adopt(Fiber::spawn(fn () => first())); nursery.adopt(Fiber::spawn(fn () => second()!)); } ``` `nursery` installs that capability and waits for the children on the normal path: ```khora pub fn nursery( body: () -> A with { 'ef | nursery: Nursery } raises 'er ) -> A with 'ef raises 'er + ChildFailed ``` Example: ```khora fn run() -> () raises ChildFailed { nursery(fan_out)! } ``` When the body completes normally, `nursery` waits until every adopted child is finished. If the body leaves by failure or cancellation, releasing the nursery cancels children that are still running and waits for them before the scope is gone. A child that acquires something is given a scope of its own, because a `Scope` stays on the fiber that opened it and a child cannot be handed its parent's. Open it in the child: ```khora fn accept_loop(connection: Connection) -> () with { scope: Scope, nursery: Nursery } { nursery.adopt(Fiber::spawn(fn () => scoped(fn () => serve(connection)))); } ``` What the child acquires is released when its `scoped` ends, on the child, on every way out including its cancellation. `serve` uses the scope `scoped` hands it, not `accept_loop`'s own. [Sharing](/docs/v0.4/reference/sharing/#regions-stay-home) has the rule. ### What a fiber is made of A fiber is an operating-system thread. There is a second implementation — stackful coroutines on a pool of workers — behind an environment variable: ```bash KHORA_FIBERS=scheduler ./build/myapp ``` Threads are the default because they are faster at the connection counts a service actually runs at. The coroutine's advantage is *density*: a suspended fiber costs roughly 4 KB against a thread's 33 KB, which matters when tens of thousands are waiting rather than working. **The two answer cancellation alike** — a fiber in `clock.sleep`, on a channel or on another fiber is woken on either — but they schedule differently, so a program that depends on an order the language does not promise can tell them apart. [Known limitations](/docs/v0.4/limitations/#the-two-fiber-backends-are-distinguishable) has the detail. A fiber gets an eight-megabyte stack on either backend — what `main` gets from a default `ulimit -s` on Linux and macOS — so a recursion that fits in `main` fits in a fiber, and the backend does not change how deep a program may go. The stack is address space reserved up front and memory committed a page at a time as it is touched, so a fiber that never recurses deeply is resident for the few kilobytes it used. Running out is reported as in [Traps](/docs/v0.4/reference/traps/#running-out-of-stack); there is no setting that makes a fiber's stack larger. [Fibers](/docs/v0.4/internals/fibers/) describes how each is scheduled. ### A child that failed A nursery is a unit: the block asked for these fibers together, so one failing means the group's answer is not coming. The first failure cancels the siblings still running when the nursery sees it; every child is still waited for, and the nursery raises ```khora pub type ChildFailed = { children: Int }; ``` A count rather than the child's own error, because `adopt` binds the row per adoption — two children may fail with two unrelated types and there is no one value to hand back. A child the nursery *canceled* is not counted: that is what a nursery does to its children, not something that went wrong. **A failure is seen in adoption order.** A nursery reaps handles oldest-first, so a child's failure is not seen until every child adopted before it has finished, and it cancels only the siblings still running at that moment. A failing child adopted first cancels all its siblings within milliseconds; one adopted in the middle of twelve canceled between two and six of the eleven in measurement; one adopted last is seen after every sibling has finished and cancels none. Every child is waited for and the failure is always reported. When a sibling's failure has to stop work that is expensive, holds a resource, or has an effect outside the process, adopt the child that can fail first, or have that work check a `Shared` flag itself. [Known limitations](/docs/v0.4/limitations/#a-childs-failure-is-seen-late-unless-it-was-adopted-early) has the numbers. The body may be a named function or a lambda. A lambda resolves its capabilities where it is written, and as the argument to `nursery` that is inside the row `nursery` installs, so `nursery(fan_out)` and `nursery(fn () => fan_out())` mean the same thing. ### An operation can be generic in a row, but not in a type `adopt` binds `'er` and cannot bind an answer type, which is why `Fiber<(), 'er>` fixes the answer and leaves the failure row free. That asymmetry is a rule about every effect operation rather than about nurseries: [Effects and rows](/docs/v0.4/reference/effects/#an-operation-may-be-generic-in-a-row-but-not-in-a-type) has the reason, with `adopt` as its example. ### Why `adopt` takes a fiber and not a thunk A fiber's body must be written where it starts, so that what it closes over can be checked against the sharing rules. A thunk built somewhere else and forwarded to `spawn` inside the handler would move that check away from the code it is about. ## Bounded nurseries The bounded form has the same row behavior plus a concurrency limit: ```khora pub fn bounded_nursery( limit: Int, body: () -> A with { 'ef | nursery: Nursery } raises 'er ) -> A with 'ef raises 'er + ChildFailed ``` ```khora fn serve() -> () with { nursery: Nursery } { loop { let request = next_request(); nursery.adopt(Fiber::spawn(fn () => handle(request)!)); } } bounded_nursery(128, serve) ``` Adopting past a bounded nursery's limit waits for older work to finish. Use this for work whose arrival rate is controlled externally so overload becomes backpressure instead of unbounded growth. A fiber canceled while it waits for room passes the cancel to the child it is waiting on, and stops as soon as that child has. Two riders on the number, both measured rather than intended: - **The limit admits `limit + 1` live children.** `Fiber::spawn` *starts* the child and `nursery.adopt` is what blocks, so by the time the bound is applied the extra work is already running. `bounded_nursery(128, serve)` above is 129 live children. Subtract one where the limit stands for a real resource such as a connection pool. - **A limit of zero or less means no limit**, because that is how the unbounded `nursery` is built. A limit computed from configuration that comes out zero removes the bound rather than clamping to one; check it before you pass it. Use unbounded `nursery` when the fan-out is already bounded by data the program holds, such as a known handful of independent tasks. ## Cancellation Cancellation belongs to the target fiber: ```khora let child = Fiber::spawn(fn () => work()); Fiber::cancel(child); Fiber::wait(child)!; continue_parent(); ``` `wait` rather than `join`, because a canceled fiber has no answer: `join` on one unwinds the joiner along with it, and `continue_parent()` would never run. `Fiber::outcome` is the third choice, when the answer is wanted if there is one: it returns `Outcome::Answered(value)` or `Outcome::Stopped` without unwinding the caller. `wait` is itself a cancellation point: a waiter canceled while it is parked stops there. It needs no `raises` row for that. Canceling a child does not cancel its parent. ### When cleanup does not finish A canceled fiber runs its cleanup — its regions' finalizers — to completion, and a second `Fiber::cancel` does not cut it short. A finalizer that blocks for ever therefore holds its fiber, and anything waiting on it, for ever. Two operations end that: ```khora Fiber::abort(child); // stop now, even inside cleanup Fiber::cancel_within(child, 5000); // cancel now; abort if still running in 5 s Fiber::wait(child); ``` `abort` stops the fiber at its next cancellation point **including inside a finalizer**, and the children of any nursery it holds with it. What it costs is cleanup cut off part-way: a `ROLLBACK` not sent, a connection dropped rather than returned. Khora has no default grace period, so nothing aborts a fiber unless the program asks. `cancel_within` is the usual way to ask: it bounds how long cleanup may take, and a fiber that finishes before the deadline is not aborted. Two things `abort` does not interrupt: a single foreign (C) or file-system call already in progress, which finishes first; and a change function running under `Shared::update` or `Shared::modify`, which holds the cell's lock and runs to its end. A blocking call inside one gives up at once instead; a `Fiber::join` or `wait` inside one that comes back stopped ends it with the cell unchanged. Inside a finalizer, a plain cancel does not end such a `join` or `wait` — it waits for the child, and the rest of the finalizer runs — and `abort` does. [Sharing](/docs/v0.4/reference/sharing/) has the detail. **There is no `timeout`, no `race` and no `select`.** A deadline can be built by hand — [Timeouts and cancellation](/docs/v0.4/cookbook/timeouts-and-cancellation/) has the shape, and it works because canceling a fiber cancels the children of any nursery it holds. A race is harder: a parent canceled while it waits stops only once the child it waits on has stopped, so a hand-written race is bounded by how quickly the losing branch reaches its next cancellation point. [Known limitations](/docs/v0.4/limitations/#concurrency-combinators) has the measurements. What does work is waiting on handles: [Take work off a queue safely](/docs/v0.4/cookbook/taking-work-off-a-queue/) and [Bound concurrent work](/docs/v0.4/cookbook/bounded-concurrency/) are the nearest recipes, and [known limitations](/docs/v0.4/limitations/) is the page to check before assuming an operation exists. Cancellation is observed at cancellation points rather than between arbitrary source instructions. They are: - a `!` site, which is also where propagation and suspension are marked; - a **loop back-edge** — the point where `loop` or `while` goes round again; - a **call** to a function that can itself reach a cancellation point; and - a **blocking operation**: `Channel::send`, `Channel::receive`, `Fiber::wait`, `Fiber::join`, `clock.sleep`, and accepting, reading and writing on a network connection. A canceled read, write or accept gives up and the fiber stops there; it does not come back to the caller as a failed read. Opening a connection and waiting for a child process are not cancellation points: a fiber stops after the call returns. Every function has them, whatever its `raises` row says. A cancellation leaves a function on a return of its own, not on the error channel. A `!` observes a pending cancellation *before* the call it marks, so a computation already asked to stop does not do work it is about to throw away, and the arguments are not evaluated. **A value the fiber is already holding is discarded with it.** Between the point a value becomes the fiber's responsibility and the next cancellation point after that, nobody else knows the fiber has it — so a cancellation there drops it, cleanly and silently. The shape that meets this is a worker taking a job off a channel and then calling something with it. Register the value with a region before that call and the region's finalizer runs on the unwind; [Take work off a queue safely](/docs/v0.4/cookbook/taking-work-off-a-queue/) is the recipe. Reading the flag after the call instead would move the problem rather than remove it: the fiber would be holding the call's result at the same point. Work in flight across a cancellation boundary is at risk whichever side the boundary is read on. The back-edge is why an ordinary background worker can be stopped: ```khora fn reporter() -> () with { clock: Clock } { loop { clock.sleep(5000); report(); } } ``` There is no `!` and no `raises` row. A fiber running it stops at the loop, at the sleep, or inside `report`, whichever it reaches first. A blocked operation is woken by a cancellation on both backends, so a fiber parked in `clock.sleep`, on an empty channel or on another fiber stops without waiting out the wait. A single foreign (C) call or file-system call is not interrupted: the fiber finishes it and stops at the next cancellation point after it. The check on the two channel operations comes *after* the call rather than before it, and only when the call comes back **empty-handed**. The runtime looks at the cancellation flag only once it has established there is nothing to take and no room to send, so a value arriving at the same moment as the cancellation is delivered rather than discarded — a canceled receive is never holding a value nobody will see again. A send that gives up releases its value, the same as a send to a closed channel. ### What cancellation costs A function that can reach a cancellation point returns a small tag alongside its answer, and its caller checks it after the call. A function that reaches none — straight-line arithmetic, a field read — is compiled without one and cannot be stopped part-way, which is the point: there is nothing in it to stop at. A function that recurses -- by name, through a closure or a function-typed field, or through a function it hands to `attempt` or another `std` operation that calls it -- checks for a cancellation when it is entered, so deep recursion stops too. Cancellation is **not** a member of a `raises` row. A `catch` that handles every declared failure, `_` included, does not see a cancellation: it passes through. During cancellation, intervening regions are released and their finalizers run before the fiber terminates. See [Memory and resources](/docs/v0.4/reference/memory-and-resources/). ## Suspension and workers Waiting for nonblocking I/O, a timer, a channel, a join, or scheduler capacity suspends the fiber so the worker can execute other runnable work. Application code continues to look like ordinary calls: ```khora let bytes = receive(socket)!; let message = decode(bytes)!; handle(message) ``` Suspension is distinct from scheduler safepoints used for fairness. Fairness may move execution between workers without being a cancellation event. A fiber may resume on a different operating-system thread after suspension. Foreign code must therefore not carry a thread-local address, borrowed errno-like state, native-thread identity, or another thread-affine value across a Khora suspension unless the foreign API explicitly permits that migration. ## Sharing boundary A value captured by or handed to concurrent work has to satisfy the sharing rules, which are their own page: [Sharing](/docs/v0.4/reference/sharing/). # Traps Source: https://khoralang.com/docs/v0.4/reference/traps/index.md A trap is an unrecoverable programming error or violated invariant. Traps are intentionally separate from typed `raises` failures: they are not part of a function's failure row and cannot be handled with `catch`. ## Operations that trap Checked integer arithmetic traps when the mathematical result does not fit the type: ```khora let total = left + right; let product = width * height; ``` Integer division and remainder trap when the divisor is zero, and when the quotient does not fit — which for a signed type is the one pair `minimum / -1`: ```khora let each = total / people; let over = total % people; ``` Array access traps when the index is outside the array: ```khora let item = Array::get(items, index); ``` Other operations trap on a violated invariant, and this is the whole list: | operation | when | | --- | --- | | `String::from_bytes` | the bytes are not valid UTF-8 | | `String::byte` | the offset is outside the string | | `Vector::at` | the index is outside the vector | | `Decimal` arithmetic | addition, subtraction, multiplication, division, rescaling or truncation overflows | | `Shared::update` / `Shared::modify` | the change function re-enters the same cell, which would otherwise deadlock — see [Sharing](/docs/v0.4/reference/sharing/) | An assertion that fails in a test is **not** a trap. `assert` reports the failing line and the test continues to the end of the run, because a test suite that stopped at the first failure would report one problem per run. A trap is appropriate when continuing would mean the program itself is wrong. It is not appropriate for ordinary external input that a caller is expected to reject. ## Running out of stack Not a trap — the operating system ends the process and there is nothing to unwind onto — but it is reported the same way, because a program that stops without saying why is the worst of both: ``` khora: the stack ran out ``` on standard error, followed by the platform's stack-overflow exit status rather than 134. **The message covers every stack Khora code runs on**: the one the program started on, a fiber's on either backend, and a test's under `khora test`, on Linux, macOS and Windows. Reporting from an exhausted stack needs room set aside before the fault, and the runtime sets it aside on each of those threads, as an alternate signal stack. That includes a stack that runs out while the runtime is starting a thread or a program: the C library blocks signals for the moment it does that, and a fault in that moment cannot be reported, so the runtime checks for 16 KB of stack first and, when it is not there, stops the program before the start with the message above. On macOS a thread's stack overflow arrives as a different signal from the main thread's, and both are reported. A C library that starts its own threads deep in a Khora stack is outside what the runtime can check. **How deep is deep.** A fiber's stack is eight megabytes on either backend, the same as `main` gets from a default `ulimit -s` on Linux and macOS. What that buys depends on the frame: a function that matches a list cell and recurses on the tail went about 100,000 calls deep in a debug build and 260,000 in a release build on x86-64 Linux, in `main` and on a fiber alike. A larger stack is not available, by setting or by `spawn` option. A walk deeper than that is one to write as a loop. Khora does not guarantee tail-call optimization, so a function that recurses once per element of its input uses a frame per element. See [Known limitations](/docs/v0.4/limitations/) for what that means for `List` in practice, and which operations are unaffected. ## Traps are not `raises` A function does not declare arithmetic overflow in its `raises` row: ```khora fn area(width: Int, height: Int) -> Int { width * height } ``` If the multiplication overflows, `area` traps. Adding `catch` around the call does not turn that trap into a recoverable failure: ```khora // `catch` handles declared failures, not traps. let result = load()! catch { LoadError::Missing => fallback(), }; ``` Use `raises` for expected conditions a caller can make a decision about. Use a trap for an invariant the program was required to uphold. ## Ask explicitly for wrapping arithmetic When modular arithmetic is the intended operation—a hash, checksum, PRNG, or wire algorithm—use the wrapping operation by name instead of relying on build-mode overflow behavior: ```khora let mixed = Int::wrapping_add(left, right); ``` Checked arithmetic traps in every build; release builds do not silently change to wrapping arithmetic. ## Default runtime behavior By default, a trap: 1. writes a diagnostic to standard error; 2. identifies the operation or invalid index; 3. identifies the spawned fiber when the trap occurred on one; 4. terminates the process with status 134. A trap with no backtrace says how to get one, so the first thing a person sees tells them what to do next: ``` khora: Int addition overflowed on fiber 7 note: re-run with KHORA_BACKTRACE=1 to see where ``` ```bash KHORA_BACKTRACE=1 khora run . ``` `RUST_BACKTRACE=1` is honored too, because a person who has debugged a Rust program will try it. `KHORA_BACKTRACE` is the name to use and the one the runtime's own note gives. When debug information is available, the trace includes Khora source frames and locations. The runtime's own frames are trimmed from the top of it, so the first frame is the line in your program that trapped. Standard output is flushed before the process ends, so what the program had already printed is not lost. A process-fatal trap is **not** normal structured failure unwinding. Do not depend on regions, `catch`, or application finalizers to recover from it. Resource correctness must come from normal return, typed failure, and cancellation paths; a fatal trap means the process is ending. ## Exit statuses A trap is one of several ways a program ends without returning from `main`, and they are told apart from outside by the status: | status | what happened | | --- | --- | | the value | `main` returned an `Int`, truncated to what the platform's status holds. `()` is 0. | | 1 | An error reached the entry point and nothing handled it. `main` has nowhere to hand one, so it ends here. | | 130 | A **cancellation** reached the entry point. 128 + `SIGINT`, which is what a shell already means by "interrupted". | | 134 | A trap, everything above — and the runtime's own fatal errors. 128 + `SIGABRT`, and 134 on Windows too rather than a native abort code. | | the platform's | The stack ran out: `SIGSEGV` on Unix, which a shell reports as 139; `STATUS_STACK_OVERFLOW` on Windows, which a POSIX shell reports as 127. | **130 is the one that surprises people**, because a cancellation is not a failure and there is no `catch` for it. The common way to reach it is a `join` on a fiber that was canceled: a canceled fiber has no answer, so the join unwinds the joiner along with it, and if the joiner is `main` the program ends there. `join_all` joins, so it ends the same way. `Fiber::wait` waits without asking for an answer and does not do this — reach for it when what you needed was "not before that finishes" rather than the value, and `Fiber::detach` when you are no longer waiting at all. A cancellation that reaches a spawned fiber's root stops that fiber and nothing else; only one that reaches `main` ends the program. **The runtime's fatal errors exit 134 rather than aborting.** `abort()` leaves the status to the platform, and on Windows a POSIX shell reports that as 127 -- which is also what it reports for a stack overflow. Two unrelated deaths arriving as one number is what this table exists to prevent, so the runtime picks 134 the way a trap does. Stack exhaustion is the one death that cannot pick anything: its handler runs with the stack already gone, so it may not allocate, lock, or call `exit`. It reports and lets the platform end the process, which is why its row says "the platform's". A supervisor should treat 130 as a deliberate stop and 134 as a crash. They are different outcomes and this is where the difference is visible. **Read them with `waitpid`, not `$?`, where the difference matters.** 128 + *N* is a shell's way of reporting a child *killed by signal N*; a Khora program that exits 130 was not signaled, it chose that number. POSIX keeps the two apart -- `WIFSIGNALED` is true of a real `SIGINT`, and `WIFEXITED` with status 130 is true of this -- but a shell collapses both into one `$?`. So a script cannot tell "somebody pressed Ctrl-C" from "a cancellation reached the entry point", and a supervisor that cares should read the wait status rather than the shell. **A signaled Khora program produces both, in order**: the first `SIGTERM` or `SIGINT` becomes a cancellation and the program *exits* 130, and a second one restores the default disposition and re-raises, so what a supervisor then sees is a genuine `WIFSIGNALED` — not an `exit` imitating one. On Windows there are no signals in this sense at all: the numbers above are Khora's own, chosen to match what a Unix shell would have said. ## Containment for exported C calls A host embedding a Khora shared library may explicitly opt into trap containment at the **export boundary**: ```c khora_set_trap_policy(1); int64_t value = price(units, scale); if (khora_trapped()) { khora_clear_trap(); /* the host process is still running */ } ``` The default remains process-fatal. Containment occurs only after the host enables it and only while entering through a generated `pub extern fn` export wrapper. When an exported call is contained, the call is discarded and allocations created by that guarded call are released before control returns to the host. The trap is still reported; containment prevents that one bug from terminating the embedding process. This is **not** a general exception mechanism and is not equivalent to `raises`. ## Why containment is limited to exports An exported C function has a deliberately narrow signature: scalars and `Ptr` cross the boundary; exported functions are not generic and do not expose Khora values, capability rows, or typed failures to the host. That makes the exported call a boundary whose newly created Khora allocations can be discarded without leaving a Khora object in the caller. If a guarded exported call spawns a fiber, containment is disarmed for that call. A spawned child may outlive the C call and may hold references to allocations made by it; freeing those allocations underneath the child would be unsound. In that case a trap retains the ordinary process-fatal behavior. There is currently no general request-fiber or arbitrary-fiber trap containment mechanism. Do not design application recovery around trapping inside a fiber and continuing the service. ## What this means for a server **A trap in a request handler ends the server, not the request.** This is the consequence of everything above, and it is worth stating plainly because the code around a handler reads as though it were not true. `Router::listen` serves each connection on its own fiber and wraps your handler so that a typed failure becomes a 500 rather than a dropped connection. That wrapper handles `raises`. A trap is not a raise: it does not unwind, so it does not reach the wrapper, and the process exits with status 134. Every other connection being served at that moment ends with it, including ones that had nothing to do with the request that trapped. Design for it in three places. **Validate at the boundary, not in the handler.** An integer that came from a request is external input until a schema has read it. `std::schema` rejects a port outside 1 to 65535 as a 400; the same value multiplied inside a handler traps and takes the process down. This is the reason the standard library's own HTTP reader caps a `Content-Length` before multiplying it, answering 413 rather than trapping. **Run more than one process.** Since the blast radius of a trap is the process, the unit of redundancy is the process. A server behind a load balancer with at least two instances survives one trapping; a single instance does not. This is ordinary operational practice and Khora does not change it, but a language that contained traps per request would let you skip it, and this one does not. **Expect a restart, and make it cheap.** Status 134 is a crash as far as a supervisor, container runtime or service manager is concerned, and the right response is a restart. Hold no state in the process that a restart cannot rebuild or that a second instance cannot serve from. What you get in exchange is that a trap is never silently swallowed. A handler that overflows is a bug in your program, and the process ending with a message naming the line is a louder and shorter path to fixing it than a 500 that happens to a fraction of requests and is discovered in a log a week later. ## Validate external data before invariant operations Untrusted input should become an ordinary value or typed failure before it reaches an operation whose precondition may trap: ```khora fn item_at(items: Array, index: Int) -> Item raises InputError { if index < 0 || index >= Array::length(items) { raise InputError::InvalidIndex(index) } Array::get(items, index) } ``` At that boundary, an invalid client-supplied index is an expected failure; the array access after validation is an invariant the program can safely rely on. See [Failures](/docs/v0.4/reference/failures/) for recoverable errors and [FFI](/docs/v0.4/reference/ffi/) for the exported C boundary. # Foreign function interface (FFI) Source: https://khoralang.com/docs/v0.4/reference/ffi/index.md Khora does not define a stable native Khora-to-Khora ABI. The stable interoperability boundary is the C ABI, expressed with `extern fn` and the opaque `Ptr` type. ## Import a C function An `extern fn` declaration with no body names a C symbol that must be provided when the program is linked: ```khora extern fn monotonic_ticks() -> Int; extern fn read_into(handle: Ptr, into: Ptr, capacity: Int) -> Int; ``` It is called like an ordinary Khora function: ```khora let now = monotonic_ticks(); ``` Foreign declarations are subject to the package's extern permissions. That permission controls whether the package may declare the boundary; it is not process sandboxing. ### Link against a native library An `extern fn` with no body needs its symbol to arrive from somewhere. The root package names the libraries: ```toml [permissions] extern = ["myapp"] [build] link = ["answer"] link-search = ["./vendor"] ``` `link` names libraries the way the linker does — `"answer"` becomes `-lanswer`, matching `libanswer.a` or `answer.lib`. `link-search` adds directories to search, relative to the manifest. A name with no directory listed is looked for where the system keeps its libraries. The `extern` permission governs the declarations rather than the link line: a package not allowed to declare an `extern fn` cannot call anything a library provides, so listing one in `link` gives it nothing to reach. What keeps the link line honest is the rule below — only the root package writes it. ### A dependency ships the library; the root package links it **`build.link` is read from the root manifest and nowhere else.** A dependency may ship an archive and declare `extern fn` against it — that is how a driver package works — but it cannot put a flag on the link line of a program that merely depends on it. A transitive package adding a native library to your build would be a supply-chain change with no signal at the place that would have to consent. So the package does the work, and the program that uses it writes one line saying yes: ```toml [permissions] extern = ["myapp", "postgres"] [build] link = ["pq"] link-search = ["../postgres/vendor"] ``` Until that line is there the build fails at the linker, naming the symbol it could not find. The consequence worth the trade: every native library a program links can be read off its own manifest. ### What this does not cover - **wasm.** A WebAssembly module's imports are resolved by its host, so there is nothing for `-l` to resolve against; `build.link` is refused for that target. - **Cross-compilation.** Linking for another triple needs a sysroot story that does not exist yet — see [Supported targets](/docs/v0.4/deployment/supported-targets/). `pub extern fn` — exporting *out* of Khora — is unaffected either way, because there the symbol is one this program defines. ## Export a Khora function to C A public extern function **with a body** publishes a C-callable symbol: ```khora pub extern fn price(units: Int, scale: Int) -> Int { units * scale } ``` Build a shared library with: ```bash khora build --lib ``` The toolchain writes the shared library and a C header generated from the exported signatures. `pub extern fn` is therefore the source form for a C export; an `extern fn` without a body is a symbol Khora expects to find on the foreign side. ## ABI-safe types The C boundary is intentionally narrow. Values may cross directly when they are C-compatible scalars or `Ptr`. Do not put Khora-managed aggregates across the boundary. In particular, exported/imported ABI signatures do not carry Khora `String` values, algebraic data types, records, closures, generic values, or typed failure returns. An exported function is also not generic and cannot expose a `raises` or capability requirement to its C caller: ```khora // C-callable shape pub extern fn sum(left: Int, right: Int) -> Int { left + right } ``` Keep richer Khora modeling behind that narrow entry point and translate to/from scalar or pointer-shaped data at the boundary. ## `Ptr` is an opaque foreign address `Ptr` represents a machine address supplied by foreign code: ```khora pub type Ptr; impl Ptr { pub fn null() -> Ptr; pub fn is_null(self) -> Bool; } ``` Khora does not expose general pointer arithmetic or dereferencing through `Ptr`, and a `Ptr` is not a reference-counted pointer into the Khora heap. A foreign handle can therefore be kept as an opaque `Ptr` and passed back to the library that created it: ```khora extern fn open_native() -> Ptr; extern fn close_native(handle: Ptr) -> (); ``` The foreign API still defines the handle's lifetime. Use a structured resource scope when the handle requires cleanup. ## Borrow buffers for one call When C needs temporary access to bytes owned by Khora, lend them for the duration of a callback rather than exposing a long-lived pointer. For strings, the standard forms are: ```khora String::with_c_string(text, body) ``` where `body` receives a temporary `Ptr` to NUL-terminated bytes, and: ```khora String::with_data(text, body) ``` where `body` receives `(Ptr, Int)` for the bytes and their length. The corresponding function types preserve the callback's effects and failures while keeping the pointer lifetime inside the call: ```khora fn with_c_string( self, body: (Ptr) -> B with 'ef raises 'er ) -> B with 'ef raises 'er fn with_data( self, body: (Ptr, Int) -> B with 'ef raises 'er ) -> B with 'ef raises 'er ``` Do not retain those borrowed pointers after `body` returns. For data returned **to** C, prefer caller-owned buffers: the C caller allocates memory and passes `(Ptr, capacity)`, while the Khora export fills it and returns a scalar length/status. This avoids sharing an allocator or requiring C to free Khora-managed objects. ## Blocking and suspension A fiber may resume on a different operating-system thread after a Khora suspension. Foreign code must not retain any of the following across such a suspension unless the native API explicitly makes that safe: - a thread-local address; - native thread identity; - borrowed errno-like thread state; - a thread-affine handle whose contract requires one OS thread. Potentially blocking native work must run on a fiber of its own rather than on the one that needs the answer: ```khora extern fn slow_native_thing(handle: Ptr) -> Int; fn measure(handle: Ptr) -> Int { Fiber::join(Fiber::spawn(fn () => slow_native_thing(handle)))! } ``` **That is the boundary**, and it is written at the call site rather than wrapped in a helper. A fiber is an operating-system thread, so the blocking call holds a thread that is running nothing else, and the caller suspends the way it would for a socket. There is no `blocking(body)` in `std` to reach for instead, and the reason is a property of the language rather than an omission. A closure's captures are not part of its type, so nothing at a `spawn` can tell whether what it captured may cross to another fiber — which is why `Fiber::spawn` requires the closure to be *written where it is spawned*, and why a helper taking `body` as a parameter is refused: > `body` cannot be handed to another fiber: `() -> A` holds a closure, and what a closure captured is not in its type — so nothing here can tell whether *that* can be written. Two consequences worth planning for. The cost is a thread and the round trip to start and join one, which is the wrong trade for a call that takes a microsecond: reach for this when the work would hold a thread long enough to matter. And it is **not a cancellation point on the far side** — a canceled caller stops at the join while the native call runs to its end, because the runtime cannot interrupt foreign code and returning early would mean doing so while a thread still holds the caller's buffer. Under the M:N coroutine backend — which already ships behind `KHORA_FIBERS=scheduler` — blocking on a fiber occupies a worker, and this shape is what the runtime can route to its blocking pool. A direct call to the native function is not. An ordinary foreign call also cannot secretly suspend Khora while foreign stack or thread-affine state remains live around the call. ## Callbacks C keeps and calls later A C caller may take the address of a `pub extern fn` and call it whenever it likes — a signal handler, a callback registered with a library, a thread of its own. Three questions decide whether that is safe, and all three have answers. **The pointer lasts as long as the process.** A `pub extern fn` is an exported symbol in the shared library, not a value that was allocated: its address is fixed once the library is loaded and stays valid until it is unloaded. There is nothing to free and nothing to keep alive. **A Khora closure cannot be exported at all, and is not a callback you can hand out.** A closure is a code pointer *and* the values it captured; the code pointer alone cannot be called, and there is no C type for the pair. If a foreign API wants a callback with user data, export a `pub extern fn` taking a `Ptr` and let C pass its own context through it. **The runtime must already be running.** An exported function may allocate, raise, or trap, and all three need the runtime that the library's own initialization starts. Calling an export before the library is loaded is not a Khora question; calling one *after* it has been unloaded is undefined in the same way calling any unloaded symbol is. **Re-entrancy is allowed and is an ordinary call.** C entering Khora while another Khora call is already on the stack — a callback invoked from inside a foreign function that Khora itself called — is a nested call on the same thread and needs nothing special. What it does not do is join the outer call: it is a separate exported boundary with its own error handling, so a `raises` row is discharged there rather than traveling out through the foreign frames in between, which could not carry it. A trap is process-fatal by default in a callback exactly as it is anywhere else; where the host has opted into export-boundary containment, the callback is its own boundary and not part of the outer one. Two rules from elsewhere on this page apply with particular force here, because a retained callback is exactly where they get broken: - The callback may be entered on **any** host thread, including one Khora has never seen (`Libraries may be called from several host threads`). - Anything the callback was handed — a borrowed buffer, a `Ptr` into Khora memory — is valid for that call and no longer, so a callback that stores one for the next invocation is storing a dangling pointer (`Borrow buffers for one call`). ## Libraries may be called from several host threads A library build cannot assume that the host calls it from only one OS thread. Python, Node, C/C++, or another embedding process controls which host threads enter exported functions. Design exported code and foreign state accordingly; do not infer single-threaded execution merely because the Khora library itself never spawns a fiber. ## Traps at an exported boundary The default trap policy is still process-fatal. An embedding host can explicitly opt into the export-boundary containment API when it cannot allow a Khora bug to terminate the host process. Containment is deliberately narrower than general fiber/request containment and has restrictions, including the fact that an exported call that spawns a fiber cannot be safely contained this way. See [Traps](/docs/v0.4/reference/traps/) for the exact host-side form and current containment rules, [Memory and resources](/docs/v0.4/reference/memory-and-resources/) for cleanup, and [Concurrency](/docs/v0.4/reference/concurrency/) for thread migration after suspension. # Modules and packages Source: https://khoralang.com/docs/v0.4/reference/modules-and-packages/index.md 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](/docs/v0.4/reference/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](/docs/v0.4/reference/declarations/#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. ## Package manifest A minimal package: ```toml [package] name = "orders" version = "0.1.0" [toolchain] version = "0.4.0" ``` Source lives under `src/`. [The manifest](/docs/v0.4/reference/manifest/) documents every table; [your first Khora project](/docs/v0.4/getting-started/first-project/) builds one end to end. ## Paths versus fields Khora spells compile-time names and runtime values differently, and the difference is load-bearing when reading unfamiliar code: ```khora 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`: ```khora 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](/docs/v0.4/reference/traits/#two-traits-declaring-the-same-method). 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. ### 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: ```khora // 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 `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/`: ```text 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: ```bash 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. ## Dependencies Package dependencies are source dependencies. Khora fetches and compiles the package's source; there are no binary artifacts to publish or to trust. ```toml [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: ```bash 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 ` explains what pulled something in, and `khora graph` draws the whole thing. ### The lockfile Resolution writes `khora.lock`, and a locked build never asks the network again: ```toml [[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. ### Publishing a package A package is consumable when its manifest says so: ```toml [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. ### Writing a library ``` 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: ```khora 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: ```khora 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: ```toml [dependencies] semver = { path = "../semver" } ``` ## Toolchain pinning Every project says which compiler builds it, and the field is required: ```toml [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](/docs/v0.4/reference/manifest/#toolchain--which-khora-builds-this) has the detail; [Installation](/docs/v0.4/getting-started/installation/#pin-a-project-to-a-compiler-version) covers toolchain management. ## 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](/docs/v0.4/reference/compatibility/). ## 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](/docs/v0.4/reference/ffi/). # The manifest Source: https://khoralang.com/docs/v0.4/reference/manifest/index.md Every Khora project has a `khora.toml` at its root. It names the package, says which compiler builds it, and declares what the code is allowed to reach. `khora new` writes one: ```toml [package] name = "hello_khora" version = "0.1.0" # Which Khora builds this project. Required. [toolchain] version = "0.4.0" ``` Tables may appear in any order. A key the compiler does not recognize is a warning rather than an error, so a manifest written for a newer Khora still builds with an older one — you are told what was ignored instead of being stopped. ## `[toolchain]` — which Khora builds this **Required.** Without it, every command stops and tells you what to add. ```toml [toolchain] version = "0.4.0" ``` | Key | Value | | --- | --- | | `version` | An exact version, or `latest`, or `latest.rc`. Required. | The version selects the compiler. Run `khora build` in a project pinned to `0.3.0` while `0.4.0` is on your path, and `0.4.0` hands the whole command over to the `0.3.0` you have installed — the build, the tests, the formatter and the editor's language server all follow the pin. A pinned version that is not installed stops the command and names it, rather than quietly building with something else. There are no ranges. A range needs a resolver, and a resolver reintroduces the thing a pin exists to remove: two machines agreeing on a constraint and disagreeing on a compiler. ### The two channels `latest` means the newest release installed on this machine; `latest.rc` includes release candidates. **They are deliberately not reproducible.** They resolve when they are read, so the same commit builds under a different compiler the moment anything new is installed, and under a different one again on a colleague's machine. Write a version for a project you want built the same way twice; the channels are for testing against whatever you have. Both resolve against installed toolchains and never over the network. Asking a server which release is newest would put a request in front of every command, including the ones your editor makes while you type. `khora update` is what makes a new toolchain available; a channel only decides which of the ones already present to run. ### Where the pin is written The pin is found by walking up from wherever you are to the nearest manifest that has one, so in a workspace it belongs at the root and members inherit it. Two members of one workspace pinning different compilers is not a thing anybody means, and a member that repeats the root's pin has written the same answer twice in two places that can drift apart. ## `[package]` — what this is ```toml [package] name = "orders" version = "0.1.0" authors = ["A Name "] publish = true ``` | Key | Value | | --- | --- | | `name` | Required. The package's name, and the first segment of every module path in it, so it is an identifier: letters, digits and underscores. Not hyphens. | | `version` | Required. A semantic version, such as `0.1.0`. | | `authors` | A list. Defaults to empty. | | `publish` | Whether the package is offered for others to depend on. Absent means no. | `publish` is an intent marker rather than a permission: anybody can write a `[dependencies]` entry by hand whatever it says, and a `path` dependency ignores it because that is your own working copy. What it prevents is depending on somebody's application, or their half-finished experiment, by accident. A manifest with no `[package]` is a workspace root, which is a normal thing to be — see below. :::note[There is no `edition` key] [`[toolchain]`](#toolchain--which-khora-builds-this) says which compiler builds the project. A manifest that still has an `edition` line gets a warning and builds. ::: ## `[workspace]` — several packages, built together A root manifest with no `[package]` of its own: ```toml [workspace] members = ["packages/*", "examples/*"] exclude = ["packages/scratch"] [workspace.package] version = "0.4.0" authors = ["A Name "] [toolchain] version = "0.4.0" ``` | Key | Value | | --- | --- | | `members` | Globs matching member directories. A directory matches only if it has a `khora.toml`. | | `exclude` | Globs removed from what `members` matched. | | `package` | Values members may inherit — `version`, `authors`, `publish`. | | `permissions` | A grants table members may take whole. | | `fmt`, `lints` | Shared formatting and lint settings. | | `lint-groups` | Lint group files, for `lints` to switch on. See [`[lint-groups]`](#lint-groups--this-projects-own-lint-groups). | | `policy` | A cap on what any member may grant. See below. | A root does not have to declare a package, and forcing it to would mean inventing a name for something that does not exist — a name that then turns up in error messages. ### Inheriting Nothing is inherited implicitly. A member that wants a shared value says so: ```toml [package] name = "alpha" version.workspace = true [fmt] workspace = true ``` `workspace = true` on a whole table takes that table entire, and grants written beside it are an error rather than being silently dropped. A member that asks to inherit something the root does not define is an error too, naming the field and the table it should be in. ### `[workspace.policy]` — a cap on grants **The values are member names.** A policy says which members may ask for a category at all, not which hosts or paths they may reach: ```toml [workspace.policy] network = ["gateway"] fs = ["reports"] env = ["gateway", "reports"] extern = ["sqlite"] ``` Only `gateway` may write a `[permissions] network` entry; only `reports` may write `[permissions.fs]`. Neither is told anything about *which* host or path — `network = ["*.internal:5432"]` here is not a narrower cap, it is a member name that does not exist, and the root is refused with `` `*.internal:5432` is not a member of the workspace ``. A typo in a cap is a cap that does not apply, so it fails loudly at the root rather than quietly wherever it should have bitten. A member that asks for a capped category without being named is refused, and the message says where the cap is and what to do about it: ```text `cli` is not allowed to grant `fs`. The workspace at .../khora.toml caps it to reports. Add `cli` to `[workspace.policy] fs` if it should be, or drop the grant ``` A category the policy does not mention is uncapped. This is the one place a workspace overrules a member rather than offering it something. **`process` is not one of the categories a policy caps.** The key parses, and a name in it that is not a member is refused like any other, so it reads as though it works. It does not: the check that refuses a member for granting a capped category runs over `network`, `fs`, `env` and `extern`, and never over `process`. A root that writes `process = ["gateway"]` does not stop any other member writing `[permissions] process`. Leave it out rather than relying on it, and cap the grant in the member's own manifest. Capping the *values* — "no member may reach anything outside `*.internal`" — is a different feature and is not here: it needs a rule for when one glob is narrower than another, and a version of that rule that is subtly wrong is a cap that looks enforced and is not. ### `--since` — building only what changed `khora check`, `khora fmt` and `khora task` take `--since ` at a workspace root, and `khora release` requires it: ```bash khora check --since main khora task test --since origin/main khora release --since v0.3.0 ``` `` is anything `git diff` takes — a branch, a tag or a commit. The selection is exact rather than heuristic: the resolver already knows which packages each member compiles, so it takes the changed files, finds the members they belong to, and adds every member that depends on one of those. A workspace of thirty packages where two changed checks two. A changed file that belongs to no member, and to nothing a member depends on — the root `khora.toml`, a CI script — selects *everything*, and the output says which file did it. That is the safe direction: a build tool that guesses a file does not matter is a build tool that skips the check that would have caught it. ## `[permissions]` — what the code may reach Khora has no ambient authority: a function that touches the network says so in its type, and this table is where a package's grants are written down. ```toml [permissions] default = "deny" network = ["api.example.com:443", "*.internal:5432"] env = ["HOME", "DATABASE_URL"] process = ["git", "docker"] extern = ["sqlite_sys"] [permissions.fs] read = ["data/**", "logs/*.log"] write = ["logs/**"] ``` | Key | Value | | --- | --- | | `workspace` | `true` to take the root's table whole. | | `default` | What a category nobody wrote down grants: `allow` or `deny`. `allow` is the default, so a program that has never heard of permissions compiles. `deny` is the strict posture: one line, set once, and every capability after it is a deliberate edit. | | `network` | Hosts the program may **connect out to**, as `name` or `name:port`. `*` spans dots, so `*.internal` covers `db.eu.internal`; a grant with no port covers every port. **Outbound only**: a port the program *binds* is not a host it reaches, so `Router::listen` and the rest of the server side are not covered by this key, or by any other — see [known limitations](/docs/v0.4/limitations/#inbound-connections-are-not-permissioned). | | `fs` | **A table, not a list**: `[permissions.fs]` with `read` and `write`. `*` stops at a separator and `**` crosses one, and neither covers the directory being described. | | `env` | Environment variable names. `*` spans everything, since a name has no segments. | | `process` | Program names, as written at the call: `run("git", ..)` names `git`. `*` spans everything, so `git*` covers `git` and `gitk`. | | `extern` | **Package names**, not library names: which packages may declare `extern fn`. `std` always may. | **`fs` is the one key that is a table**, because reading and writing are not the same grant and a single list cannot say which one it is. The rest are lists. `[workspace.policy]` above takes `fs` as a *list*, because there it names which members may grant filesystem access at all rather than which paths they may reach. **`process` is what stops the rest of the table being advisory.** A program that may run another program can ask it to do anything the program itself may not: with `read = ["data/**"]` and no `process` grant, both `read_text("/etc/hostname")` and `checked_output("cat", ["/etc/hostname"])` are refused. A refused program raises `ProcessError::Denied`, which is a separate case from `NotStarted` for the reason `IoError::Denied` is separate from `Failed`: one sends the reader to their `PATH` and the other to a line in a file they own. **A shell line is checked on its first word, which is a weaker promise** and is said here rather than left to be found. `Process`'s `shell` runs a string nobody has parsed, so `sh -c 'a; b'` runs two programs and the grant sees one. That is the bound; `run` is the operation for a command built out of anything that came from outside the program, and it takes its arguments as a list so nothing re-parses them. **`default` applies to a category you did not write down, not to one you wrote down empty.** `network = []` grants no host; leaving `network` out entirely takes `default`. So `default = "deny"` on its own denies every category, and `default = "deny"` beside one `[permissions.fs]` grant allows exactly that grant and nothing else. ## `[dependencies]` — other packages ```toml [dependencies] serde = { version = "1.2.0" } shared = { path = "../shared" } tools = { git = "https://example.com/tools.kh", tag = "v1.4.0", subdir = "core" } ``` Exactly one of `version`, `path` and `git` says where a package comes from. A `path` is resolved relative to this manifest and needs no version, because the source is right there. A `git` dependency takes `rev` or `tag`, and `subdir` when the package is not at the repository root. **`version` has nothing to resolve against yet.** There is no public registry, so the key is accepted and reserved rather than usable; today a dependency comes from a `path` or a `git` URL. [Modules and packages](/docs/v0.4/reference/modules-and-packages/#dependencies) is the same point from the other side. ## `[fmt]` — how `khora fmt` writes ```toml [fmt] indent-style = "space" indent-width = 2 ``` | Key | Value | | --- | --- | | `workspace` | `true` to take the root's table whole. | | `indent-style` | `space` or `tab`. | | `indent-width` | A number. | ## `[lints]` — turning findings up and down ```toml [lints] undocumented-export = "deny" unused-import = "allow" ``` Each key is a lint name or a lint group, and a lint's value is `allow`, `warn` or `deny`. A group is always a table, `[lints.]`, with an optional `level`. The names, and how groups combine with lints, are in [Lints](/docs/v0.4/reference/lints/). `workspace = true` takes the root's table whole, together with the root's `[workspace.lint-groups]`. `[lints]` and `[lint-groups]` are read by `khora check`, `build`, `run`, `test` and `bench`, and by the language server. `khora fmt` and `khora doc` do not read them, so a lint setting that stops `check` does not stop those two. ## `[lint-groups]` — this project's own lint groups ```toml [lint-groups] strict = "lints/strict.toml" [lints.strict] ``` Each key is a group name and each value is the path of the group's file, relative to this manifest. Declaring a group does not switch it on; the `[lints.strict]` table does. The file's format is under [Groups](/docs/v0.4/reference/lints/#groups). A name here cannot be a lint's name or a built-in group's name, and cannot contain `::`. A file that is missing, does not parse or names a lint that does not exist is an error naming the file and the key, and `khora check` and `khora build` stop until it is fixed. In a workspace root the table is `[workspace.lint-groups]`, with paths relative to the root. A member with `lints.workspace = true` takes it along with `[workspace.lints]`, and a `[lint-groups]` table of its own beside that flag is an error. ## `[build]` — what to produce ```toml [build] target = "x86_64-unknown-linux-gnu" ``` | Key | Value | | --- | --- | | `target` | The triple to compile for. **Not read yet** — see below. | | `plugin` | A build plugin, named and versioned — `protobuf-compiler@2.1`. It names a plugin rather than pointing at a script. **Not read yet** — see below. | | `link` | Native libraries to link, named as the linker names them: `link = ["pq"]` becomes `-lpq`. | | `link-search` | Directories to search for those libraries, relative to this manifest. | **`link` is read from the root package only.** A dependency may ship an archive and declare `extern fn` against it, but it cannot add a flag to your link line — see [Foreign function interface](/docs/v0.4/reference/ffi/#a-dependency-ships-the-library-the-root-package-links-it). Both keys need the package to hold the `extern` permission, because linking a native library is how an `extern fn` is satisfied. **Neither `target` nor `plugin` does anything today, and the toolchain says so.** Both are recognized, both are documented here because the decisions behind them are made, and setting either gets you a warning rather than silence: ``` warning: khora.toml: 6:1: nothing reads `build.target`: cross-compilation is not supported yet, so the build is for the host either way. ``` `target` waits on cross-compilation, which needs a linker and sysroot story rather than code generation — see [Supported targets](/docs/v0.4/deployment/supported-targets/). `KHORA_TARGET` makes the compiler *emit* for another triple, which checks code generation and does not produce a runnable artifact. `plugin` waits on the sandboxed WASM plugin mechanism; until it exists, [`[tasks]`](#tasks--project-commands) is the thing that runs commands, and it runs only what you wrote in a manifest you are standing in. ## `[tasks]` — project commands ```toml [tasks.migrate] description = "Bring the development database up to date" run = "khora run src/bin/migrate.kh" [tasks.ci] description = "What the pipeline runs" depends_on = ["fmt", "check", "test"] ``` `khora task migrate` runs one; `khora task` with no argument lists them with their descriptions. A task with no `run` is a grouping, which is what `ci` above is — unless its name is one of the toolchain's own verbs, in which case it runs that. So `depends_on = ["fmt", "check", "test"]` works without declaring three tasks that only say what `khora fmt`, `khora check` and `khora test` already do. **This is not a build script.** A task runs only when somebody types `khora task ` in a manifest they are standing in. Nothing reaches it during resolution, fetching or building, and a dependency's tasks are never even read. | Key | Value | | --- | --- | | `description` | Shown in the listing. | | `run` | The command line. | | `depends_on` | Tasks to run first, in order. | # Testing and benchmarks Source: https://khoralang.com/docs/v0.4/reference/testing/index.md `test` and `bench` are declarations, not a naming convention, and they live beside ordinary source. Tests run with `khora test`; benchmarks run with `khora bench`. The declaration syntax is in [Declarations](/docs/v0.4/reference/declarations/#tests); this page is how they are used. ## Write a test A test has a string name and a block: ```khora module pricing_test; import std::core::{assert}; fn double(value: Int) -> Int { value * 2 } test "double returns twice its input" { assert(double(21) == 42); } ``` `assert` is a `std::core` function, not a keyword: the `import` line above is required. It is also legal **only inside a `test` block** — in application code the compiler answers ``error: `assert` is only allowed inside a `test` block; elsewhere, `raise` says the same thing and says where it goes``, and it does so at `khora check`, so the editor shows it as you type rather than the build finding what the check passed. Run a package's tests from its root: ```bash khora test . ``` Filter by a substring of the test name: ```bash khora test . --filter double ``` ## Typed failures Use the same `catch` or `attempt` that application code uses: ```khora module users_test; import std::core::{Result, assert, attempt}; pub type UserError = | NotFound(id: Int) | Unavailable(why: String); fn load_user(id: Int) -> String raises UserError { if id == 999 { raise UserError::NotFound(999) } else { "ada" } } test "missing users are reported" { let result = attempt(fn () => load_user(999)!); match result { Result::Ok(_) => assert(false), Result::Err(UserError::NotFound(id)) => assert(id == 999), Result::Err(UserError::Unavailable(_)) => assert(false), } } ``` `attempt` and `Result` are `std::core` names like `assert`, so all three are on the `import` line. `UserError` and `load_user` are declared here to keep the block runnable; in a real test they come from the module under test. [Failures](/docs/v0.4/reference/failures/) is the model behind it. ## Capabilities, supplied by the test Effectful code takes controlled handlers instead of reaching real services: ```khora import std::clock::{Clock}; const fixed_clock = handler for Clock { unix_seconds: fn () => 1700000000, unix_millis: fn () => 1700000000000, monotonic_millis: fn () => 5000, sleep: fn _millis => (), }; test "session uses the supplied clock" { let session = create_session(user) with { clock: fixed_clock, }; assert(session.created_at == 1700000000000); } ``` The function's `with` row tells the test exactly which outside authority has to be supplied — there is nothing to discover by reading the body. A named context can supply an ordinary application environment with one binding overridden for the test; see [Capabilities](/docs/v0.4/reference/capabilities/#override-named-context-entries). `sleep` is one of the clock's operations, so that four-line handler is also how a test drives a retry loop or a poller without waiting for it. There is no test runtime and no special mode. ## Failure and cancellation paths Khora's strongest guarantees are about control flow that does not return normally, so a test for a resource, a transaction or concurrent work should cover successful completion, typed failure, cancellation where it applies, and the cleanup or rollback each of those paths owes. [Cancellation-safe resources](/docs/v0.4/cookbook/cancellation-safe-resources/) is that written out. ### A trap ends the whole run An `assert` that fails, or a `raise` that escapes, is a *failing test*: it is reported, the other tests still run, and the process exits 1. A **trap** is different — dividing by zero, indexing past the end, arithmetic overflow — and ends the process immediately, including the tests that had not run yet: ``` test a ... ok khora: Int division by zero on fiber 8 note: re-run with KHORA_BACKTRACE=1 to see where khora: this ended the test run. 3 test(s) had not been reported: `b traps`, `c`, `d`. khora: a trap ends the process, so the rest of the run did not happen. `khora test --filter ` runs one at a time. ``` Tests run concurrently, so how many verdicts appear before the trap varies between runs — a test whose result had not been printed yet is listed as not reported, even if it would have passed. What does not vary is that the run says it ended early and names what it did not reach. Trapping cannot be contained into a failing test: recovering would need an unwinder to run the reference-count operations between the trap and a landing point, and a fiber abandoned without them leaks everything it was holding. So a trap in a test is a bug to fix rather than a result to read, and `khora test --filter ` is how to work on one test while another traps. ## Write a benchmark `bench` takes the same named-block shape: ```khora bench "parse representative payload" { parse_payload(fixture)!; } ``` ```bash khora bench . khora bench . --filter payload ``` Keep setup that is not part of the measurement outside the operation being compared, where the benchmark's design allows it. ### Benchmarks build unoptimized unless told otherwise They build unoptimized like everything else the toolchain compiles, because a language being brought up should give a readable crash by default. There is no `--release` flag here; `khora test` and `khora bench` read `KHORA_PROFILE`, because a flag on every subcommand is three ways to say one thing: ```bash KHORA_PROFILE=release khora bench . ``` `khora bench` prints the profile it used above the results, so a number pasted into an issue carries that with it. A small integer loop differs by about a factor of two between the two profiles; code the optimizer can see through differs by much more. **And some workloads differ by nothing measurable.** Code whose time goes on allocation, reference counting or I/O spends it in the runtime, which is compiled once and is the same either way. A release build buying nothing is information about where the time is going, not a sign the profile did not take. [Performance](/docs/v0.4/performance/) is the methodology. ## In CI A useful baseline: ```bash khora fmt . --check khora check . khora test . ``` A project with performance budgets can run selected `khora bench` workloads separately from correctness CI — with `KHORA_PROFILE=release`, or the budget is measured against a build nobody ships. # Lints Source: https://khoralang.com/docs/v0.4/reference/lints/index.md `khora check` runs 21 lints alongside type checking. They are part of the compiler rather than a separate tool, so the editor underlines what the command line reports and there is no second configuration to keep in step. ## The lints | Name | Default | What it finds | | --- | --- | --- | | `bool-comparison` | `allow` | `b == true` or `b == false`, which are `b` and `!b`. In the `idiomatic` group. | | `concatenated-string` | `allow` | `"a " + x + "!"`, which is `"a ${x}!"`. In the `idiomatic` group. | | `dangling-expression` | `warn` | A statement that computes something and does nothing with it. | | `discarded-result` | `warn` | A statement that produces a `Result` and drops it on the floor. | | `inconsistent-constructor` | `warn` | A constructor whose name disagrees with what it takes — `new`, `empty`, `root` and `of` follow a rule `std` keeps. | | `method-call` | `allow` | `x.m(a)`, which is `T::m(x, a)`. In the `idiomatic` group. | | `misplaced-main` | `warn` | A `main` in a file that is not an entry point. | | `module-path` | `allow` | `module main;` in a package, where `khora new` writes `module ::main;`. In the `idiomatic` group. | | `needless-return` | `allow` | `return e;` as a function's last statement, where the tail `e` says it. In the `idiomatic` group. | | `nested-verdict` | `warn` | A `Result` handed to something that decides on the outer tag alone, so the inner one's failure passes as success. | | `parenthesized-parameter` | `allow` | `fn (x) =>` with one untyped parameter, which is `fn x =>`. In the `idiomatic` group. | | `reference-cycle` | `warn` | A cycle that reference counting cannot collect. | | `subtraction-from-zero` | `allow` | `0 - 1` on `Int` literals, which is `-1`. In the `idiomatic` group. | | `undocumented-export` | `allow` | A `pub` item nobody described in one line. | | `unknown-allow` | `warn` | A `// @klint allow` naming something that is not a lint. | | `unlabeled-flag` | `allow` | A `true` or `false` passed without a label to a parameter declared `Bool` that is not the first. In the `idiomatic` group. | | `unreachable-code` | `warn` | A statement that cannot run, because the one before it left the block. | | `unused-binding` | `warn` | A binding nothing reads — locals, parameters, and the names a pattern binds. | | `unused-capability` | `warn` | A capability a signature asks for that its body cannot be using. | | `unused-import` | `warn` | An imported name the file never mentions. | | `useless-allow` | `allow` | A `// @klint allow` that suppressed nothing. | ## Levels ```toml [lints] unused-import = "deny" undocumented-export = "warn" unreachable-code = "allow" ``` | Level | Effect | | --- | --- | | `allow` | Not reported. | | `warn` | Reported; `khora check` still succeeds. | | `deny` | Reported as an error; `khora check` and `khora build` fail. | The table form carries an option alongside the level, for a lint that takes one: ```toml [lints] some-lint = { level = "warn", max = 15 } ``` No shipped lint takes an option yet. The manifest accepts the form so that adding one does not need a manifest change. In a workspace, `[workspace.lints]` sets the defaults a member inherits with `lints.workspace = true`. Naming a lint that does not exist is a warning, and the message lists the ones that do — a typo would otherwise configure nothing and say nothing, which is the worst direction for a setting to fail in, because a setting you have written down is one you have stopped thinking about. It is a warning rather than an error because a manifest may be older or newer than the toolchain reading it. ## Groups A group is a named set of lints, switched on together: ```toml [lints.idiomatic] ``` A group's table present means the group is on, and **each lint in it runs at the level the group gives that lint**, which can differ from lint to lint. The group's table absent means the group is off, and its lints are at their own defaults from the table above. `level` applies one level to every lint in the group, and is optional: ```toml [lints.idiomatic] level = "deny" # allow | warn | deny ``` `level = "allow"` switches the group's lints off explicitly, each of them, including one another enabled group also holds (see below). ### Which level wins Most specific first: 1. a `// @klint allow` on the line; 2. the lint's own entry in `[lints]`; 3. an enabled group's `level`; 4. the level an enabled group gives the lint; 5. the lint's default. The order of tables in the file never matters. A group that turns `unused-binding` up to `deny` is overridden by `unused-binding = "warn"` under `[lints]`, above or below the group's table. Steps 3 and 4 each look at every enabled group that holds the lint. If any of them writes `level`, step 3 decides: the written levels settle the lint, and the other groups' own levels for it are not consulted. Only when none of them writes `level` does step 4 decide, from the levels the groups give the lint. Two groups conflict only when they disagree at the step that decides: two written `level`s that differ, or, with no `level` written, two groups that give the lint different levels. A conflict is an error naming both groups and the lint, and one line under `[lints]` settles it. `level = "allow"` sets every lint in that group to `allow`, including a lint another enabled group holds. With these two groups: ```toml # lints/a.toml holds unused-binding = "deny" # lints/b.toml holds unused-binding = "warn" [lint-groups] a = "lints/a.toml" b = "lints/b.toml" [lints.a] [lints.b] level = "allow" ``` `unused-binding` is `allow`, with no error: `b`'s written level decides at step 3, ahead of `a`'s `deny` at step 4. To keep `a`'s `deny`, write it for the lint itself: ```toml [lints] unused-binding = "deny" ``` ### What a group's table refuses Each of these stops `khora check` and `khora build`, with a message naming the manifest and the key: | Written | Why | | --- | --- | | `idiomatic = "warn"` | A group is always a table. The message shows the table form. | | `enabled = true` | Writing the table switches the group on; `level = "allow"` switches it off. | | `exclude`, `rules`, `extends` | Reserved for a later version. | | any other key | A group's table takes `level` and nothing else. | | `"acme::strict"` | Groups published in packages are not yet supported. | A lint's own table still needs its `level`: `[lints.unused-binding]` with nothing under it is an error. ### The group file A group is a TOML file: ```toml [group] name = "strict" description = "What this project holds itself to." [group.lints] unused-binding = "deny" unused-import = "warn" undocumented-export = "warn" ``` `[group.lints]` lists each lint in the group with the level it runs at when the group is on. `name` must match the name the group is used by. There is nothing else in the file: a group combines lints and never defines one. The toolchain ships its groups as files of this kind in `lints/` beside the standard library. A project adds its own under [`[lint-groups]`](/docs/v0.4/reference/manifest/#lint-groups--this-projects-own-lint-groups) in its manifest: ```toml [lint-groups] strict = "lints/strict.toml" ``` Both kinds are used the same way. The rules for both: - every entry in `[group.lints]` must be a lint from the table above; a group inside a group is refused; - a project's group cannot take the name of a lint or of a group the toolchain ships; - a file that is missing or does not parse is an error naming the file, never an empty group. ### The groups that ship | Group | What it holds | | --- | --- | | `idiomatic` | One way to write Khora: `bool-comparison`, `concatenated-string`, `method-call`, `module-path`, `needless-return`, `parenthesized-parameter`, `subtraction-from-zero` and `unlabeled-flag`, each at `warn`. | None of the `idiomatic` lints finds a mistake. Each finds correct code written in a second form where Khora has a first one, so each is `allow` until the group is switched on. `level = "deny"` makes the first form the only one a build accepts, and `khora check --fix` rewrites the rest, except `unlabeled-flag`, which is reported with no fix: which label to write is the reader's call. A `// @klint allow` names one lint. Naming a group there is reported by `unknown-allow`, which says it is a group and lists its lints. ## The lints that are off Two lints are off by default for reasons of their own, below. The eight `idiomatic` lints are off too, until the group is switched on; see [Groups](#groups). `unlabeled-flag` is one of them, and why it is off is below as well. **`undocumented-export`** is off for the reason Rust's `missing_docs` is: a young package gets forty warnings on its first build, and the answer to forty warnings is not forty doc comments. Switch it on when a package decides its surface is a promise. This repository sets it to `deny`, because `khora doc` regenerates the reference from `///` comments and the gate fails on a stale page — so a *documented* export cannot drift, and nothing else checked that an export was documented at all. **`useless-allow`** is off because it fires on exactly the lines somebody is already editing to satisfy a new lint, so turning it on while lints are still being added produces churn in the files under the most pressure. Turn it on once they have settled; a stale suppression hides the next finding on that line. **`unlabeled-flag`** is off because the call it reports is correct. `reply(connection, "ok", false)` compiles, and a reader cannot tell what `false` switches off without opening `reply`; the lint asks for `reply(connection, "ok", keep_alive: false)`, a [labeled argument](/docs/v0.4/reference/expressions/#labeled-arguments) the compiler checks against the declaration. It reports only a parameter *declared* `Bool` that is not the first: `fold(true, step)` passes a value of a type variable, not a switch, and `assert_that(false, "unreachable")` is about its first argument, which a label would not explain. A call through a function value is never reported, because a function type has no parameter names to write. Switch it on with the `idiomatic` group or on its own. ## Suppressing one line ```khora // @klint allow unused-binding let width = measure(shape); ``` The pragma applies to the line that follows it. `unknown-allow` is what makes it safe to have: a misspelled lint name in a comment would otherwise suppress nothing and say nothing, and the reader would believe the line was handled. Prefer the naming escape where one exists. A binding whose name starts with `_` is deliberately unused, and `_` alone binds nothing — both are quieter than a pragma and neither goes stale. **The receiver of a trait method is the exception.** `self` is part of the signature: it cannot be deleted, and renaming it to `_self` is refused with ``error: the receiver of `tag` is `?` here, but `Codec` declares `A``. A trait method that ignores its receiver — common in a witness trait — takes the pragma: ```khora impl Codec for IntCodec { // @klint allow unused-binding fn encode(self, value: Int) -> String { Int::to_string(value) } } ``` ## Fixing what they find ```sh khora check --fix ``` rewrites every finding that carries a fix, in place, then checks again. It prints each file it changed and how many fixes it made, or `nothing to fix`. It fixes exactly what `khora check` reports: only lints at `warn` or `deny` under the manifest, never a line a `// @klint allow` covers, and only the package's own files, never the standard library or a dependency. A file with a parse or type error is left alone. Two fixes where one sits inside the other are made one after the other, so `--fix` runs until nothing is left to fix. Before it writes anything, `--fix` checks the fixed program: a fix that would leave a file that does not parse, or an error anywhere in the package, is not made. It prints each one as `not fixed`, with the lint and the file it would have broken, and the finding stays for you to rewrite by hand. If any file in the package does not parse, nothing is fixed until it does: `--fix` says which file, and to fix the parse errors first. Each pass is written whole or not at all. The new text of every file goes to a temporary file beside it, and only when all of them are written are they moved into place, so a full disk or a file you may not write leaves every file as it was, and `--fix` says that nothing was written. A file with a second hard link is split: the name `--fix` wrote gets the new text, and the other name keeps the old. If `--fix` is killed while it writes, your sources are untouched, and a `..khora-fix-` file may be left beside one of them; it is not read as source, and can be deleted. The lints with a fix are the `idiomatic` group's. Each is made only where the rewritten program means the same thing, and where it would not, the finding is not made or is made without a fix: | Lint | Rewrite | Left alone | | --- | --- | --- | | `concatenated-string` | `"a " + x + "!"` becomes `"a ${x}!"`. A `$` that meets a `{` across a join is written `\$`, so `"$" + "{a}"` still prints `${a}`. | A chain across lines; a piece that is already interpolated; a backtick string; a character literal inside a piece; a piece holding an interpolated string; a comment inside the chain. Reported with no fix when a piece holds a call, or uses a local with no type written on it -- a `let` without `: T`, a binding in a `match`, `for` or `catch` pattern, a lambda parameter -- because `s + "!"` may be what makes `s` a `String`, and `todo() + "!"` what makes `todo()` one. | | `needless-return` | `return e;` in last place becomes `e`; a last `return;` is deleted. | A `return` inside a lambda or a nested block, which is an early exit. Reported with no fix when the statement before it has no `;`, when `e` starts with `{`, or when there is a comment in the statement. | | `subtraction-from-zero` | `0 - 1` becomes `-1`. | `Float`, because `0.0 - 0.0` is `+0.0` and `-0.0` is not; the fixed-width integers; `0 - x`, which differs from `-x` when `x` is the smallest `Int`. Reported with no fix on a line that starts right after a `}` with no `;`, because `-1` there subtracts from the value before it. | | `parenthesized-parameter` | `fn (x) =>` becomes `fn x =>`. | A typed parameter, which needs its brackets; more than one parameter. | | `bool-comparison` | `b == true` becomes `b`; `b == false` becomes `!b`, or `!(a < c)` for an operator. | `!=`. `b == true` is reported with no fix when `b` holds a call or uses a local with no type written on it, because the `==` may be what makes it a `Bool`; `b == false` is still fixed there, because `!b` says the same. Reported with no fix when the result would start with `(` on a line right after a `}` with no `;`, because `(c)` there calls the value before it. | | `module-path` | `module main;` becomes `module ::main;` in `src/main.kh`, and `module ::;` in `src/bin/.kh`. | Any other file is reported without a fix, because renaming a module breaks every file that imports it; so is an entry file that another file imports, such as a test file with `import main::{helper}`. | | `method-call` | `x.m(a, b)` becomes `T::m(x, a, b)`, where `T` is the type that declares `m`, or the trait for a trait's method (a type parameter's method is its bound's trait). Labels are kept, and a piped value keeps its slot: `v \|> x.m(a)` becomes `v \|> T::m(x, _, a)`. A chain `a.f().g()` nests, `G::g(F::f(a))`. Where the file does not have `T` in scope, an `import` of it is added. | A field holding a function, `r.f(x)`, which is not a method call. Reported with no fix where `T::m` in this file would reach something else -- a type parameter named `T`, another type named `T`, a constructor `T::m` -- or where importing `T` would clash with a name the file already has; where the receiver's type is not settled at the call; and where a comment sits between the receiver and the `(`. | The language server offers the same fixes as one code action, "Apply idiomatic fixes", of kind `source.fixAll.khora`. An editor runs it on save when asked for `source.fixAll`; it is not offered in the lightbulb menu. It fixes what `khora check --fix` would, one pass at a time. ## Where they run - `khora check` and `khora build`, against the manifest nearest the file. - The language server, at the same levels, so the editor and the command line agree. There is no `khora lint`. A separate command is a second thing to run and a second answer to disagree with the first. # Debugging a program Source: https://khoralang.com/docs/v0.4/reference/debugging/index.md Khora compiles to an ordinary native executable with ordinary debug information, so the tools you already have work on it. This page says which parts of that are tested and which are not, because "should work" and "is checked by CI" are different claims and only one of them is worth acting on. ## Backtraces This is the first thing to reach for and the part that is covered by tests. A trap — a checked overflow, an index outside its array, a division by zero — prints the Khora source location that caused it: ```text khora: Int addition overflowed 6: deep at .\main.kh:4 7: middle at .\main.kh:8 8: main at .\main.kh:13 ``` The frames below `main` belong to the C runtime that started the process and are not trimmed. Backtraces are off by default, because capturing one costs every well-behaved program a page of stack on the way out and the first thing anybody does with a bug is run it again. A trap without them says how to get them: ```bash KHORA_BACKTRACE=1 ./build/myapp ``` `RUST_BACKTRACE` is honored too, so a machine that already exports it for everything is not asked twice. **A failed assertion is not a trap.** `assert` names the line that failed and the run carries on to the end, because a suite that stopped at the first failure would report one problem per run. It prints no backtrace and the process does not end with status 134. [Traps](/docs/v0.4/reference/traps/) is the list of what does. **The runtime's own frames are not at the top.** The frames belonging to the backtrace machinery are trimmed, because the top of a backtrace is the part anybody reads first and six frames of library internals above the line that trapped make it useless. That trimming is tested. ## Debug information Debug builds emit DWARF line tables on Linux and macOS, and a PDB beside the executable on Windows. A release build emits neither. ```bash khora build . # debug information khora build . --release # none KHORA_DEBUG=1 khora build . --release # release code, debug information KHORA_DEBUG=0 khora build . # debug code, none ``` `KHORA_DEBUG` overrides the profile in both directions. It is part of the build cache key, so switching it does not hand you the other build's artifact. **On Windows, debug information costs reproducibility.** Relinking one unchanged object twice gives identical bytes without `-g` and different bytes with it — what varies is inside lld-link's PDB emission, and no linker flag available here fixes it. A build that is reproducible only without debug information is a real limit and it is named here rather than worked around. ## LLDB and GDB A Khora executable is a native executable with standard debug information, so a debugger loads it, breaks on a symbol, and shows source lines: ```bash khora build . lldb ./build/myapp (lldb) breakpoint set --file main.kh --line 12 (lldb) run ``` Linker symbols carry the module that defines them and a `kh$` prefix — `kh$myapp$main$handle` for `handle` in `myapp::main`. The debug information records the source name as well, so a debugger can show `handle`, but breaking by file and line avoids having to find out which one your debugger wants. **A backtrace prints the qualified form**, `myapp$main$handle`, because the frame is resolved from the symbol table rather than from the debug information: ``` khora: Int division by zero 2: btrace$main$deep at ./src/main.kh:5:45 3: btrace$main$middle at ./src/main.kh:6:33 ``` The numbering starts partway in, commonly at 2. The runtime's own frames are cut from the top of the capture — `Backtrace::force_capture` and the trap entry point sit above the line that trapped, and they are the same frames in every backtrace. The numbers are the ones the capture assigned, so the first one you see is whatever the trimmed frames left; nothing is missing below it. **What is not verified.** Nothing in CI drives a debugger, so stepping, frame inspection and variable display are not covered by any test. Line tables are emitted and the linker is told about them; whether every Khora construct produces a frame a debugger renders usefully is unmeasured. Treat this section as a starting point rather than a supported workflow, and prefer a backtrace or a `print` when you need an answer you can rely on. This is [release-gate item §9](https://github.com/codyspate/khoralang/blob/main/docs/release-readiness.md) and it is not ticked. ## Printing `print` is not a debugging tool with a bad reputation here; it is the tool with the best coverage. String interpolation calls `Show`, so any type that derives it can be printed whole: ```khora print("entry ${entry} after ${Int::to_string(applied)} steps"); ``` Deriving `Show` on a type you are chasing costs one line and survives the session. ## When a fiber is involved A trap inside a fiber names that fiber's stack, not the parent's. If a program hangs rather than traps, the usual cause is a fiber waiting on something that will not arrive — a channel nobody sends to, or a nursery whose child is blocked in a call with no cancellation point in it. [Concurrency](/docs/v0.4/reference/concurrency/) lists where a fiber can be stopped: a loop going round, a call to a function that can itself be stopped, and a blocking operation, in every function whatever its `raises` row. What does not stop it is a single foreign (C) call, a file-system call, a blocking connect or a wait for a child process already in progress, and a `Shared::update` change function, which runs to its end. ## Reporting what you find ```bash khora --version ``` prints the version, the commit it was built from and the target triple — `khora 0.1.0 (a416574) x86_64-pc-windows-msvc`. Include it. Two builds of the same version from either side of a fix are otherwise indistinguishable in a bug report, and a `-dirty` marker tells both of us that the tree had uncommitted changes. # Compatibility and stability Source: https://khoralang.com/docs/v0.4/reference/compatibility/index.md Khora is `0.x`. It may break. This page says when, how you find out, and what 1.0 is waiting for — because "pre-1.0, anything can change" is not a policy, it is the absence of one. ## What `0.x` promises **Within one release, everything.** A program that compiles with one `khora 0.4.0` build compiles with every `0.4.0` build, and the lockfile resolves the same way. Pin the toolchain and a build is reproducible. ```toml [toolchain] version = "0.4.0" ``` A pin that cannot be satisfied fails loudly. It never silently runs a different compiler — the toolchain shim hands over before argument parsing, and `khora toolchain which` tells you which build answered and why. **Between releases, nothing is guaranteed** — but nothing breaks silently either: - Every breaking change is in the [changelog](https://github.com/codyspate/khoralang/blob/main/CHANGELOG.md), under a **Breaking** heading, before anything else. - A change that made a program *silently wrong* is listed under Breaking as well as Fixed, because code written around the old behavior will behave differently now. - Where a mechanical fix exists, the entry names it. ## What counts as breaking | Surface | Breaking? | | --- | --- | | Language syntax and semantics | Yes | | Making a `pub` field of a `pub` type private, or closing an open wrapper (`= pub T` to `= T`) | Yes | | Adding a private field to a type that already has one | No: nothing outside the module could build it before either | | Adding any field to a type whose fields are all `pub` | Yes: literals outside the module stop compiling | | A `std` signature, type or behavior | Yes | | A `std` item's removal or rename | Yes | | Renaming a parameter of a public function, in `std` or a package | Yes. A caller may have [labeled the argument](/docs/v0.4/reference/expressions/#labeled-arguments) with the name | | Lockfile format | Yes | | Manifest keys the toolchain requires | Yes | | CLI flags and their meanings | Yes | | Diagnostic *wording* | No | | A new lint, or a lint's default level | No | | Compiler internals, IR, symbol names | No | | Which fiber implementation is the default | **Yes, today.** It is meant to be unobservable and is not — see below | | Anything under `docs/` in the repository | No | A new lint can make `khora check` report something it did not report before. That is deliberate and is not treated as breaking: a lint tells you about code that was already wrong. Set its level in `[lints]` if you disagree. ### The fiber backend is observable The two backends answer cancellation alike: a fiber in `clock.sleep`, on a channel, on a socket or on another fiber is woken by a cancellation under either one. They schedule differently — which fiber runs when, and how many run at once — so a program that depends on an order the language does not promise can see which backend it has. So the backend is not an implementation detail, and **the default cannot change without a breaking-change note.** [Known limitations](/docs/v0.4/limitations/#the-two-fiber-backends-are-distinguishable) has the detail. ## Editions There is no editions mechanism, and there will not be one until something needs it. An edition is a promise to maintain two languages at once, and a `0.x` with three release candidates has no evidence that it is the right shape of promise. `khora.toml` has no `edition` key. The question it looks like it would answer — *which Khora builds this?* — is answered by [the `toolchain` table](/docs/v0.4/reference/manifest/#toolchain--which-khora-builds-this), which names a version that exists and selects the binary that runs. ## What 1.0 is waiting for 1.0 means the surfaces in the table above stop changing without a major version. It is waiting on four things, none of which is a feature: 1. **A bug-discovery rate that has flattened.** The honest signal is not a feature list; it is how often a session aimed at known issues turns up something new. Recent sessions have still produced silent-wrongness bugs in trait dispatch and structured concurrency. Until that stops, a stability promise would be a promise to keep bugs. 2. **The formal soundness review finished.** All 282 `unsafe` blocks now name the invariant that makes them sound, and a gate step keeps it that way. What is missing is the other half: which *test* protects each invariant. The load-bearing ones say so; most do not. [The soundness design note](https://github.com/codyspate/khoralang/blob/main/docs/design/soundness.md) is where that lives. 3. **The scheduler measured on Linux.** Fibers are OS threads by default and the M:N scheduler is opt-in; that choice is settled for 0.3.0 and written up in [the fibers design note](https://github.com/codyspate/khoralang/blob/main/docs/design/fibers.md). What is not settled is whether it should stay that way, and the missing evidence is the density claim on Linux — the reason the scheduler exists. The I/O backends underneath it are the other half of that question, and Linux now has `epoll` -- a backend rather than a rewrite, with a `poll` fallback where the kernel will not open one. macOS wants `kqueue`, which is the same shape; Windows wants IOCP, which is not a backend swap because it is completion-based. 4. **Use by people who did not write it.** Nothing else substitutes for it, and it has not happened yet. The [known limitations](/docs/v0.4/limitations/) page tracks the shorter-term version of the same list. ## How language changes are decided In the open, in an issue, before the code. Changes that alter what a program means are recorded in the roadmap or in a [design note](https://github.com/codyspate/khoralang/tree/main/docs/design), with the argument rather than only the conclusion. [`CONTRIBUTING.md`](https://github.com/codyspate/khoralang/blob/main/CONTRIBUTING.md) has the process. One maintainer has final say. That is stated plainly rather than dressed as a committee, so you can judge the project's bus factor for yourself. ## Reporting a break If an upgrade breaks a program and the changelog does not say it would, that is a bug in the changelog and worth reporting on its own. Include `khora --version` — it carries the commit and the target triple, which is usually enough to find the change that did it. # Environment variables Source: https://khoralang.com/docs/v0.4/reference/environment/index.md Khora reads a handful of `KHORA_*` variables. This page is the complete list of the ones you may set: what each does, what it does when unset, what values it accepts, and what it costs where that is not obvious. **A variable not on this page is not a knob.** The compiler and the runtime read a few more names — test scaffolding, and one or two things that reach a program by accident rather than by design. They are not listed here because setting them is not supported and they may change or disappear without notice. A check in the repository fails when a name read by shipped code is neither documented here nor recorded internally, so this page cannot quietly fall behind the source. Every variable here is read once, when the thing that reads it starts. Changing one mid-run changes nothing. ## Where Khora keeps things ### `KHORA_HOME` The one directory Khora owns on the machine. Everything that persists between builds lives under it: | Under `KHORA_HOME` | What it holds | | --- | --- | | `toolchains/` | Installed compilers, one directory per version | | `bin/` | The `khora` the installer puts on your `PATH` | | `cache/` | Build artifacts, keyed on the inputs that produced them | | `store/` | Extracted package sources | **Default:** `~/.khora` — `$HOME/.khora`, or `%USERPROFILE%\.khora` on Windows. **Value:** an absolute path. It is used as given, and directories under it are created as needed; it does not have to exist beforehand. ```bash export KHORA_HOME=/data/khora khora build . ``` Moving it is an ordinary thing to do. The build cache is the part that grows — see `KHORA_CACHE_BUDGET` below — and a machine whose home directory is on a small root disk will want `KHORA_HOME` pointed somewhere with room. Everything above moves together: a toolchain installed under one `KHORA_HOME` is invisible under another, and so is every cache entry, so the first build after a move rebuilds from scratch. **What it costs.** Setting it per-shell rather than per-machine is the way to get two of everything. `khora` resolves it at the moment it runs, so a build in a shell that exports it and a build in a shell that does not are using two different caches and possibly two different compilers. Khora needs to know its home before it can do anything. If neither `KHORA_HOME` nor a home directory is set, it says so and stops rather than guessing. ### `KHORA_STD` Where the standard library's sources are. **Default:** unset, and the compiler looks beside its own executable and one directory up. A toolchain unpacked from a release archive, and a compiler built in its own source tree, are both found this way. **Value:** a path to a directory. It is ignored if no directory is there, so a stale setting falls back to the search rather than failing. Set it only for a layout the search does not cover — a standard library relocated away from the binary. It is not a way to substitute a modified `std`: the compiler is entitled to assume `std` matches the version it was built from. ### `KHORA_RT_LIB` The runtime archive that generated executables are linked against. **Default:** unset, and the same search as `KHORA_STD`: beside the executable, one directory up, then the compiler's own build tree. **Value:** a path to the archive file itself, not the directory holding it. Ignored if no file is there. The same caveat applies, and more sharply: the runtime archive and the compiler agree about object layout, so an archive from a different version produces a program that links and then misbehaves. ## Choosing what gets built ### `KHORA_PROFILE` Which build profile to use. **Default:** `debug`. **Values:** `release` selects the optimized, reproducible profile. Anything else — including a typo — is `debug`. It is read in the middle of a build where there is nowhere to report an error, and a misspelling that silently optimized would be worse than one that silently did not. ```bash KHORA_PROFILE=release khora test . KHORA_PROFILE=release khora bench . ``` `khora build . --release` is the flag for the same thing and is what a build should use. The variable exists because `khora test` and `khora bench` have profiles too and a flag on every subcommand is three ways to say one thing. ### `KHORA_DEBUG` Whether the build emits debug information, overriding what the profile would decide. **Default:** unset — debug builds emit it, release builds do not. **Values:** `1`, `on` or `true` to emit it; `0`, `off` or `false` to suppress it. Anything else is treated as unset. ```bash KHORA_DEBUG=1 KHORA_PROFILE=release khora build . # optimized, with line tables KHORA_DEBUG=0 khora build . # unoptimized, without ``` It overrides in both directions on purpose: profiling wants an optimized build it can attribute to source lines. It is part of the build cache key, so switching it does not hand you the other build's artifact. [Debugging a program](/docs/v0.4/reference/debugging/) has what the debug information is good for and what it is not. ### `KHORA_TARGET` What to generate code for, instead of the machine you are on. **Default:** unset — the host. **Values:** one of `linux`, `macos`, `windows`, `wasm`, or a full target triple such as `aarch64-unknown-linux-gnu`. A triple selects its own family, so one setting moves both halves of a cross build: which platform's `std` files are read, and which target the code generator emits for. Any other value is rejected with a message naming what is accepted. **What it costs, and it is the whole story here.** This does not produce a runnable program for another platform. There is no cross linking: the build stops at generating and verifying a module. What it is for is checking that a combination *compiles* — a `std` surface that only one platform selects, a calling convention only one platform uses — from whichever machine you have. Running the result still needs the real platform. [Supported targets](/docs/v0.4/deployment/supported-targets/) is what is actually shipped. ### `KHORA_RC_LOCAL` Count references to objects that only one fiber can reach without a locked instruction. **Default:** on. Every reference count in a program that starts a fiber is, unless the object is shared, an ordinary load and store rather than a locked read-modify-write. **Value:** `0` turns it off, so every count is locked whether or not the object is shared. Anything else, including unset, leaves it on. Read when a program is compiled, and part of the build cache's key, so a cached build made the other way is not reused. A program that never calls `Fiber::spawn` counts every reference with plain arithmetic whatever this says. For one that does, an object is local to the fiber that made it until the runtime hands it to another fiber — a spawn's captures, a channel send, a `Shared` cell, a fiber's answer or error — and marks it shared. With the default, a local object's count is an ordinary load and store and a shared object's is locked. On a handler that does real work, the locked instructions are a large part of the CPU a request costs; against every count locked, this was measured never slower and up to 8% faster on a realistic web workload. Turn it off to compare against every count locked, or to rule this out while chasing a bug that might be a lost update on a shared reference count. **What it does not change.** Two programs the compiler accepts can reach a value with a mutable field from two fibers: a record captured by `Fiber::spawn` through a variable whose type is only settled after the spawn, and a record with a mutable field raised as a fiber's error and joined by two fibers. With every count locked, a write to that field from one fiber while another reads it is a race on the field. With the default, it is also a race on the reference counts of what the field holds, which can crash the program or free memory still in use. A debug build stops such a program the first time the second fiber counts the value, with `object made on fiber N was counted on fiber M without being shared`, whether or not this is set. ### `KHORA_UNBOXED` — not a knob It changes how values are laid out across the whole program and is not a supported setting. [Variables that are not knobs](#variables-that-are-not-knobs) says why it is reachable at all. ## Dependencies ### `KHORA_LOCKED` Refuse to change the lockfile — resolve exactly what it pins, and fail rather than update it. **Default:** unset, and a resolve may update the lockfile. **Values:** any value, including the empty string. This is a present-or-absent switch; `KHORA_LOCKED=0` turns it **on**, because what is read is whether the name is set at all. The `--locked` flag does the same thing for one command. Setting the variable is for CI, where every command in the job should behave that way and adding a flag to each is how one gets forgotten. ### `KHORA_RELEASE_REPO` Where `khora toolchain install` looks for releases. **Default:** `codyspate/khoralang`. **Value:** a GitHub `owner/repository`. For an organization publishing its own toolchain builds. Checksums are verified against the release regardless of where it came from. ## The build cache ### `KHORA_CACHE_BUDGET` How large the build cache may grow before the least recently used entries are evicted. **Default:** 2 GiB (`2147483648`). **Value:** a size in bytes, as a plain integer. A value that is not an integer is ignored and the default stands. The default is chosen against the disk rather than the workload: small enough that a build loop left running cannot fill a modest disk, large enough to hold many builds of a large project. Raise it on a machine with room and several large projects; lower it on a small disk. A cache below the size of one build's output evicts on every build and costs you the cache entirely. ### `KHORA_CACHE_EXPLAIN` Print the cache key and why a lookup missed. **Default:** unset — a build says whether it reused an artifact, not why it did not. **Values:** any value except `0` turns it on. `KHORA_CACHE_EXPLAIN=0` is off. ```bash KHORA_CACHE_EXPLAIN=1 khora build . ``` Reach for it when a build rebuilds something you expected it to reuse. The interesting answer is that the key *moved*: a tree that has not changed should produce the key it produced last time, so a key that moved anyway means an input moved that nobody meant to move, and this names it. ## Watching the compiler work ### `KHORA_TIMINGS` Print one line per compilation phase to standard error, and a total. **Default:** unset. **Values:** any value except `0` turns it on. It measures phases of the compiler, not of your program. There is no sampling profiler for Khora code; [Profiling your program](/docs/v0.4/performance/your-program/) is what that question has instead. ### `KHORA_EMIT_LLVM` Write the generated LLVM IR beside the executable. **Default:** unset. **Values:** any value, including the empty string — present or absent is what is read. The module is written as `.ll` *before* verification, so a module that fails to verify is still there to be read, which is exactly when it is wanted. A release build writes the optimized module as well, as `.opt.ll`, so the two can be diffed. This is for reporting a compiler bug. The IR is not an interface and its shape changes freely. ## The runtime, inside a compiled program These are read by the program Khora builds, not by `khora`. Setting them changes how *your* executable behaves. ### `KHORA_BACKTRACE` Print a backtrace when a trap kills the program. **Default:** unset — a trap prints the failure and the source location, and says to re-run with this set. **Values:** any value. `RUST_BACKTRACE` is honored identically, so a machine that already exports it for everything is not asked twice. ```bash KHORA_BACKTRACE=1 ./build/myapp ``` Off by default because capturing a backtrace costs every well-behaved program a page of stack on the way out, and the first thing anybody does with a bug is run it again. The frames are symbolized from the debug information the executable carries, so a build made without it gives addresses rather than names — still worth printing, since an address and the binary can be symbolized later. [Traps](/docs/v0.4/reference/traps/) is the list of what counts as one; a failed `assert` does not. ### `KHORA_FIBERS` Which fiber backend the program runs on. **Default:** unset — a fiber is an operating-system thread. **Values:** `scheduler` selects the M:N scheduler, stackful coroutines on a pool of workers. Any other value, including `threads`, is the default. ```bash KHORA_FIBERS=scheduler ./build/myapp ``` **This is a supported setting, and it is the one to think hardest about.** A program cannot tell which backend it is on — that is the design, and the operations behave the same — but the two are not equally exercised. Threads are the default because they are faster at the connection counts a service actually runs at, and because they are the better-traveled path. The scheduler's advantage is density: a suspended fiber costs roughly 4 KB against a thread's 33 KB, which matters when tens of thousands of fibers are waiting rather than working. **What it costs.** The density figure is measured on Windows. On Linux, `vm.max_map_count` and guard pages splitting mappings mean it has not been reproduced, so on the platform most deployments use, the reason to switch is not established. The scheduler is also the less-exercised path and therefore the likelier home of the next runtime bug. It is a real choice with real evidence on one side of it, not a flag to set for luck. [Concurrency](/docs/v0.4/reference/concurrency/) has what a fiber is either way. It is also useful the other direction: running a suspected runtime bug under both backends separates a bug in one of them from a bug in what they share. ### `KHORA_BLOCKING_THREADS` How many threads the pool that runs blocking work may hold. **Default:** twice the number of cores, and never fewer than four. **Value:** a positive integer. A value that is not an integer is ignored and the default stands. Blocking work — anything that would otherwise stall a worker — goes to this pool. The bound exists so the pool does not become one thread per fiber. When a program wants more concurrent blocking work than the pool has room for, the extra waits rather than growing the pool, and the runtime counts how often that happened; a count that climbs with load is what says this is the number to raise. The default is a starting point, not a measurement. ### `KHORA_SCHEDULER_REPORT` Print the scheduler's counters to standard error on an interval. **Default:** unset. **Value:** an interval in milliseconds, as a plain integer. A value that is not an integer is ignored, and the interval is at least 1 ms. ```bash KHORA_FIBERS=scheduler KHORA_SCHEDULER_REPORT=500 ./build/myapp ``` This is how counters get out of a program that does not end: a server runs until it is killed, so there is no moment to print them at. The difference between two lines is the interesting part. It does nothing without `KHORA_FIBERS=scheduler` — there is no scheduler to report on — and each line is a write to standard error, so a short interval on a busy program is itself load. Each line ends with `turns=[...]`, how many times each worker has run a fiber so far. The differences between two lines say how the work was shared out over that interval. ### `KHORA_WAKE_LOCAL` Where a fiber goes when another fiber wakes it, under `KHORA_FIBERS=scheduler`. **Default:** on. **Value:** `0` turns it off. Any other value, or none, leaves it on. ```bash KHORA_FIBERS=scheduler KHORA_WAKE_LOCAL=0 ./build/myapp ``` A fiber woken by another fiber — a channel receive released by a send, a pooled connection handed back — goes to the queue of the worker the waking fiber is running on, and that worker runs it as soon as the waking fiber waits. Nothing else is signaled. A socket that a worker finds ready goes the same way: workers check their sockets between fibers, and whatever a worker finds ready it puts on its own queue and runs. A wake from anywhere else — a timer expiring, a thread that is not a fiber, or a socket found ready by the scheduler's own watcher thread, which checks only when no worker has checked for 2 ms, as when every worker is held by a long-running fiber — goes to the shared queue and wakes an idle worker. So does a local wake when the worker's queue already holds 512 fibers, so that no one worker's queue grows without limit from wakes. Idle workers take work from a busy worker's queue by stealing half of it. With the value `0`, every wake goes to the shared queue. That costs a signal to an idle worker for every handoff, which on a server whose requests pass through channels is most of its system time. It is there to rule the local path in or out of a problem, the way `KHORA_FIBERS=threads` rules the scheduler in or out. The value is read once, when the scheduler starts, and does nothing under the thread backend. ## Variables that are not knobs Two names reach a compiled program and are deliberately not documented above, because setting them is not supported: - **`KHORA_UNBOXED`** turns off flat layout for small values, across the whole program. Whether a value is laid out inline or behind a header is a compiler decision; it is reachable this way because separating a miscompile from the change that exposed it needed a switch, not because anybody should set it. - **`KHORA_NO_SIGNALS`** stops the signal watcher from installing. A program running with it set does not shut down gracefully on `SIGTERM` or `SIGINT`, and nothing reports that it did not. Both are recorded as decisions still to be made rather than as settings. The rest of the `KHORA_*` names in the source are test and packaging scaffolding — they configure the repository's own soak tests and release builds, and do nothing in a shell. # Standard library Source: https://khoralang.com/docs/v0.4/stdlib/index.md The Khora standard library is written primarily in Khora and ships with the compiler toolchain. It provides the stable vocabulary and cross-package contracts that ordinary programs need without turning every database, telemetry backend, or platform integration into `std`. ## Major areas The current standard library includes APIs for: - strings and collections, in [`std::core`](/docs/v0.4/stdlib/api/core/); - JSON encoding and decoding, in [`std::json`](/docs/v0.4/stdlib/api/json/); - files, in [`std::fs`](/docs/v0.4/stdlib/api/fs/), and processes, in [`std::process`](/docs/v0.4/stdlib/api/process/) and [`std::process::shell`](/docs/v0.4/stdlib/api/process/shell/); - the environment, in [`std::env`](/docs/v0.4/stdlib/api/env/); - randomness, in [`std::random`](/docs/v0.4/stdlib/api/random/); - clocks and sleeping, in [`std::clock`](/docs/v0.4/stdlib/api/clock/); - describing and decoding untrusted input, in [`std::schema`](/docs/v0.4/stdlib/schema/); - typed configuration, in [`std::config`](/docs/v0.4/stdlib/api/config/); - retries and backoff schedules, in [`std::resilience`](/docs/v0.4/stdlib/api/resilience/); - civil dates and offsets, in [`std::time`](/docs/v0.4/stdlib/api/time/); - exact `Decimal` arithmetic, in [`std::decimal`](/docs/v0.4/stdlib/decimal/); - HTTP client/server building blocks, in [`std::net::http`](/docs/v0.4/stdlib/api/net/http/), over [`std::net::socket`](/docs/v0.4/stdlib/api/net/socket/); - TLS, in [`std::net::tls`](/docs/v0.4/stdlib/api/net/tls/); - structured concurrency and shared state, in [`std::core`](/docs/v0.4/stdlib/api/core/); - database capability and row/cell vocabulary, in [`std::db`](/docs/v0.4/stdlib/api/db/); - tracing context and W3C propagation vocabulary, in [`std::trace`](/docs/v0.4/stdlib/api/trace/); - structured logging and writing to standard error, in [`std::log`](/docs/v0.4/stdlib/api/log/); - capability permissions, in [`std::permissions`](/docs/v0.4/stdlib/api/permissions/), over the grants in [`std::permissions::grants`](/docs/v0.4/stdlib/api/permissions/grants/) that `khora build` writes out of the manifest. Four of those are easy to miss: `std::clock` holds the `Clock` capability, and `Clock` has a `sleep` — an operation on the capability, so a fake clock is one line in a test. `std::config` reads settings out of the environment, types them, and reports *every* bad key in one pass rather than one restart per key. See [the cookbook recipe](/docs/v0.4/cookbook/configuration/). `std::resilience` holds `Schedule`, `retry`, `retry_while` and `repeat`. See [Retrying a flaky call](/docs/v0.4/cookbook/retrying/). `std::schema` describes the shape of a value once and decodes it from anywhere. It is the newest, and the one that changes how the others are meant to be used: a `Schema` reads the same settings from the environment, from a request body or from a test fixture, because the description does not know where the bytes came from. [Schemas](/docs/v0.4/stdlib/schema/) is the model; [Decode untrusted input](/docs/v0.4/cookbook/decoding-input/) is a program that runs. ## Choosing a collection `List` is a linked sequence, for ordered transformation and iteration. `Vector` is a growable contiguous sequence, for accumulation and indexing. `Map` is a mutable hash table; `Dict` is a persistent ordered map. The language does not change with the choice: ordinary calls, methods, pipelines, lambdas, `for` and pattern matching work the same over all four. The operations themselves are in [`std::core`](/docs/v0.4/stdlib/api/core/). The mutable ones — `Vector` and `Map` — stay fiber-local. Where several fibers need one evolving value, that is a `Shared` boundary or a capability, not a container passed around; [Sharing](/docs/v0.4/reference/sharing/) says why wrapping the type name in a shareable record does not make it safe. ## What belongs in `std` Khora keeps shared contracts in the standard library and leaves fast-moving implementations in packages. For example, `Db`, transaction semantics, and database cell types belong in `std`; PostgreSQL, SQLite, and Cloudflare D1 drivers do not. Trace vocabulary and propagation belong in `std`; vendor exporters do not. The same principle keeps the IANA time-zone database out of the compatibility promise while retaining stable date/time/offset types. **`std::ai` is not part of `std`.** Model inference is the `ai` package, published alongside `postgres` and `otlp`. What it offers is the effect a caller names when it wants a model, so that the provider is the caller's choice. Name the package as a dependency and import from it. ## Generated API reference This page is the curated entry point; the **API reference** pages beneath it are generated by `khora doc` from the `///` and `//!` comments in `std/` at the revision being documented. Every exported type, trait, effect, function and constant is there, with its signature and whatever its author wrote about it. They are checked rather than trusted. `khora doc --check` runs in the project baseline and fails if the checked-in pages no longer match the source, so a page cannot quietly drift from the declaration it describes. Two limits worth knowing: - **Almost no example is compiled.** A CI job compiles the fenced blocks that declare their own `module` line, and across the whole of `stdlib/api` there is exactly one of those out of about 1,100 code blocks. The rest — signatures and fragments — are prose, and can be wrong without anything noticing. - **Type names inside a rendered signature are not links.** A signature mentioning `Decimal` names it but does not link to it. Prose *around* a signature does link: `khora doc` renders the `` [`Name`] `` form as a working anchor, and there are about a hundred of those, plus an "API at a glance" index at the top of every page. Use the [Language Reference](/docs/v0.4/reference/) for the language, this section for the library, and the generated pages beneath it for exact declarations. # Schemas Source: https://khoralang.com/docs/v0.4/stdlib/schema/index.md `std::schema` describes the shape of a value once, and decodes it from anywhere. The description does not know where the bytes came from, which is the whole of the library and the reason it is worth a page. Everything else in this section is reached through the generated [`std::schema` API](/docs/v0.4/stdlib/api/schema/). This page is the model behind it. ## The separation it is built on A reader per source reads well, and none of it is reusable. A reader that both names a field and fetches it says why: ```khora pub fn string(name: String) -> Validated with { env: Env } ``` `string(name)` is not *this field is text*. It is *go to the environment, fetch this variable, and give me the text or a reason* — the shape and the reading are one function. A JSON body then needs its own vocabulary, and so does a CLI argument, and so does a database row, and none of them can share a description with the others. A `Schema` splits that in half. It describes an `A`; a [`Raw`] is whatever some source produced; `decode` is one function over the pair. ```khora let decoded = Schema::decode(schema(), incoming); ``` There is no `!` on that call: `decode` answers a `Validated` rather than raising, which is what lets it report every bad field at once. [What a failure is](#what-a-failure-is) below is how you read one. Where `incoming` came from is the caller's business. The same `Schema` reads the environment, a request body and a test fixture. ## A schema is two halves in one record ```khora pub type Schema = { shape: Shape, read: (List, Raw) -> Validated, }; ``` Both halves are load-bearing, and neither would do on its own. The **closure** is what makes `schema.decode(value)` an ordinary call, and what lets schemas be combined — `optional(list(int()))` is three closures wrapped around each other. The **shape** is untyped, deliberately: it has no type parameter, which is what lets a record's fields — all of different types — sit in one `List` and be walked. That is how a deployment can ask which keys a configuration needs without starting the program: ```khora print("it needs: ${Shape::keys(schema().shape)}"); ``` A record of two closures would have lost the shape. A bare description tree would have lost the decoder. [the schema design note](https://github.com/codyspate/khoralang/blob/main/docs/design/schema.md) has the argument in full. ## The vocabulary Six primitives, the combinators that wrap another schema, and the assemblers. Every constructor is named after the type it answers. | | | | --- | --- | | [`string()`](/docs/v0.4/stdlib/api/schema/#string) | a `String`; a number is not text, and is refused | | [`int()`](/docs/v0.4/stdlib/api/schema/#int), [`float()`](/docs/v0.4/stdlib/api/schema/#float) | an `Int` or a `Float`, parsed from the token | | [`decimal()`](/docs/v0.4/stdlib/api/schema/#decimal) | a `Decimal`, parsed from the token, or from text, because money travels as a string on most wires | | [`bool()`](/docs/v0.4/stdlib/api/schema/#bool) | a `Bool` | | [`any()`](/docs/v0.4/stdlib/api/schema/#any) | the `Raw` as it arrived, for a field whose shape is somebody else's business | | [`optional(s)`](/docs/v0.4/stdlib/api/schema/#optional) | an `Option`; absent or `null` is `None`, present and wrong is still an error | | [`nullable(s)`](/docs/v0.4/stdlib/api/schema/#nullable) | an `Option` that must be present and may be `null` | | [`default(s, value)`](/docs/v0.4/stdlib/api/schema/#default) | an `A`, with `value` when the field is absent; `null` is still an error | | [`list(s)`](/docs/v0.4/stdlib/api/schema/#list) | a `List`, indexing each element into the error path | | [`dict(s)`](/docs/v0.4/stdlib/api/schema/#dict) | a `Dict`, for a record whose keys are data | | [`refine(s, must, holds)`](/docs/v0.4/stdlib/api/schema/#refine) | the same `A`, rejected unless `holds`; `must` is the rest of the sentence after `must be` — `"between 1 and 65535"` reads as *port must be between 1 and 65535* | | [`between`](/docs/v0.4/stdlib/api/schema/#between), [`at_least`](/docs/v0.4/stdlib/api/schema/#at_least), [`at_most`](/docs/v0.4/stdlib/api/schema/#at_most), [`min_length`](/docs/v0.4/stdlib/api/schema/#min_length), [`max_length`](/docs/v0.4/stdlib/api/schema/#max_length), [`min_items`](/docs/v0.4/stdlib/api/schema/#min_items), [`max_items`](/docs/v0.4/stdlib/api/schema/#max_items), [`non_empty`](/docs/v0.4/stdlib/api/schema/#non_empty), [`one_of`](/docs/v0.4/stdlib/api/schema/#one_of) | the same, with a rule that carries its bounds, so a rendered document can carry them too | | [`secret(s)`](/docs/v0.4/stdlib/api/schema/#secret) | a `Redacted`, and a failure inside it does not quote what it saw | | [`key(wire, s)`](/docs/v0.4/stdlib/api/schema/#key) | the same `A`, read under a key that is not the field's name | | [`renamed(s, wire, field)`](/docs/v0.4/stdlib/api/schema/#renamed) | a record schema with one key spelled differently on the wire | | [`Schema::map`](/docs/v0.4/stdlib/api/schema/#map), [`Schema::try_map`](/docs/v0.4/stdlib/api/schema/#try_map) | something made from the value, or something it may fail to become | | [`Schema::closed`](/docs/v0.4/stdlib/api/schema/#closed) | the same record, refusing a key it did not declare | | [`Schema::described`](/docs/v0.4/stdlib/api/schema/#described) | the same schema, with a sentence for a document to carry | | [`Schema::cases`](/docs/v0.4/stdlib/api/schema/#cases) | a variant: a bare string for a payload-free case, an object tagged with `type` for the rest | | [`Schema::lazy`](/docs/v0.4/stdlib/api/schema/#lazy) | a schema built when first read, so a type may mention itself | | [`struct({ .. })`](/docs/v0.4/stdlib/api/schema/#struct) | a record: a literal whose fields are schemas, typed by the compiler as the record they decode | | [`Schema::record`](/docs/v0.4/stdlib/api/schema/#record) over [`Fields`](/docs/v0.4/stdlib/api/schema/#fields) | the same, assembled by hand, for what neither `struct` nor a derive can say | They compose in the obvious way, which is most of the point: ```khora between(int(), 1, 65535) ``` ## Reaching a schema through the type A type that can be read from untrusted input implements `Decode`, whose one function is `schema() -> Schema`. `std` implements it for its own types — `String`, `Int`, `Float`, `Bool`, `Decimal`, `Date`, `Time`, `DateTime`, `Option`, `List`, `Vector`, `Dict`, `Map`, `Redacted` (as `secret`), `Json` and `Raw` — and a program implements it for a record or a variant with the constructors above: ```khora impl Decode for Listen { fn schema() -> Schema { struct({ host: string(), port: between(int(), 1, 65535) }) } } ``` Every schema that contains a `Listen` then reaches this one through the trait, which is the whole override story: write the impl for the type that needs something derivation cannot know, and composition does the rest. The schema is reached by name, `Listen::schema()`, or by the type the surrounding expression asks for: ```khora let settings: Validated = decode(raw); ``` `Encode` is the other direction, `encode(self) -> Raw`, and `Raw::to_json` is the bridge out. It is a separate trait rather than a second half of the schema because a secret has no representation on the wire: `Redacted` implements `Decode` and not `Encode`, so a record holding one reads and does not write, and the build says so. `Rejection` implements `Encode`, so a list of problems is a response body a client can read: one object per problem, with its `path` and its `message`. **Two routes to text, and they differ in key order.** `json_text(value)` -- what `Response::json` sends -- writes a derived record straight to text, its keys in the order the type declares them, so a `{ zebra, apple, mango }` record encodes as `{"zebra":..,"apple":..,"mango":..}`. An `Option` field holding `None` is left out. `encode(Raw::to_json(value.encode()))` builds a `Json` value on the way, and a `Json::Object` holds a `Map`, so every object it prints is sorted by key. A `Map` encodes sorted by key on both routes, because a hash table has no order of its own that stays the same from one run to the next. `Encode` has a second method, `encode_json(self) -> String`, which is the fast route. It has a default that goes through `encode`, so a hand-written impl writes only `encode`; `derive(Encode)` writes both. An impl that writes its own `encode_json` must produce the same value as its `encode`, with an empty string for a value that is absent. ## Where a `Raw` comes from `Raw::of_json` turns a parsed document into one, and `Raw::to_json` turns one back, which is the one bridge in each direction. `Raw::of_map` takes a query string, the path parameters or the headers; `Raw::of_arguments_for(shape, args)` takes the command line, so `--port 8080` is a field named `port`, `-c` is one named `c`, and whatever is not a flag is collected under `arguments`. It asks the shape which flags are switches: a field that is a `Bool` never takes the word after it, so `khq -c '.name' f.json` reads `c` as `true` and the query as the first argument. `Raw::of_arguments` is the same reading without a shape, for a program that has none in hand. A source that cannot label its values, which is every one of those but JSON, hands over `Raw::Untyped`, and every primitive reads it. A source that can label them is taken at its word: `"port": "8080"` in a JSON body is text, and `int()` refuses it with `port should be a whole number, and is "8080"`, the way serde and `encoding/json` refuse it. Strictness is a fact about the source, recorded by the source. [Decode untrusted input](/docs/v0.4/cookbook/decoding-input/) puts them together into a program that runs. ## What a failure is `decode` answers a [`Validated`](/docs/v0.4/stdlib/api/core/#validated), not a `Result`, so a record with four bad fields reports four. A person fixing a deployment wants the list, not one line of it per restart: ```text refused: listen.host is not set listen.port must be between 1 and 65535 password is not set rate should be an exact decimal, and is "about seven percent" ``` Each line is one [`Rejection`](/docs/v0.4/stdlib/api/schema/#rejection), and `Rejection::describe` is what wrote it. A rejection carries a path, so it can say `listen.port` or `items[3].id`; the path is held innermost-first and turned round only when a message is built. A caller with nothing to do with a list wants a `Result` instead: [`decode_or_stop`](/docs/v0.4/stdlib/api/schema/#decode_or_stop) answers `Result>`, which `!` carries straight out of the function. `Validated::to_result` converts one that was already decoded, and that is exactly what `decode_or_stop` does — it is the shape that differs, not the work. **It is the caller that stops, not the decode**: the `Err` still holds every rejection, because a schema's `read` ends in a `Validated` and has no way to short-circuit. ### A number keeps its text `Raw::Number` holds the token, not a `Float`. A `Float` carries about fifteen significant digits, so `9007199254740993` comes back one short of itself and `10.10` is never exactly recoverable — and a schema that decoded a `Decimal` through one would rebuild, inside the library meant to prevent that class of thing, the exact bug it exists to prevent. `std::json` keeps its numbers the same way, so the two trees agree about what a number is. They remain separate types: a `Raw` is what *any* source produced, and JSON is one source. ### A secret does not appear in its own error Quoting the bad value is most of what makes a decode error worth reading, and it is also the easiest imaginable way to put a password in a log. So `Rejection`'s `found` is a `Redacted` unconditionally — not only inside a secret — and `describe` decides once whether to expose it. No later variant can forget. ```text public should be a whole number, and is "not a number" token should be a whole number ``` ## The declaration is the schema Effect writes the schema and recovers the type, because TypeScript erases types. Khora's compiler holds every declaration, so the direction reverses: `derive(Decode)` writes the schema from the type. ```khora derive(Show, Decode, Encode) pub type Mode = | Local | Remote(url: String); derive(Show, Decode) pub type Settings = { pub listen: Listen, pub password: Redacted, pub debug: Option, pub rate: Decimal, pub tags: List, pub mode: Mode, }; ``` Nothing describes the record a second time. Every customization is a type: `Option` is optional, `List` is a list, `Redacted` is a secret whose failures never quote what they saw, `Decimal` is exact, a nested type is found through the trait — so `Listen` above may derive its schema or write one by hand with a refined port, and `Settings` picks up whichever it is. A variant derives to a bare string for a payload-free case and an object tagged with `type` for the rest; a newtype, `type UserId = Int`, is transparent; a type that mentions itself needs nothing written, because a derived schema is built when it is first read. The `///` above the type and above each field is carried along as the description, so the JSON Schema rendered from `Settings` says about `rate` what the comment next to `rate` says. Two things a derive refuses, at the `derive` line: a field whose type has no `Decode`, and a case whose payload has no field names, because the wire needs a key and a name the type did not declare is not the compiler's to invent. ## The record literal The hand-written form is the record's own literal with a schema where each value would go: ```khora impl Decode for Listen { fn schema() -> Schema { struct({ host: string(), port: between(int(), 1, 65535) }) } } ``` Which record it is comes from the type the expression is asked for — the declared return type here, an annotation or a parameter elsewhere — or from the labels alone when only one record has them, the way any record literal resolves. A field whose schema decodes the wrong type is reported at that schema; a field the record does not have is reported at the call. `struct` is not a function that runs. Its argument is a record of *schemas* and its result a schema of the record they decode, and there is no type-level map from one to the other; a call to it is rewritten before it is typed into `Schema::record` over `Fields`, which is what a derived schema is too. So it takes a record literal and nothing else, there is no arity, and a record with a hand-written schema is picked up by every schema that contains it. **There is no field limit.** `struct` is rewritten into `Schema::record` over `Fields`, and `Fields::zip` nests a tuple however many fields there are — a seven-field record checks, and so does a twelve-field one. **`std::json` parses and prints, and does not decode.** `parse` turns text into a `Json` and `Raw::of_json` turns that into what a schema reads; `Raw::to_json` and `encode` are the way back. There is no second decoding vocabulary beside this one. ## See also - [`std::schema` API](/docs/v0.4/stdlib/api/schema/) — every type and function, with its signature. - [Decode untrusted input](/docs/v0.4/cookbook/decoding-input/) — the whole thing as a program, with its output. - [Load application configuration](/docs/v0.4/cookbook/configuration/) — the same schema read out of the environment through `std::config::read`. # Decimals Source: https://khoralang.com/docs/v0.4/stdlib/decimal/index.md `Decimal` is exact base-10 arithmetic, and it is deliberately not `Float`. Use `Float` for approximate numerical work; use `Decimal` when the base-10 representation is part of being correct — money, tax, anything a schema or a regulator specified in decimal digits. ```khora let subtotal: Decimal = 19.99d; let tax_rate: Decimal = 0.0825d; ``` The `d` suffix is the literal; the module needs `import std::decimal::{Decimal};` for it to have a type. The suffix is visible in the source on purpose, so a reader can tell at a glance which kind of arithmetic an API is doing. ## Arithmetic is by method, comparison is by operator `Decimal` compares with the ordinary operators, because `Eq` and `Ord` are traits and `Decimal` implements them — **by value**, so `1.50d` and `1.5d` are the same number: ```khora if paid == owed { settle(invoice) } if amount < limit { approve(amount) } ``` Arithmetic is by name. `+`, `-` and `*` belong to the primitive numeric types, and adding a trait per operator is a language change Khora has not taken: ```khora let total = Decimal::sub(Decimal::add(subtotal, shipping), discount); let tax = 19.99d |> Decimal::mul(0.0825d) |> Decimal::rounded(2, Rounding::HalfEven); ``` Both forms are the same call. The pipeline is the shape a chain of money operations tends to take, and is worth knowing for that reason. ## Scale is part of the number `mul` **adds** the scales: two numbers with four decimal places give one with eight. `add` brings both operands up to the larger scale. That is what makes the arithmetic exact, and it is also what makes a long chain overflow sooner than people expect. `rounded` is the way back: ```khora let charge = Decimal::rounded(total, 2, Rounding::HalfEven); ``` `Show` prints every place the scale says, so `Decimal::scaled(150, 2)` is `1.50` and not `1.5`. A price to two places stays a price to two places, which is what makes a column of them line up. `divide` is the one operation that cannot be exact, so it takes the scale and the rounding mode and answers `Option` — `None` when the divisor is zero. Everything else stops the program rather than returning a number that is not the answer; see [Traps](/docs/v0.4/reference/traps/). ## How much fits The significand is 128 bits — 38 digits. That covers every fiat computation, `NUMERIC` as real schemas declare it, and an eighteen-decimal token balance. It matters because alignment happens before addition, so what has to fit is not the two numbers but the *aligned* ones: ```khora let notional = 100000000.00d; // scale 2 let rate = 0.000000000001d; // scale 12 let together = Decimal::add(notional, rate); // needs the notional at scale 12 ``` Aligned, the notional needs 20 significant digits — well inside 38, and the sort of pair a rates desk writes down every day. Going past 38 digits stops the program: the answer would be a different number, and a different total is the failure this type exists to prevent. ## A column of them ```khora let total = Decimal::total(rows); // every one added up let shown = Decimal::at_scale(Decimal::total(rows), 2); // and at the column's width ``` `total` of nothing is `Decimal::zero()`, which has scale nought and prints as `0`; `at_scale` only ever raises the scale, so it cannot round a total that was already wider. `Decimal::zero_at(2)` is the empty total where the shape matters from the start. `abs`, `min` and `max` are there too, comparing by value, so `1.5d` and `1.50d` are one number to them as well. ## See also - [`std::decimal` API](/docs/v0.4/stdlib/api/decimal/) — every operation, with its signature. - [Decimal literals](/docs/v0.4/reference/lexical-structure/#decimal-literals) — the `d` suffix in the grammar. - [Traps](/docs/v0.4/reference/traps/) — what overflow does, and why it is not a wrapped answer. # Packages Source: https://khoralang.com/docs/v0.4/packages/index.md Khora's standard library holds the contracts that two pieces of software must agree on: `Db`'s shape, the trace vocabulary, the row and cell types a result set is made of. **The things that speak a protocol are packages**, because a protocol is versioned by somebody else and a release schedule that is not Khora's should not be able to hold up a compiler release. That split is settled in [the ecosystem design note](https://github.com/codyspate/khoralang/blob/main/docs/design/ecosystem.md), and it means a real program reaches for packages early. These pages document the ones maintained in the Khora repository. ## Naming one in a manifest **There is no registry yet.** A dependency names where it lives — a repository, a revision, and the directory the package's own manifest sits in: ```toml [dependencies] postgres = { git = "https://github.com/codyspate/khoralang", rev = "v0.4.0", subdir = "packages/postgres" } ``` `khora.lock` records the commit each dependency resolved to, so a build stays reproducible even when the revision is a branch name. **Pin a tag when the dependency lives in the compiler's own repository** — `rev = "v0.4.0"` for a published 0.4.0 toolchain, or `rev = "v0.4.0-rc.2"` for that published candidate — because `main` moves ahead of the compiler you have, and a package from a newer commit may use `std` that yours lacks. These tags must exist on the release page before the corresponding example can be installed. `khora install --subdir ` writes the line for you. The full set of dependency keys is in [the manifest reference](/docs/v0.4/reference/manifest/). A registry is a 1.0 question. Until there is one, `git` and `subdir` are the whole mechanism, and a monorepo of packages works because `subdir` exists. ## What is here - **[`postgres`](/docs/v0.4/packages/postgres/)** — a PostgreSQL client that speaks the wire protocol directly, with no `libpq` to install. Documented here. - **`ai`** — the effect a caller names when it wants model inference, so the provider stays the caller's choice. It declares `LLMService` and `extract` and implements no provider: there is no HTTP and no tokenizer in it, and a handler supplies those. **No page here yet** — read [`packages/ai/src/llm.kh`](https://github.com/codyspate/khoralang/blob/main/packages/ai/src/llm.kh), whose module comment is the documentation. - **`otlp`** — an exporter for the trace vocabulary in `std::trace`, over OTLP/HTTP JSON. **No page here yet** — read [`packages/otlp/README.md`](https://github.com/codyspate/khoralang/blob/main/packages/otlp/README.md). Each of the three is depended on the same way, with `git` and `subdir`; not having a page here is a gap in this section rather than a difference in how the package is used. ## These are not covered by the language's compatibility promise [Compatibility](/docs/v0.4/reference/compatibility/) is a statement about the language and `std`. A package in this repository is versioned with the compiler for convenience, not as a promise: it can break between releases when the protocol underneath it changes, and the reason to keep it out of `std` is exactly that it might need to. `postgres` has a page here, and the honest list of gaps is at the bottom of it. For `ai` and `otlp` that list is in the source linked above. # postgres Source: https://khoralang.com/docs/v0.4/packages/postgres/index.md A PostgreSQL client written in Khora. It speaks the v3 wire protocol over a socket — there is no `libpq` to install and no C to link — and it supplies the [`Db`](/docs/v0.4/stdlib/api/db/) capability that `std::db`'s transaction contract is written against. ```toml [dependencies] postgres = { git = "https://github.com/codyspate/khoralang", rev = "v0.4.0", subdir = "packages/postgres" } ``` Use `v0.4.0-rc.2` instead when testing that published candidate with a `0.4.0-rc.2` toolchain; the stable tag is available only after publication. **[`packages/postgres/README.md`](https://github.com/codyspate/khoralang/blob/main/packages/postgres/README.md) is the reference** — every function, both usage styles, and the honest list of what is missing. This page is the overview. ## As a capability The point of the package is that a caller does not hold a connection. It holds `Db`, and `std::db`'s [`transaction`](/docs/v0.4/stdlib/api/db/) decides what happens when a fiber is canceled mid-statement: ```khora import postgres::pool::{open, with_db}; let pool = open(crew, settings, 8); with_db(pool, fn () => transfer(10, 20, 2500) ) ``` A `transaction` inside another one on the same lease is a savepoint on that connection (`SAVEPOINT khora_sp_1`, `khora_sp_2`, and so on, one per level), released when the inner body answers `Ok` and rolled back to when it does not. Each connection keeps its own depth, so fibers on different leases nest independently. A `COMMIT` that PostgreSQL answers with a rollback, because a statement in the transaction failed, is reported as `DbError::RolledBack`. `with_db` leases a connection, installs it as the `db` capability for the duration, and returns it afterwards — including when the body raises, and including when the fiber is canceled, whether in the body or while it is still waiting for a connection. `transfer` never names a connection, which is what keeps the capability from turning back into a parameter threaded through every signature. A connection can also be used directly, without a pool, for a script or a migration. The README covers that shape. ## Lost connections A pool reconnects a connection it loses, and never lends one that is down. - **What counts as lost.** A reply cut off partway, a `ROLLBACK` that did not arrive (`std::db` calls `broken`), a connection the server closed or reset while it sat idle, and one left inside a transaction. Each connection is checked before it is lent, so one the server closed between leases is caught there rather than by the next query. A notification, a notice or a parameter change the server sent to an idle connection does not count: the connection is lent as it is, with its session. A message of any other kind counts as soon as its first byte is in, so a connection the server is in the middle of ending is not lent. - **A stopped caller costs nothing.** A fiber stopped by `cancel`, `abort` or `cancel_within` anywhere in `with_db` — waiting for a connection, holding one, in the middle of a statement or a batch, or inside a transaction's `ROLLBACK` — gives its connection back, and the pool stays its full size. A reply still arriving on it is read to the end before the connection is lent again, so the next borrower never receives it. A fiber the body spawns cannot use the body's `db`; it takes a lease of its own. - **Reconnecting.** The old socket is closed first and nothing more is read from it. The new one is tried on a backoff: 50 ms, doubling to 5 s, each delay drawn at 50-100% of its value, for up to 30 s. `with_db` waits through it, the same way it waits when every connection is busy. A connection that opens and fails its check before it is ever lent counts as a failed attempt on the same backoff, so a server that accepts connections and spoils each one sees the backoff's handful of attempts, not a slot reconnecting as fast as it can. The 30 s counts time spent reconnecting, not the time a connection sat open, so one left idle for longer and then closed by a server restart is retried on the backoff too. - **Shrinking.** A connection that has not come back after 30 s leaves the pool. It is not lent, and it tries again every 30 s (±20%) until it connects, when it rejoins. So a pool that lost its server grows back to full size within one retry interval of the server's return. - **An empty pool answers at once.** When no connection is live or reconnecting, `with_db` returns `Err(DbError::Disconnected(reason))` with the reason the last attempt failed, and so do the callers that were already waiting. It does not wait for the next 30-second retry. ```khora import std::resilience::{Schedule}; import postgres::pool::{Reconnect, health, open_with}; let plan: Reconnect = { fast: Schedule::UpTo(Schedule::backoff(100, 2000), 10000), slow: Option::Some(60000), handshake: 5000, }; let pool = open_with(crew, settings, 8, plan); let now = health(pool); // { live, reconnecting, down } ``` `Reconnect::default()` is what `open` uses. `Reconnect::never()` makes one attempt and never retries, and `slow: Option::None` keeps a connection that gave up out of the pool for good. `handshake` is how long, in milliseconds, an attempt waits for the server to finish the startup exchange before it counts as failed and the backoff goes on; the default is 10 s. A server that accepts connections and never answers is therefore a server that is down, not a pool that hangs. `health` is for a readiness check or a test: a pool with `live` and `reconnecting` both 0 is failing its callers. `open` returns before any connection has opened, and a pool lends nothing until one does. A pool whose server is unreachable answers its first callers `Disconnected` once the 30-second backoff has run out. **Limits.** - A connection that dies between the check and the borrower's first statement answers that statement `Disconnected`. The pool does not resend it, because it cannot know whether the server ran it. Retrying is the caller's decision, and the connection reconnects once the lease ends. - `close` ends a connection that is waiting to retry within about 25 ms, and one in the middle of the startup exchange within the `handshake` bound. The TCP connect before that exchange has no bound of its own: an address that drops packets rather than refusing takes the operating system's connect timeout, which is minutes on Linux, and `close` waits for it. - A connection whose peer vanished without closing (a cable pulled, a firewall dropping the flow) looks healthy until a statement times out on it. The check reads what has arrived and cannot see that. - A lease hands the borrower the connection itself: the borrower's fiber writes the socket and reads the reply, with no fiber in between. What a lease costs on top of its statements is taking the connection, the check (one non-blocking read), and giving it back. ## Authentication `scram-sha-256` is the default on PostgreSQL 14 and later, and it is what this speaks — a stock server needs nothing done to it first. The password is never transmitted. The client proves it knows one by signing a challenge built from both sides' nonces, so a recording of one exchange cannot be replayed into another. **The server is made to prove itself too**: its `ServerSignature` is checked rather than accepted, because a client that skips that has proved itself to a peer it never made prove anything back. `SCRAM-SHA-256-PLUS` is deliberately **not** offered. The channel-binding variant exists so that a client refuses to authenticate through a proxy it cannot see, and advertising it without binding to the channel throws that away. ## Prepared statements Each connection parses a statement once and reuses the parse by name. The first time a connection meets a statement it sends Parse, Bind, Describe, Execute and Sync, and keeps the name and the columns the server described. After that, running the same SQL sends only Bind, Execute and Sync. Round trips are the same either way: one write, one read. - **A refusal forgets the statement.** If the server refuses a prepared statement, the connection drops it and parses it again on the next call. This covers a statement the server has dropped (`DEALLOCATE`, `DISCARD ALL`), and a table altered so that the statement's result type changed. The call that meets the change still fails, as it does in pgx. - **At most 512 per connection.** One more closes the statement used longest ago, on the server as well, so a program that builds its SQL out of values cannot fill the server's memory with plans. - **Not behind a transaction-mode pooler.** A pooler that hands each transaction a different server connection (PgBouncer's transaction mode) does not carry the names across, so use session mode, as with pgx. ## Many lookups in one exchange `db.query_each(sql, sets)` runs one statement once per set of values and answers what `db.query` once per set would, in order. This handler pipelines it: every set goes to the server in one write, as Bind, Execute and Sync, and the replies are read in order. Twenty lookups cost one round trip instead of twenty. ```khora import std::core::{List, Result}; import std::db::{Cell, Db, DbError, Row}; fn users(ids: List) -> List, DbError>> with { db: Db } { db.query_each( "select id, name from users where id = $1", List::map(ids, fn id => [Cell::Number(id)]), ) } ``` - **Each set is its own statement, with its own `Sync`.** Outside a transaction each runs in its own implicit transaction, so a set that fails fails alone and the sets after it still run. Inside a transaction a failed set aborts it, and the sets after it are refused, as separate `query` calls would be. - **The first use of a statement on a connection prepares it** with the first set, the ordinary way, and pipelines the rest behind it. - **A batch that is refused throughout is asked again.** A statement the server dropped refuses every set that names it; the sets after the first refusal are then run again after a fresh parse, which is what separate calls would have done. - **A borrower canceled while its batch's replies are arriving** leaves the rest on the connection, and the connection knows how many are still owed. They are read to the end before it is lent again, so the next borrower never receives them. ## What it does not do yet Named here because a driver's gaps decide whether it fits a service: - **TLS.** `SSLRequest` is one message and `std::net::tls` exists, so this is closer than it sounds. Until then, a password does not cross the network in the clear under SCRAM — but **every row you read does**. On an untrusted network, this is not ready. - **MD5 authentication.** Refused by name rather than hung. `ring` does not carry MD5, deliberately. - **Binary result format, `COPY`, notifications, cursors.** ## Status Versioned with the compiler for convenience, not as a promise — see [Packages](/docs/v0.4/packages/) on what the compatibility statement does and does not cover. It is tested against a real PostgreSQL 17 in the repository's own suite, and the SCRAM implementation is checked against the RFC 7677 worked example as well as against a live server. # Cookbook Source: https://khoralang.com/docs/v0.4/cookbook/index.md The Cookbook combines Khora language features and shipped standard-library APIs into complete application patterns. Each recipe includes the imports, types, functions, and boundary wiring needed to understand the example as a whole instead of showing an isolated call with important pieces omitted. Start with the recipe closest to the problem you are solving: - [Build an HTTP service](/docs/v0.4/cookbook/http-service/) — route requests with `Router`, `Request`, `Response`, and `SharedFn`. - [Build a typed JSON API](/docs/v0.4/cookbook/json-api/) — parse JSON, decode typed request bodies, derive encoders/decoders, and return JSON responses. - [Run a database transaction](/docs/v0.4/cookbook/database-transactions/) — use the portable `Db` capability and `transaction` so failure and cancellation cannot leak an open transaction. - [Take work off a queue safely](/docs/v0.4/cookbook/taking-work-off-a-queue/) — register a job with a region before the first `!` after it, so a cancellation cannot drop work nobody knows the fiber was holding. - [Bound concurrent work](/docs/v0.4/cookbook/bounded-concurrency/) — use `Fiber`, `Nursery`, and `bounded_nursery` to turn a concurrency limit into backpressure. - [Give work a deadline](/docs/v0.4/cookbook/timeouts-and-cancellation/) — build a timeout from a fiber and a clock, and give the work somewhere to be interrupted. - [Make resource cleanup cancellation-safe](/docs/v0.4/cookbook/cancellation-safe-resources/) — pair `scoped` and `acquire` so cleanup runs on return, failure, and cancellation. - [Decode untrusted input](/docs/v0.4/cookbook/decoding-input/) — describe a value's shape once with `std::schema`, decode it from anywhere, and report every bad field at once. - [Load application configuration](/docs/v0.4/cookbook/configuration/) — read settings with `std::config`, report every bad key at once, and keep secrets out of the logs with `Redacted`. - [Retry a flaky call](/docs/v0.4/cookbook/retrying/) — pick a `Schedule`, drive it with `retry_while`, and test the whole thing in under a millisecond. - [Test code that uses capabilities](/docs/v0.4/cookbook/testing-capabilities/) — supply deterministic handlers without global mutation or test-only branches in application code. - [Trace an operation](/docs/v0.4/cookbook/tracing/) — implement a `Tracer`, wrap work with `around`, and keep span lifetime structured. - [Log, and correlate it with traces](/docs/v0.4/cookbook/logging/) — emit one JSON object per line with `std::log`, and carry the trace and span identifiers onto every line inside a span. ## How to read the examples The examples use current Khora syntax and the public APIs shipped in `std`. They favor small complete modules over framework-style pseudocode. When a recipe depends on a provider outside `std`—for example a concrete PostgreSQL connection—it says so at the construction boundary instead of inventing a driver API. For exact declarations after you understand the pattern, use the [Standard Library API reference](/docs/v0.4/stdlib/). For the language rules underneath the pattern, use the [Language Reference](/docs/v0.4/reference/). # Build an HTTP service Source: https://khoralang.com/docs/v0.4/cookbook/http-service/index.md Khora's shipped HTTP router works directly with `Request`, `Response`, `Router`, and shareable handler functions. A small service does not need an application framework before it can route requests. ## Complete example This service exposes `/health` and `/hello?name=...` and listens on port 8080: ```khora module main; import std::core::{ChildFailed, Option, SharedFn}; import std::net::http::{HttpError, Request, Response, Router}; fn health(_request: Request) -> Response { Response::text(200, "ok") } fn hello(request: Request) -> Response { let name = match Request::query(request, "name") { Option::Some(value) => value, Option::None => "world", }; Response::text(200, "hello ${name}") } pub fn main() raises HttpError + ChildFailed { Router::new() |> Router::get("/health", SharedFn::of(health)) |> Router::get("/hello", SharedFn::of(hello)) |> Router::listen(8080)! } ``` A route handler is an ordinary direct-style function from `Request` to `Response`: ```khora fn hello(request: Request) -> Response ``` `SharedFn::of` certifies the handler for the router's concurrent serving boundary. The router can then invoke the handler from request fibers without turning the handler into a special async type. ## Read request data once it reaches the handler The HTTP layer parses the request before routing it. Handlers can read the normalized path, matched route parameters, query values, headers, and body directly from `Request`. For example, a route with a path parameter can inspect it through `request.params`: ```khora fn show_user(request: Request) -> Response { match Params::get(request.params, "id") { Option::Some(id) => Response::text(200, "user ${id}"), Option::None => Response::text(400, "missing id"), } } ``` and mount it with: ```khora Router::new() |> Router::get("/users/:id", SharedFn::of(show_user)) ``` ## Return transport decisions at the HTTP boundary A handler should translate application outcomes into HTTP status codes and response bodies at the boundary. Domain functions below the handler can keep their own typed failures instead of knowing about status code 404 or 503. For typed request/response bodies, continue with [JSON API](/docs/v0.4/cookbook/json-api/). For failure translation before the HTTP boundary, see [Typed failure with raises](/docs/v0.4/reference/failures/#translate-failure-types). ## Two verbs the router answers without being mounted **A `GET` route is also a `HEAD` route.** The router runs the `GET` handler and sends the headers alone — including the `Content-Length` of the body a `GET` would have sent, which is the whole point of the method. RFC 9110 requires the two answers to agree, and running the same handler is the only arrangement in which they cannot drift. Nothing above needs a second mount for `curl -I`, a cache, or a health checker that sends one. **`OPTIONS` is answered with `204` and an `Allow` header** naming what the path mounts, plus `OPTIONS` itself and `HEAD` wherever `GET` is mounted — the same list a `405` gives, because a client cannot be told two things about one path and act on both. A path nothing mounts is `404` rather than a `204` with an empty `Allow`: "this path allows nothing" and "there is no such path" are different answers. Mount either explicitly and the default steps aside for that path. There are two reasons to: - `Router::head`, for a resource whose length or validators are cheap and whose body is not, where answering `HEAD` by building the body and throwing it away is the entire cost of the request. Mounting it separately gives up the guarantee that `HEAD` and `GET` agree, so it earns its keep only when that cost is real. - `Router::options`, for CORS. The default answer is the correct reply to the question `OPTIONS` asks, but it carries no `Access-Control-Allow-Origin`: the library cannot know which origins a service trusts, and both "none" and "any" are wrong defaults. A browser calling this service from another origin sends a preflight first, and a handler mounted here is what answers it — including the `Allow` header the default would have sent, since the mount replaces the default rather than adding to it. The actual `GET` or `POST` response needs `Access-Control-Allow-Origin` too: the preflight authorizes the request, it does not authorize the answer. ```khora fn preflight(req: Request) -> Response { Response::text(204, "") |> Response::with_header("Access-Control-Allow-Origin", "https://app.example") |> Response::with_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") |> Response::with_header("Access-Control-Allow-Headers", "content-type") |> Response::with_header("Allow", "GET, POST, HEAD, OPTIONS") } ``` ## State a handler shares between requests A handler is called on a different fiber per connection, so anything they hold in common has to be shareable. **`Shared>` is the shape**, and it is worth saying because the maps a handler already has in hand are not: `request.params`, `request.queries` and `request.headers` are `Map`, `Map` is a record with `mut` fields, and a mutable record cannot cross into a fiber: ```text error: `Map` does not implement `Share`, which `Shared::of` requires ``` `Dict` is the persistent one — an insert gives back a new dictionary and leaves the old one alone — so it holds nothing writable and goes into a cell. Build the state before the router, and let each handler close over it: ```khora module main; import std::core::{ChildFailed, Dict, Option, Shared, SharedFn}; import std::net::http::{HttpError, Request, Response, Router}; /// One counter per name, in a cell every request fiber shares. fn greet(visits: Shared>, request: Request) -> Response { let name = match Request::query(request, "name") { Option::Some(value) => value, Option::None => "world", }; let seen = Shared::update(visits, fn table => Dict::insert(table, name, match Dict::get(table, name) { Option::Some(n) => n + 1, Option::None => 1, })); match Dict::get(seen, name) { Option::Some(n) => Response::text(200, "hello ${name} (${n})"), Option::None => Response::text(500, "lost the count"), } } pub fn main() raises HttpError + ChildFailed { let visits = Shared::of(Dict::new()); Router::new() |> Router::get("/hello", SharedFn::of(fn request => greet(visits, request))) |> Router::listen(8080)! } ``` `SharedFn::of` takes a closure literal, and the closure captures `visits` — so the state reaches the handler as an ordinary parameter. A capability reaches one the same way, which is the [parameter form](/docs/v0.4/reference/capabilities/#a-capability-as-an-ordinary-parameter): `SharedFn` has no capability row, so a handler that needs a database or an HTTP client takes it as an argument and the closure closes over it. `Shared::update` reads, changes and writes under one lock, which is what makes the read-modify-write above correct when two requests for the same name arrive at once; two separate `get` and `set` calls would not be. Use `Shared::modify` when the change has something to report beyond the new state — the key it generated, say. **A cell is process-local.** It does not survive a restart and a second instance does not see it, and [a trap in a handler ends the process](#a-trap-in-a-handler-ends-the-server). So this is for what a restart can rebuild — a cache, a counter, a connection registry — and anything else belongs in a database. ## Bound the resource that is actually constrained **The server has one capacity number, not two.** `Router::listen` runs its accept loop inside `bounded_nursery(256, ...)`, and an accepted connection is a fiber that is inside your handler for as long as the handler runs. There is no second, smaller pool that handlers queue for, so that one number is both the most connections served at once and the most handlers running at once — 257 live, in fact, for the reason [Bounded concurrency](/docs/v0.4/cookbook/bounded-concurrency/) gives. It is not configurable in this release. That bound is not usually what limits throughput: a server saturates its cores well before it runs out of connection slots, and past saturation, raising a bound lengthens the queue while lowering it sheds load sooner — neither makes the server faster. Where the saturation point actually is for your handler on your machine is a measurement, and [Performance](/docs/v0.4/performance/) sets out what a number has to carry before it is worth quoting: the ladder, the generator, the machine, the profile and the date. This page deliberately prints none, because it had one and it carried none of them. So bound the thing that is actually scarce. If a handler waits on a database pool or a rate-limited API, put a smaller bound around *that* work rather than lowering the connection limit, which would refuse connections that could have been served. See [Bounded concurrency](/docs/v0.4/cookbook/bounded-concurrency/). The other number is per request rather than per connection, and it *is* configurable. A router holds at most 8 KB of one request — headers and body together — and answers `413` past it, before the handler runs. `Router::holding(most)` sets another: ```khora Router::new() |> Router::holding(1048576) |> Router::post("/documents", SharedFn::of(store)) |> Router::listen(8080)! ``` The default is a policy about how much an unauthenticated client may make a server hold, not a limit of the parser. The buffer is allocated once at that size per connection, so the number you choose multiplies by the 256 above when deciding what a full server costs — raise it to what the largest legitimate document needs and not further. ## Stopping a service `Router::listen` serves until the process stops. **A `SIGTERM` or `SIGINT` stops it** — the signal becomes a cancellation at the root, a parked `accept` observes it in about ten milliseconds, and `scoped` finalizers run on the way out. For an ordinary deploy that is the whole answer and this section is optional reading. It needs nothing of `main`: a cancellation stops any function, whatever its `raises` row. A `/shutdown` route exists for the case the signal does not cover: a service that stops *itself* on a request. It runs `listen` on a fiber and lets go of that fiber when a route says to. **Cancel the listener, or detach it — both work.** A canceled listener notices in the poll loop a parked `accept` sits in, unwinds, and releases its port. `cancel` is the better of the two when you want the port released before the next line runs: it stops the listener and `Fiber::wait` tells you when that has happened. `detach` says "stop waiting on this" and returns at once, which is what you want when nothing afterwards depends on the socket being closed. Drain first and stop last: stop accepting work, wait for what is in flight, then let go of the listener. Inside a `main` that has already created the `jobs` channel, spawned `worker`, bound `port`, and installed a `Clock`: ```khora let stop = Shared::of(false); let server = Fiber::spawn(fn () => Router::new() |> Router::post("/shutdown", SharedFn::of(fn _r => { Shared::set(stop, true); Response::text(200, "stopping") })) |> Router::listen(port)!); loop { clock.sleep(50); if Shared::get(stop) { break }; }; // Whatever the handlers feed: close it, and wait for the work already taken. Channel::close(jobs); Fiber::wait(worker)!; // Only now. Fiber::detach(server); ``` A worker that fails on its own prints `khora: a fiber ended with an error nobody was waiting for`. The line is written **the moment that fiber ends**, not at exit, and neither `Fiber::wait` nor `Fiber::join` changes whether it appears — the fiber writes it before any joiner could have taken its outcome, so a failure the program joins and handles is reported too. Cancellation is excluded, so the shutdown above is silent: the line appears only when a child raised. It cannot be suppressed and does not change the exit status — see [known limitations](/docs/v0.4/limitations/#concurrency-combinators). Read it as "a fiber raised", not as "a failure went unhandled" — the wording claims more than the runtime checks, and a program that handles its failures correctly still sees it. What is reliable is the *position*: the line sits where the failure happened, so one among a program's first few lines means something failed during startup. A listener that could not bind is the usual one. Two things are worth being deliberate about. A request that arrives during the drain finds a closed channel, so `Channel::send` answers `false` — count it where it is refused, or the job is accepted and never seen again; the same reconciliation [taking work off a queue](/docs/v0.4/cookbook/taking-work-off-a-queue/) is about, one layer up. **A program does not exit while a listener fiber is still running.** Releasing a `Fiber` handle waits for the child — that is the guarantee that stops a fiber outliving the scope that owns it — and a listener never ends on its own. So a `main` that spawns a listener and then finishes its other work *hangs at the end*, after its last line, with nothing printed to say why. Cancel the listener before `main` returns, or detach it if the program is exiting anyway: ```khora Fiber::cancel(server); Fiber::wait(server)!; ``` This is the same fact as "drain first, stop last" seen from the other side: the listener is a child like any other, and somebody has to end it. **A listener that cannot bind does not stop the process on its own.** `Router::listen` raises inside the fiber that `Fiber::spawn` started. The runtime prints `khora: a fiber ended with an error nobody was waiting for` when that happens, so the failure is not silent — but a `main` polling a stop flag has still been told nothing it can act on, and keeps polling. A second copy started on a busy port serves nothing and, under a supervisor that restarts on exit, never restarts. Logging the port before you listen does **not** mitigate this: the line is emitted whether or not the bind succeeded, so it reports a healthy start for a process listening on nothing. Ask the fiber instead. `Fiber::finished` answers without waiting, which is the one question a supervisor loop needs: ```khora let server = Fiber::spawn(fn () => Router::listen(router, port)!); loop { clock.sleep(50); if Fiber::finished(server) { // A listener that stopped without being asked never started. `join` has // the error; nothing below this is worth doing. // // Ask `canceled` first. A fiber that was told to stop has no answer, and // `join` on one ends the program at 130 rather than handing back the // `HttpError` this branch is written to report. if Fiber::canceled(server) { break }; error("the listener stopped on its own"); Fiber::join(server)!; break }; if Shared::get(stop) { break }; }; ``` That turns a process that hung for ever into one that exits non-zero with the `HttpError` on stderr, which is what a supervisor can see. Probing your own port once after spawning works too and proves a little more — that the socket answers, not just that the fiber is alive: ```khora match HttpClient::send(client, Request::get("http://127.0.0.1:${port}/health")) { Result::Err(_) => { eprint("could not bind ${port}"); Fiber::detach(server); return 1 }, Result::Ok(_) => info("listening on ${port}"), }; ``` Restarting on the same port inside a minute is fine: the listener sets `SO_REUSEADDR`, so a port left in `TIME_WAIT` by the previous run binds straight away. A port held by a *live* process still fails, as it should. For a container, a `SIGTERM` unwinds the program and runs its finalizers, so the in-program half is covered. Draining at the layer above — out of the load balancer, wait, then stop — is still what [Containers](/docs/v0.4/deployment/containers/) recommends, because it is what stops *new* requests arriving. ## A trap in a handler ends the server The router turns a typed failure into a 500 rather than a dropped connection. It cannot do that for a trap. A checked overflow, an index outside an array or a division by zero does not unwind, so it never reaches the wrapper, and the process exits with status 134 taking every in-flight connection with it. The practical consequence is that request-shaped integers must be validated before they are used in arithmetic, which is what [Decoding input](/docs/v0.4/cookbook/decoding-input/) is for, and that a service wants more than one process behind it. [Traps](/docs/v0.4/reference/traps/#what-this-means-for-a-server) has the whole of it. For the complete router, request, response, and client surface, see the [HTTP API reference](/docs/v0.4/stdlib/api/net/http/). # Build a typed JSON API Source: https://khoralang.com/docs/v0.4/cookbook/json-api/index.md Use typed request and response records at the HTTP boundary. `std::json` parses wire text into `Json`, a derived schema reads a Khora type out of it and reports every problem, and `Encode` writes a typed value back for the response. ## Complete example This service accepts `POST /users` with a JSON body such as `{"name":"Ada"}` and returns a typed user as JSON: ```khora module main; import std::core::{ChildFailed, Result, SharedFn, Validated}; import std::json::{parse}; import std::net::http::{HttpError, Request, Response, Router}; import std::schema::{Decode, Encode, Raw}; derive(Decode) pub type CreateUser = { pub name: String, }; derive(Encode) pub type User = { id: Int, name: String, }; fn create_user(request: Request) -> Response { let document = match parse(request.body) { Result::Err(_) => return Response::text(400, "body is not valid JSON"), Result::Ok(value) => value, }; let input = match Schema::decode(CreateUser::schema(), Raw::of_json(document)) { Validated::Invalid(problems) => return Response::json(422, problems), Validated::Valid(input) => input, }; let user: User = { id: 1, name: input.name, }; Response::json(201, user) } pub fn main() raises HttpError + ChildFailed { Router::new() |> Router::post("/users", SharedFn::of(create_user)) |> Router::listen(8080)! } ``` The boundary handles two distinct failures separately. `parse` returns `Result::Err` when the body is not JSON at all, so the handler answers `400`. `decode` answers `Validated::Invalid` when the document is valid JSON but has the wrong shape for `CreateUser`, so the handler answers `422` — with every problem, because a `Rejection` encodes as an object with its `path` and its `message`, and a client wants the list rather than the first line of it: ```json [{"path":"name","message":"name should be text, and is 7"}] ``` ## A body over the limit never reaches the handler `create_user` distinguishes two client mistakes. There is a third it never sees: a request over the router's size limit is answered `413` and the connection closed before anything is parsed, so neither `parse` nor `decode` runs and none of the above applies. The default is 8 KB for headers and body together — the right answer to "how much may an unauthenticated client make a server hold", and the wrong one for an API that accepts a document. Say so in the pipeline: ```khora Router::new() |> Router::holding(1048576) |> Router::post("/users", SharedFn::of(create_user)) |> Router::listen(8080)! ``` What that number costs a full server is worked out in [an HTTP service](/docs/v0.4/cookbook/http-service/#bound-the-resource-that-is-actually-constrained). ## Derive when the wire shape matches the type These declarations: ```khora derive(Decode) pub type CreateUser = { pub name: String, }; ``` ask the compiler to write the schema from the type. This is the right default when the API representation intentionally matches the Khora record. When the external API has different field names, a rule that is part of the wire contract, or a legacy shape, implement `Decode` for that record by hand with a `struct({ .. })` literal, and every schema that contains it picks the impl up through the trait. [Decode untrusted input](/docs/v0.4/cookbook/decoding-input/) has the whole story. ## Parsing and decoding are separate operations Keep this distinction visible: ```text std::json::parse String -> Result Schema::decode Raw -> Validated Raw::of_json Json -> Raw ``` Two different answers, and `Raw::of_json` sits between them: it takes a `Json`, not the `Result` that `parse` hands back, so the `Result` has to be resolved first. `create_user` above does it in two `match` expressions for exactly that reason. A malformed document and a well-formed document with the wrong fields are different client mistakes. Keeping them separate also gives an API boundary enough information to choose different responses without string-parsing an error message. ## Encode typed responses `Response::json` takes anything that implements `Encode`: a derived record, a `Json` built by hand, a list of rejections. ```khora Response::json(201, user) ``` A record holding a `Redacted` has no `Encode`, so it cannot be sent by accident; the build stops at the `derive` line. A derived record's keys go out in the order the type declares them, and an `Option` field holding `None` is left out. A `Map` or a `Json` object goes out sorted by key. For more complex response policies, `std::schema::json_text(value)` gives the same text `Response::json` sends, and `Response::of` sends it with the status and content type you choose. `std::json::encode(Raw::to_json(value.encode()))` gives the same document built as a `Json` value first, with every object's keys sorted. For the complete JSON surface, see the [JSON API reference](/docs/v0.4/stdlib/api/json/). For HTTP routing and request data, see [HTTP service](/docs/v0.4/cookbook/http-service/). For state a handler keeps between requests — and why the answer is `Shared>` rather than the `Map` a `Request` hands you — see [State a handler shares between requests](/docs/v0.4/cookbook/http-service/#state-a-handler-shares-between-requests). # Run a database transaction Source: https://khoralang.com/docs/v0.4/cookbook/database-transactions/index.md Wrap the body: `transaction(fn () => body())`. It commits when the body answers `Result::Ok`, rolls back on `Result::Err`, and rolls back during unwinding if the body is canceled. It takes `db` from the capability row, so there is nothing to thread through. Khora keeps the transaction contract in `std::db` while concrete database engines live in packages. Application code depends on the `Db` **capability**, not on a database value threaded through every function call. That distinction is the point of the API. A function that talks to the database says so in its type: ```khora import std::db::{Cell, Db, DbError, Row, transaction}; fn load_account(id: Int) -> Result, DbError> with { db: Db } { db.query( "select id, balance from accounts where id = ?", [Cell::Number(id)], ) } ``` There is no `db: Db` parameter. `with { db: Db }` is the function's authority to perform database operations, and the caller supplies that authority at a boundary. `transaction` follows the same rule. It requires `db: Db` through its capability row, so transaction boundaries do not turn the capability back into explicit dependency plumbing. ## Complete example This complete module transfers money between two accounts. Both application functions require `Db` through their capability rows. The concrete `demo_db` handler appears only at the wiring boundary in `main`. ```khora module main; import std::core::{List, Result, Shared, Show, print}; import std::db::{Cell, Db, DbError, Row, transaction}; fn demo_db() -> Db { // The handler keeps the depth: how many transactions are open on its // connection. `transaction` asks for it to choose between `BEGIN` and a // savepoint. let depth = Shared::of(0); handler for Db { query: fn (_sql, _params) => Result::Ok(List::Nil), // The same answers as `query` once per set. A handler over a driver that // can pipeline sends every set before reading a reply; this one loops. query_each: fn (_sql, sets) => List::map(sets, fn _params => Result::Ok(List::Nil)), execute: fn (sql, _params) => { print("execute: ${sql}"); Result::Ok(1) }, depth: fn () => Shared::get(depth), begin: fn () => { print("BEGIN"); Shared::set(depth, 1); Result::Ok(()) }, commit: fn () => { print("COMMIT"); Shared::set(depth, 0); Result::Ok(()) }, rollback: fn () => { print("ROLLBACK"); Shared::set(depth, 0); Result::Ok(()) }, savepoint: fn level => { print("SAVEPOINT sp_${level}"); Shared::set(depth, level + 1); Result::Ok(()) }, release: fn level => { print("RELEASE SAVEPOINT sp_${level}"); Shared::set(depth, level); Result::Ok(()) }, // Called for a savepoint that was never opened when a cancel lands // before it was; that must change nothing. rollback_to: fn level => { if Shared::get(depth) > level { print("ROLLBACK TO SAVEPOINT sp_${level}"); Shared::set(depth, level); }; Result::Ok(()) }, broken: fn () => print("the connection's state is no longer known"), } } fn transfer_body( from_account: Int, to_account: Int, amount: Int, ) -> Result<(), DbError> with { db: Db } { let debited = db.execute( "update accounts set balance = balance - ? where id = ?", [Cell::Number(amount), Cell::Number(from_account)], ); match debited { Result::Err(error) => Result::Err(error), Result::Ok(_) => { let credited = db.execute( "update accounts set balance = balance + ? where id = ?", [Cell::Number(amount), Cell::Number(to_account)], ); match credited { Result::Err(error) => Result::Err(error), Result::Ok(_) => Result::Ok(()), } }, } } fn transfer( from_account: Int, to_account: Int, amount: Int, ) -> Result<(), DbError> with { db: Db } { transaction(fn () => transfer_body(from_account, to_account, amount) ) } pub fn main() { with { db: demo_db() } { match transfer(10, 20, 2500) { Result::Ok(_) => print("transfer committed"), Result::Err(error) => print("transfer failed: ${error}"), } } } ``` The dependency flow is visible directly in the signatures: ```text main installs db ↓ transfer with { db: Db } ↓ transaction with { db: Db } ↓ transfer_body with { db: Db } ↓ db.execute(...) ``` Neither `transfer` nor `transfer_body` knows whether `db` is PostgreSQL, SQLite, D1, an in-memory test handler, or something else. They know only that a `Db` capability is available. ## Failure behavior If either `execute` returns `Result::Err`, `transfer_body` returns that error. `transaction` sees the failed result and rolls the transaction back instead of committing it. If the fiber is canceled at any point after `transaction` starts, including while it waits for the server to answer `BEGIN` or `COMMIT`, the transaction's internal region finalizer performs the rollback during unwinding, so a pooled connection never goes back to the pool inside an open transaction. A caller does not need a second cancellation-specific transaction API. If `commit` itself fails, the commit error is returned. The helper does not report success for a transaction the database did not commit. A commit that loses its connection is reported as `DbError::Disconnected` with a message saying it is not known whether the transaction committed: the `COMMIT` may have reached the server, so do not treat that error as "nothing happened" and blindly retry. A body that ignores a failed statement and returns `Result::Ok` anyway is told `DbError::RolledBack`. PostgreSQL ends such a transaction with a rollback when it is asked to commit, and the PostgreSQL package reports that as the rollback it is. ## Nesting A `transaction` inside another `transaction` on the same `db` is nested for real, with a savepoint: - the inner one opens a savepoint instead of a transaction; - its `Result::Ok` releases the savepoint, so its writes become part of the enclosing transaction and commit or roll back with it; - its `Result::Err`, a raise, or a cancellation rolls back to the savepoint, which undoes only the inner writes. The enclosing body is told `DbError::RolledBack` and can carry on and commit its own. ```khora fn record_transfer(from_account: Int, to_account: Int, amount: Int) -> Result with { db: Db } { transaction(fn () => { // If the transfer fails, only its own writes are undone; the audit row // below is still written and committed. let moved = transaction(fn () => transfer_body(from_account, to_account, amount)); db.execute( "insert into audit (outcome) values (?)", [Cell::Text(match moved { Result::Ok(_) => "moved", Result::Err(e) => Show::show(e) })], ) }) } ``` Nothing is committed until the outermost `transaction` commits: an inner `Result::Ok` inside an outer failure is rolled back with the rest. Nesting goes as deep as the calls do, and each level is undone on its own. The depth belongs to the connection, which is why a handler keeps it rather than `transaction`: two fibers leasing two connections from a pool nest independently, and one `db` used twice from one fiber is one connection at one depth. A cancellation inside an inner body rolls back the inner savepoint first and then the enclosing transaction, so nothing either body wrote survives. A lease's `db` stays on the fiber that took the lease. `with_db` lends the body the connection itself, so a fiber spawned inside the body cannot use the body's `db`, and each fiber has its own transaction depth because each has its own connection. A fiber that needs the database takes its own lease: ```khora fn record_both(pool: Pool, a: Int, b: Int) -> () { let first = Fiber::spawn(fn () => with_db(pool, fn () => insert(a))); let second = Fiber::spawn(fn () => with_db(pool, fn () => insert(b))); Fiber::wait(first); Fiber::wait(second); } ``` Spawning a fiber that uses the body's `db` is refused where the fiber uses it, and the message names the line of the spawn: ```khora fn record_both(a: Int, b: Int) -> () with { db: Db } { let first = Fiber::spawn(fn () => insert(a)); let second = Fiber::spawn(fn () => insert(b)); Fiber::wait(first); Fiber::wait(second); } ``` ```text error: `db` cannot be handed to another fiber, and the fiber spawned at line 2, column 15 uses it here: a `Db` stays on the fiber it was installed on, because it drives a connection that fiber writes, and a fiber spawned inside a `with_db` body cannot use that body's `db`: take a lease in the spawned fiber instead, `Fiber::spawn(fn () => with_db(pool, work))` ``` Two fibers that each take a lease from a pool of one connection take turns: the second waits in `with_db` until the first lease ends. A handler implements nesting with four operations beside `begin`, `commit` and `rollback`: `depth` says how many transactions are open, and `savepoint`, `release` and `rollback_to` take the level a savepoint was opened at. `rollback_to` is called for a savepoint that was never opened when a cancellation lands before it was, and must answer `Result::Ok` without undoing anything then, exactly as `rollback` must when no transaction is open. A handler that does not support nesting still answers `depth` truthfully (1 between `begin` and its `commit` or `rollback`, 0 otherwise) and refuses `savepoint`; a nested `transaction` is then answered with that refusal instead of opening a second transaction. ## Install a real database at the boundary The portable application contract is the capability row: ```khora fn transfer(from_account: Int, to_account: Int, amount: Int) -> Result<(), DbError> with { db: Db } ``` A PostgreSQL, SQLite, D1, or other package constructs a handler that satisfies `Db`. Install that handler at the application's composition boundary: ```khora with { db: postgres_db } { transfer(10, 20, 2500) } ``` **No driver ships in `std`.** `postgres_db` above stands for a handler somebody has written; `Db` is a record of closures, so a handler over an existing client is a day's work and the in-memory double below is the whole of what a test needs. For the drivers maintained alongside the compiler, see [Packages](/docs/v0.4/packages/). The PostgreSQL pool helper does the same installation for a leased connection. Its callback requires `db: Db`; `with_db` supplies it and removes that requirement from the caller: ```khora with_db(pool, fn () => transfer(10, 20, 2500) ) ``` ## Testing becomes substitution, not plumbing Because the dependency is a capability, the same function can be tested with a different handler without changing its arguments: ```khora with { db: recording_db() } { transfer(10, 20, 2500) } ``` For exact `Db`, `Cell`, `DbError`, and `transaction` declarations, see the [database API reference](/docs/v0.4/stdlib/api/db/). For the capability model itself, see [Effects and capabilities](/docs/v0.4/reference/capabilities/). For the cleanup mechanism underneath cancellation-safe transactions, see [Resources and regions](/docs/v0.4/reference/memory-and-resources/). # Take work off a queue safely Source: https://khoralang.com/docs/v0.4/cookbook/taking-work-off-a-queue/index.md A worker pool is a fiber that reads a job from a channel and does it. The obvious loop loses jobs, silently, and exits 0 while doing it. ```khora loop { match Channel::receive(jobs) { Option::None => break, Option::Some(job) => serve(job)!, // the hole } } ``` `Channel::receive` has already emptied that slot. The call to `serve` is a cancellation point, and a cancellation that arrived in between is taken *before* the call runs — so the job is in nobody's hands. Not served, not in the queue, gone. Nothing is printed and the fiber unwinds cleanly. Measured over two hundred canceled rounds, the loop above took 198 jobs out of the channel and accounted for none of them. ## The rule **A value stops being the queue's and starts being yours at the moment it is received.** Between that moment and the first `!` after it, the fiber owns something nobody else knows about, and a cancellation there discards it. So register it before the first `!`: ```khora Option::Some(job) => { let settled = Shared::of(false); let region = Region::open(); Region::defer(region, fn () => { if !Shared::get(settled) { abandon(job); }; }); serve(job)!; Shared::set(settled, true); } ``` `Region::defer` runs on **every** way out of the enclosing block — a normal return, a typed failure, and an unwind from cancellation — which is why the flag is there. A finalizer that counted every job would be counting successes as losses; the flag is what tells it which of the three it is cleaning up after. Set it after the last `!`, where there is no longer a cancellation point between the work finishing and the record of it. What `abandon` should do is the application's decision — put the job back, mark it for redelivery, write it to a dead-letter queue, or simply record that it was dropped. The point is that something happens, and that the decision is made where the job is taken rather than left to nobody. **A `Region` is the right tool here because the cleanup depends on what happened.** For a resource that always releases the same way, `scoped` and `acquire` are shorter — see [Cancellation-safe resources](/docs/v0.4/cookbook/cancellation-safe-resources/). ## Complete example ```khora module worker::main; import std::clock::{Clock}; import std::core::{Channel, Fiber, Option, Region, Shared, attempt, print}; pub type Halt = | Now; fn serve(done: Shared) -> () raises Halt { let _ = Shared::update(done, fn n => n + 1); } fn worker( jobs: Channel, took: Shared, done: Shared, abandoned: Shared, ) -> () raises Halt { loop { match Channel::receive(jobs) { Option::None => break, Option::Some(_job) => { let _ = Shared::update(took, fn n => n + 1); // Registered before the first `!` below, because after that line this // fiber may never run again. The flag is what tells the finalizer // whether it is cleaning up after a cancellation or after success. let settled = Shared::of(false); let region = Region::open(); Region::defer(region, fn () => { if !Shared::get(settled) { let _ = Shared::update(abandoned, fn n => n + 1); }; }); serve(done)!; Shared::set(settled, true); }, } } } pub fn main() -> Int { let _ = attempt(fn () => { let jobs: Channel = Channel::bounded(4); let took = Shared::of(0); let done = Shared::of(0); let abandoned = Shared::of(0); let hand = Fiber::spawn(fn () => worker(jobs, took, done, abandoned)!); Channel::send(jobs, 1); Channel::send(jobs, 2); clock.sleep(40); Fiber::cancel(hand); Fiber::wait(hand)!; print("took ${Shared::get(took)}, served ${Shared::get(done)}, abandoned ${Shared::get(abandoned)}"); } with { clock: Clock::real() }); 0 } ``` Every job that came off the queue is in exactly one of the last two counts. That reconciliation — *taken equals served plus abandoned* — is the invariant worth asserting in a real pool, because it is the one that catches this class of bug. Over two hundred rounds cut at a jittered moment it held every time. Without the reconciliation the loss is invisible: the program exits 0 either way. **This program prints nothing on standard error**, and that is worth saying because a nearby sentence in the [known limitations](/docs/v0.4/limitations/#concurrency-combinators) is easy to misread. The runtime prints `khora: a fiber ended with an error nobody was waiting for` for a fiber that *failed* — written by that fiber the moment it ends, and neither `wait` nor `join` changes whether it appears. A cancellation is excluded from the message, so the canceled worker above is silent. If you ever do see the line from a supervisor like this one, a child raised on its own: investigate it rather than filtering it out, and note that it may be a failure the program went on to handle. ## Why the check is before the call and not after A `!` reads the cancellation flag before the call it marks, so a computation already asked to stop does not evaluate arguments or do work it is about to throw away. The cost is exactly the case above. Checking afterwards instead would not remove the problem, only move it: the fiber would then be holding the call's *result* when it unwound. Work in flight across a cancellation boundary is at risk whichever side the boundary is read on, and a region is what makes it recoverable. That is what regions are for. `Channel::send` and `Channel::receive` are the exception, and are safe: they are cancellation points themselves, and the runtime looks at the flag only once it has established there is nothing to take and no room to send. A canceled receive is never holding a value. See [Concurrency](/docs/v0.4/reference/concurrency/). ## See also - [Make resource cleanup cancellation-safe](/docs/v0.4/cookbook/cancellation-safe-resources/) — the same mechanism for a resource rather than a job. - [Bound concurrent work](/docs/v0.4/cookbook/bounded-concurrency/) — how many workers, and what happens when they are all busy. # Bound concurrent work Source: https://khoralang.com/docs/v0.4/cookbook/bounded-concurrency/index.md Structured concurrency tells you who owns concurrent work. `bounded_nursery` adds an admission limit: once the nursery has `limit` live children, adopting another child waits until capacity is available. Use this when the amount of work is controlled by the outside world or can otherwise grow beyond the capacity of a database, remote service, filesystem, or memory budget. ## Complete example This program has 1,000 jobs available but allows only 64 of them to be live children at once: ```khora module main; import std::core::{ChildFailed, Fiber, Nursery, bounded_nursery, print}; fn handle(job: Int) -> () { print("processing job ${job}"); } fn launch_jobs() -> () with { nursery: Nursery } { let mut next = 0; while next < 1000 { let job = next; nursery.adopt( Fiber::spawn(fn () => handle(job)) ); next = next + 1; } } pub fn main() raises ChildFailed { bounded_nursery(64, launch_jobs)! } ``` `launch_jobs` requires a `Nursery` capability because it adopts children. `bounded_nursery(64, launch_jobs)!` supplies that capability and does not return until all adopted children have finished. The `!` is there because a nursery raises [`ChildFailed`](/docs/v0.4/reference/concurrency/#a-child-that-failed) when a child fails: every child is waited for, the failure is reported, and the block's answer does not arrive. Whether it stops the siblings depends on where the failing child was adopted — see [Failure and cancellation](#failure-and-cancellation) below before you rely on it. The important line is not the `Fiber::spawn`; it is the adoption: ```khora nursery.adopt(Fiber::spawn(fn () => handle(job))); ``` When 64 children are already live, the next adoption waits. The producer therefore slows down at the same boundary where it creates more work instead of filling an unbounded queue somewhere else. **Subtract one when the limit stands for a real resource.** `bounded_nursery(64, ...)` admits **65** live children, because `Fiber::spawn` starts the child before `adopt` blocks on the limit — the fiber the producer is currently handing over is already running. It does not matter when the limit is a rate you picked; it matters a great deal when it is a connection pool of exactly 64, where the sixty-fifth child is the one that waits on a connection that will never come free. Write `bounded_nursery(63, ...)` for a pool of 64. **A limit of zero or less is not a bound at all** — it is how the unbounded [`nursery`](/docs/v0.4/stdlib/api/core/#bounded_nursery) is built — so check a limit that was computed from configuration before you pass it. `adopt` takes a `Fiber<(), 'er>`. The answer is fixed at `()` — a nursery has nothing to do with a result it cannot hand back — but the failure row is free, so a job that fails needs no `catch` at the adoption site: ```khora nursery.adopt(Fiber::spawn(fn () => handle(job)!)); ``` The row is for failures only. Every child can be canceled whatever its row; the row is what lets a child that can fail be adopted, with its failure reported to the nursery at runtime as `ChildFailed` rather than caught at compile time. Keep a job's answer by holding its handle instead of adopting it. `Fiber::join` gives back what the body computed, and re-raises what it raised. ## Keep unrelated limits separate A service can legitimately have different limits for different resources. For example, it might accept many mostly-idle HTTP connections while allowing only a smaller number of database operations to compete at once. Put the bounded nursery around the work controlled by the constrained resource rather than inventing one global fiber limit. For a known, already-bounded handful of tasks, use an ordinary `nursery` instead. If you have three independent lookups, the collection itself already bounds the fan-out; adding another limit usually adds machinery without changing behavior. ## Failure and cancellation The nursery owns its adopted children. On normal return it waits for them. If the nursery body leaves through failure or cancellation, children that are still running are canceled and joined before the nursery is released. **A child's own failure stops only the siblings still running when the nursery sees it.** A nursery reaps handles oldest-first, so a failure is not seen until every child adopted before it has finished. A failing child adopted first cancels all its siblings within milliseconds; one adopted last is seen after they have all finished, and cancels none. A bounded nursery also goes on admitting and starting new children after a failure has been recorded, so a limit of 64 over 1,000 jobs does not mean the run stops near the failure. What does hold is the other half: every child is waited for and `ChildFailed` is reported, never lost. So where a job is expensive, holds a connection, or has an effect outside the process, have the job check a `Shared` flag itself rather than expecting the group to collapse. [Known limitations](/docs/v0.4/limitations/#what-a-nursery-actually-does) has the measurements. That ownership rule is why bounded concurrency remains structured rather than becoming a semaphore wrapped around detached tasks. See [Concurrency](/docs/v0.4/reference/concurrency/) for the model underneath this and for the exact signatures, and [`Nursery` in `std::core`](/docs/v0.4/stdlib/api/core/#nursery) for the declarations. # Make resource cleanup cancellation-safe Source: https://khoralang.com/docs/v0.4/cookbook/cancellation-safe-resources/index.md A resource should register its cleanup as soon as acquisition succeeds. In Khora, `scoped` creates the lifetime and `acquire` ties a value's release function to that lifetime. That means the same cleanup path runs when the body returns normally, raises a typed failure, or is unwound by cancellation. **`acquire` is for a value with a release function.** When cleanup depends on *what happened* — whether the work finished, or what to do with a value already taken — open a `Region` and `defer` a closure instead: see [Take work off a queue safely](/docs/v0.4/cookbook/taking-work-off-a-queue/). ## Complete example This example intentionally raises after acquiring a resource. The registered release still runs before the failure leaves the scope: ```khora module main; import std::core::{Scope, acquire, print, scoped}; pub type Resource = { name: String, }; pub type UseError = | Failed; fn open_resource() -> Resource { print("open connection"); { name: "orders" } } fn close_resource(resource: Resource) -> () { print("close ${resource.name}"); } fn fail_after_open() -> Int with { scope: Scope } raises UseError { let resource = acquire(open_resource(), close_resource); print("using ${resource.name}"); raise UseError::Failed } pub fn main() { let result = scoped(fail_after_open)! catch { UseError::Failed => -1, }; print("result = ${result}"); } ``` The lifetime is established here: ```khora let resource = acquire(open_resource(), close_resource); ``` `acquire` returns the resource immediately, but also registers `close_resource(resource)` with the active `Scope`. The body does not need a later `close_resource(...)` line that can be skipped by non-local control flow. `scoped` supplies the `scope` capability required by `fail_after_open`: ```khora scoped(fail_after_open)! ``` When `fail_after_open` raises, the scope unwinds, the registered release runs, and only then does `UseError::Failed` continue outward to the `catch` in `main`. ## Cancellation uses the same lifetime rule Cancellation does not require a second cleanup mechanism. If a fiber is canceled at a cancellation point while it owns this scope, unwinding releases the scope and runs the same registered finalizers before the fiber is finished. That is the key production rule: **do not rely on the line after the work to release a resource**. Put release behavior into the resource lifetime itself. The same pattern underlies cancellation-safe transactions, pooled connections, sockets, files, permits, and tracing spans. See [Resources and regions](/docs/v0.4/reference/memory-and-resources/) for `Region`, `Scope`, `scoped`, and `acquire`, and [Fibers and nurseries](/docs/v0.4/reference/concurrency/) for cancellation behavior. # Decode untrusted input Source: https://khoralang.com/docs/v0.4/cookbook/decoding-input/index.md Write the type once. The compiler writes the schema from it, and the schema decodes a request body, a test fixture or the deployment's variables without knowing which. ```khora derive(Show, Decode) pub type Settings = { pub listen: Listen, pub password: Redacted, pub rate: Decimal, pub debug: Option, pub mode: Mode, }; let settings = Schema::decode(Settings::schema(), Raw::of_json(document)); ``` The alternative is a reader per source, and the signature of one that both names a field and fetches it shows why that does not generalize: ```khora pub fn string(name: String) -> Validated with { env: Env } ``` `string(name)` is not "this field is text". It is *go to the environment, fetch this variable, and give me the text or a reason* — the shape and the reading are one function. So a JSON body needs its own vocabulary, and so does a CLI argument, and so does a database row. Now `std::config::read(schema)` is one source among several, and the same `Settings` reads all of them. ## Every problem, not the first A record with six bad fields reports six. A person fixing a deployment wants the list, not one line per restart: ```text refused: listen.hostname is not set listen.port must be between 1 and 65535 password should be text rate should be an exact decimal, and is "about seven percent" debug should be true or false, and is "maybe" mode.type should be one of `Local`, `Remote`, and is "Cloud" ``` That is why `decode` answers a `Validated` rather than raising. Paths read the way somebody would write them, and a rule supplies its own sentence, so `between(int(), 1, 65535)` becomes *listen.port must be between 1 and 65535*. Text is quoted and a number is bare, so `and is "8080"` says the value arrived as text. A caller who has nothing to do with a list wants a `Result` instead, and [`Schema::decode_or_stop`](/docs/v0.4/stdlib/api/schema/#decode_or_stop) is that shape: `Result>`, which `!` carries straight out of the function. `Validated::to_result` converts a `Validated` you already have, and the two are the same call — `decode_or_stop` is `decode` followed by that conversion. **It is the caller that stops, not the decode.** The `Err` holds every rejection the decode found, because the combinators have no way to short-circuit: a schema's `read` is one function ending in a `Validated`, and the reader for two fields reads both to collect both. So this is a choice about the shape you want in hand, not about work saved. ## The declaration is the schema Every customization is a type. `Option` is optional, `Redacted` is a secret, `Decimal` is exact, `List` is a list, and a nested type is found through the `Decode` trait — which is where a rule the declaration cannot say goes: ```khora derive(Show) pub type Port = Int; impl Decode for Port { fn schema() -> Schema { Schema::map(between(int(), 1, 65535), fn n => Port(n)) } } ``` A `Port` that came through the schema passed the rule, and `Settings` picks the impl up through `Listen` without being told. `= Int` rather than `= pub Int` is what makes that true everywhere: another module cannot write `Port(99999)`, so every `Port` is one this module made. A variant derives to a bare string for a payload-free case and an object tagged with `type` for the rest, so `"mode": "Local"` and `"mode": { "type": "Remote", "url": ".." }` are the two forms. ## A record written by hand When the wire spells a field differently from the type, the record's schema is written as the record's own literal with a schema where each value would go: ```khora impl Decode for Listen { fn schema() -> Schema { struct({ host: key("hostname", string()), port: Port::schema() }) } } ``` The literal is checked against `Listen` from the declared return type, the way any record literal is. A field whose schema decodes the wrong type is reported at that schema, and a field the record does not have is reported at the call. ## A secret is a combinator `Redacted` in the type is `secret(string())` in the schema, and `secret` wraps any schema, so `secret(int())` works too. The decoded value shows as ``. More importantly, **a failure inside a secret does not quote what it saw**: ```text password should be text ``` Quoting the bad value is most of what makes a decode error worth reading, and it is also the easiest imaginable way to put a password in a log. The wrapper is unconditional inside the error type so no future variant can forget it, and only the message decides whether to expose it. A record holding a secret derives `Decode` and refuses `Encode`, so it reads and does not write. ## Numbers keep their text `Raw::Number` holds the token rather than a `Float`, so `decimal()` parses it with `Decimal::of_string` and `0.0725` stays `0.0725`. A price read through a double is the wrong price. `decimal()` reads text too, because money travels as a string on most wires, and `Decimal` encodes as one. ## A schema reads more than JSON `Raw::of_json` is one way in and not the only one. `Raw::of_map` takes a `Map` — a query string, a set of headers, the cells of a CSV row against its header — and hands it to the same schema: ```khora let cells: Map = Map::new(); for pair in List::zip(header, row) { Map::insert(cells, pair.key, String::trim(pair.value)); }; let sale: Validated = decode(Raw::of_map(cells)); ``` The rejections come out naming the column, because a `Raw`'s field name is what the schema asked for either way. This is what "the same `Schema` reads the environment, a request body and a test fixture" means in practice: the source changes and the declaration does not. ## Ask what a configuration needs, without running it A schema carries an untyped `Shape` beside its decoder, so its structure can be walked: ```khora print("it needs: ${Shape::keys(Settings::schema().shape)}"); ``` ```text it needs: [listen, password, rate, debug, mode] ``` The question a deployment asks. Top-level keys only — walk into `Shape::Struct` for the nested ones. A schema that were only a closure could not answer this at all, which is why it is two halves in one record. ## Complete example ```khora module service::main; import std::core::{List, Option, Redacted, Result, Show, Validated, print}; import std::decimal::{Decimal}; import std::json::{parse}; import std::schema::{Decode, Raw, Rejection, Schema, Shape, between, int, key, string, struct}; /// A port a socket will accept. A newtype, so the rule lives on the type and /// nothing downstream checks again. derive(Show) pub type Port = Int; impl Decode for Port { fn schema() -> Schema { Schema::map(between(int(), 1, 65535), fn n => Port(n)) } } /// Written by hand, because the wire spells the host `hostname`. Everything /// that contains a `Listen` finds this through the trait. derive(Show) pub type Listen = { host: String, port: Port }; impl Decode for Listen { fn schema() -> Schema { struct({ host: key("hostname", string()), port: Port::schema() }) } } /// Local, or an upstream by URL. derive(Show, Decode) pub type Mode = | Local | Remote(url: String); /// What the service needs, written once. The declaration is the schema. derive(Show, Decode) pub type Settings = { pub listen: Listen, pub password: Redacted, pub rate: Decimal, pub debug: Option, pub mode: Mode, }; fn report(text: String) -> String { match parse(text) { Result::Err(_why) => "not JSON", Result::Ok(document) => match Validated::to_result(Schema::decode(Settings::schema(), Raw::of_json(document))) { Result::Ok(s) => "listening on ${s.listen.host}:${s.listen.port} at ${s.rate}, password ${s.password}, debug ${s.debug}, mode ${s.mode}", Result::Err(problems) => List::fold(problems, "refused:", fn (acc, r) => acc + "\n " + Rejection::describe(r)), }, } } pub fn main() -> Int { print(report(`{"listen": {"hostname": "localhost", "port": 8080}, "password": "hunter2", "rate": "0.0725", "mode": "Local"}`)); print(report(`{"listen": {"port": 99999}, "password": 42, "rate": "about seven percent", "debug": "maybe", "mode": {"type": "Cloud"}}`)); print("it needs: ${Shape::keys(Settings::schema().shape)}"); 0 } ``` Its output: ```text listening on localhost:Port(8080) at 0.0725, password , debug None, mode Mode::Local refused: listen.hostname is not set listen.port must be between 1 and 65535 password should be text rate should be an exact decimal, and is "about seven percent" debug should be true or false, and is "maybe" mode.type should be one of `Local`, `Remote`, and is "Cloud" it needs: [listen, password, rate, debug, mode] ``` ## See also - [Load application configuration](/docs/v0.4/cookbook/configuration/) — the same schema read out of the environment, with every problem spelled as the variable it came from. - [Build a typed JSON API](/docs/v0.4/cookbook/json-api/) — `std::json`'s own decoders, which are separate from this; `Raw::of_json` is the bridge from a parsed document to a schema. # Load application configuration Source: https://khoralang.com/docs/v0.4/cookbook/configuration/index.md Read every setting at start-up, report every bad one in a single message, and keep the password out of the logs. Write the type once, and `std::config` reads it out of the environment. The alternative is what most services do: stop at the first missing variable, print it, and wait for somebody to redeploy so it can tell them the next one. Five restarts to learn five things it knew the first time. ## Complete example ```khora module main; import std::config::{read, report}; import std::core::{Redacted, Show, Validated, print}; import std::env::{Env}; import std::schema::{Decode, Schema, default, int, string, struct}; derive(Show) type Listen = { host: String, port: Int, }; /// Written by hand, so the host has a default; everything that contains a /// `Listen` finds this through the trait. impl Decode for Listen { fn schema() -> Schema { struct({ host: default(string(), "0.0.0.0"), port: int() }) } } derive(Show, Decode) type Settings = { listen: Listen, db_password: Redacted, }; pub fn main() { with { env: Env::real() } { match read(Settings::schema()) { Validated::Invalid(problems) => print(report(problems)), Validated::Valid(config) => { print(Show::show(config)); serve(config) } } } } fn serve(config: Settings) -> () { print("listening on ${config.listen.host}:${config.listen.port}") } ``` Start it with nothing set and it says everything at once: ```text LISTEN_PORT is not set DB_PASSWORD is not set ``` Start it properly and the printed settings still have no password in them: ```text Settings { listen: Listen { host: 0.0.0.0, port: 8080 }, db_password: } listening on 0.0.0.0:8080 ``` If your `khora.toml` names an `env` list, the variables have to be in it, or you get a third kind of message that points at the manifest instead of at your deployment script: ```toml [permissions] env = ["LISTEN_HOST", "LISTEN_PORT", "DB_PASSWORD"] ``` A manifest with no `env` list grants every variable. Tightening is opt-in and each category is independent — naming `network` says nothing about `env`. ## The declaration is the schema `read` takes any `Schema`, and `derive(Decode)` writes one from the type, so most settings records need nothing but the declaration. The shape decides which variables are read and what they are called: | the type says | the environment holds | | --- | --- | | a field `port` | `PORT` | | a nested record `listen: Listen` with a `port` | `LISTEN_PORT` | | `tags: List` | `TAGS=a,b`, split on commas | | `mode: Mode` with `Mode = \| Local \| Remote(url: String)` | `MODE=Local`, or `MODE=Remote` and `MODE_URL` | | `debug: Option` | `DEBUG`, or nothing | | `password: Redacted` | `PASSWORD`, and it never reaches a log | `variables(Settings::schema().shape)` lists them all, in order, without reading anything — the question a deployment asks, answered without starting the program. ## Why nothing here raises A `raises` stops at the first failure. That is right for a chain where the next step needs the last one's value, and wrong for configuration, where the keys have nothing to do with each other and you want the whole list. So `read` answers `Validated`, and a record with three bad fields reports three. Nothing is lost by that choice: `Validated::to_result` turns the whole list back into a `Result` in one call whenever a caller wants to stop at the first, and a start-up path that is going to exit anyway usually does not. ## A default, and a rule A field with a default, or a rule the declaration cannot say, is a record written by hand with a `struct({ .. })` literal, as `Listen` is above. A default fires on *missing* and on nothing else: ```khora struct({ host: default(string(), "0.0.0.0"), port: between(int(), 1, 65535) }) ``` `LISTEN_HOST` unset gives `0.0.0.0`. `LISTEN_PORT=eigthy` does not quietly become `8080` — a value that is present and wrong is still an error, which is the bug this module exists to catch. A flag takes `true`, `false`, `1` and `0`, and nothing else — `yes`, `on` and `Y` all mean true somewhere, and a reader that accepts all of them accepts a typo as a `false`. ## Three ways a key can be wrong A `Rejection` keeps them apart because they send you to three different files: | | What it means | Where the fix is | | --- | --- | --- | | `Missing` | nobody set it | the deployment script | | `Wrong` or `Refused` | it is set to nonsense | the value | | `Denied` | the manifest does not grant it | `khora.toml` | `report` turns a list of them into one line each, in the order the type declares them, with each path spelled as the variable it came from. That is the string a service prints just before it stops. ## Secrets `Redacted` in the type is a compile-time thing rather than a convention: - `Show` prints ``, so a record holding one still derives `Show` and the start-up log stays useful. - There is no `Encode`, so a record holding one does **not** derive `Encode`. The build stops, which is the right place to stop. - A failure inside one never quotes what it saw: `DB_PASSWORD should be text`, and not what the deployment put there. - `"${password}"` compiles and prints ``, because a hole calls `Show` like anything else. Nothing needs `expose` to print one, which matters: `expose` hands over the secret itself, and it is what somebody reaches for when a hole will not compile. The only way out is `Redacted::expose`, which is a word a reviewer can search for. There is deliberately no `Eq`: comparing two secrets byte by byte is how a timing side channel gets written by somebody who was not writing one. ## Testing it `Env` is a capability, so a test hands `read` a different environment instead of setting one on the machine: ```khora const fake_env = handler for Env { variable: fn name => if name == "LISTEN_PORT" { Option::Some("8080") } else { Option::None }, arguments: fn () => [], }; test "a missing password is reported, not guessed" { let answer = read(Settings::schema()) with { env: fake_env }; assert(!Validated::is_valid(answer)); } ``` Khora's provider is the `Env` handler and it was already swappable; the schema is not there to defer the read, it is there so that the same `Settings` reads a request body and a test fixture without knowing which. See [Testing capabilities](/docs/v0.4/cookbook/testing-capabilities/) for the pattern in full, [Decode untrusted input](/docs/v0.4/cookbook/decoding-input/) for the schema itself, and the [`std::config` reference](/docs/v0.4/stdlib/api/config/) for exact signatures. # Retry a flaky call Source: https://khoralang.com/docs/v0.4/cookbook/retrying/index.md Somebody else's service is down for four seconds. You would like your service not to be. `std::resilience` has the policy (`Schedule`) and the two ways to run one (`retry`, `repeat`). ## Complete example A GET that backs off, gives up after five attempts, and does not waste any of them on a failure that will never come good: ```khora module main; import std::clock::{Clock}; import std::core::{Result, attempt, print}; import std::net::http::{Answer, Call, CallError, HttpClient}; import std::random::{Random}; import std::resilience::{Schedule, retry_while}; fn fetch(url: String) -> Answer with { http: HttpClient } raises CallError { match http.send(Call::get(url)) { Result::Ok(answer) => answer, Result::Err(why) => raise why, } } fn policy() -> Schedule { Schedule::Intersect(Schedule::backoff(100, 30000), Schedule::times(5)) } fn worth_retrying(why: CallError) -> Bool { match why { CallError::Denied(_host) => false, CallError::BadUrl(_url) => false, CallError::Unreachable(_at) => true, CallError::Insecure(_why) => false, CallError::Closed(_why) => true, CallError::Malformed(_why) => false, CallError::TooLarge(_limit) => false, } } pub fn quote(url: String) -> Answer with { http: HttpClient, clock: Clock, random: Random } raises CallError { retry_while(policy(), worth_retrying, fn () => fetch(url)!)! } pub fn main() { with { clock: Clock::real(), random: Random::real(), http: HttpClient::real() } { match attempt(fn () => quote("https://api.example.com/quote")!) { Result::Ok(answer) => print(Int::to_string(answer.status)), Result::Err(_why) => print("the quote service never answered"), } } } ``` The row says what retrying costs: `clock` because it waits, `random` because the schedule is jittered. Neither is hidden, and neither is ambient — which is what makes the test below possible. ## Say what the policy is `Schedule::backoff(100, 30000)` is the one to reach for: wait 100ms, then 200, 400, 800, capped at thirty seconds, each delay scaled by a random 50–100%. That last part is the jitter, and it is not decoration. A thousand clients that all failed at the same instant and all back off by exactly 100ms retry at the same instant, which is the outage again. The pieces underneath are an ordinary ADT, so a policy nobody anticipated is still spellable: | | | | --- | --- | | `Times(n)` | at most `n` attempts, no delay | | `Spaced(millis)` | every `millis`, anchored to the start so it does not drift | | `Exponential(base, factor_pct, cap)` | doubling (at `200`) from the last failure | | `Fibonacci(base)` | grows more gently than doubling | | `Jittered(inner, low_pct, high_pct)` | `inner`, each delay scaled by a random percentage | | `Union(a, b)` | runs while *either* would | | `Intersect(a, b)` | runs while *both* would | | `AndThen(first, then)` | `first` until it stops, then `then` | | `UpTo(inner, millis)` | `inner`, but nothing after `millis` from the beginning | `Intersect(backoff(..), times(5))` is "back off, but no more than five attempts". `AndThen(Times(3), Spaced(60000))` is "try hard three times, then keep trying once a minute" — and no library had to think of that one. `UpTo` is the wall-clock budget, which is usually what somebody means by "give up after a minute": ```khora Schedule::UpTo(Schedule::backoff(100, 30000), 60000) ``` A `Schedule` is a description and holds no clock, so it can be compared, printed, sent to another fiber, and written down in a test. That is the reason it is an ADT rather than the `(attempt, elapsed) -> Option` closure this type usually is: a log line saying `Exponential { base: 100, .. }` is worth more than one saying nothing. ## Do not retry a 404 `retry` tries again on any failure. `retry_while` takes a predicate, and this is where most of the value is: a request denied by `khora.toml`, a malformed URL, a TLS handshake that failed — none of those get better on the second attempt, and retrying them five times just makes the error arrive later. The predicate is a closure over your own error type rather than a `Schedule` case, on purpose. *When to try again* is a policy somebody configures. *Whether this failure is worth trying again* is knowledge about the error. Keeping them apart is what lets `Schedule` stay a plain comparable value. ## Poll with `repeat` `repeat` is the other half: run on the schedule until the body fails, and answer how many runs succeeded. ```khora fn poll_forever() -> Int with { clock: Clock, random: Random, http: HttpClient } { repeat(Schedule::Spaced(30000), fn () => fetch("https://api.example.com/health")!) } ``` "Every thirty seconds until something breaks." The count comes back so a caller can tell "the schedule ended" from "it never ran once". ## Testing it takes no time at all Waiting is an operation on `Clock`, so a test that pins the clock gets a retry loop that runs instantly: ```khora const instant = handler for Clock { unix_seconds: fn () => 0, unix_millis: fn () => 0, monotonic_millis: fn () => 0, sleep: fn _millis => (), }; test "it gives up after five attempts" { let flaky = handler for HttpClient { send: fn _call => Result::Err(CallError::Unreachable("api.example.com:443")), }; let answer = attempt(fn () => quote("https://api.example.com/quote")!) with { clock: instant, random: Random::seeded(1), http: flaky }; assert(Result::is_err(answer)); } ``` Five attempts with a thirty-second cap, and the test finishes in under a millisecond. There is no test runtime, no special mode, and no rule about which fiber may advance time — `sleep` is a capability operation, so a fake clock is four lines. `Random::seeded` pins the jitter the same way, which is the same argument about the other unrepeatable input. See the [`std::resilience` reference](/docs/v0.4/stdlib/api/resilience/) for exact signatures, and [Testing capabilities](/docs/v0.4/cookbook/testing-capabilities/) for the pattern in general. # Test code that uses capabilities Source: https://khoralang.com/docs/v0.4/cookbook/testing-capabilities/index.md A capability requirement is an explicit seam for tests. Instead of mutating process-global state or adding test-only branches to application code, provide a small handler for the capability the function already requires. ## Complete example This session function depends on the current clock. The production program can install `Clock::real()`, while the test installs a deterministic handler: ```khora module session_test; import std::core::{assert}; import std::clock::{Clock}; pub type Session = { user_id: Int, created_at: Int, }; fn create_session(user_id: Int) -> Session with { clock: Clock } { { user_id: user_id, created_at: clock.unix_millis(), } } const fixed_clock = handler for Clock { unix_seconds: fn () => 1700000000, unix_millis: fn () => 1700000000000, monotonic_millis: fn () => 5000, sleep: fn _millis => (), }; test "session uses the supplied clock" { let session = create_session(42) with { clock: fixed_clock, }; assert(session.user_id == 42); assert(session.created_at == 1700000000000); } ``` **Keep the double in the same file as the test, or make it a function.** A `const` has no declared type — its type comes from inferring over its initializer — and the type map that carries a module's exports is built from syntax, before anything is inferred. So nothing records what an exported `const` is, and a file that imports one gets a name with no type behind it. A function has a signature, and a signature is what travels: ```khora // In another module, and usable from anywhere. pub fn fixed_clock() -> Clock { handler for Clock { unix_seconds: fn () => 1700000000, unix_millis: fn () => 1700000000000, monotonic_millis: fn () => 5000, sleep: fn _millis => (), } } ``` Called at the use site — `with { clock: fixed_clock() }` — which also means each test gets its own, rather than sharing one value. Run it with the normal test runner: ```bash khora test . ``` The subject under test did not change for the test. Its contract already says exactly what the test must provide: ```khora fn create_session(user_id: Int) -> Session with { clock: Clock } ``` ## Production wiring uses the same boundary Application startup can install the real clock around the same function: ```khora with { clock: Clock::real() } { let session = create_session(user_id); // ... } ``` There is no global clock singleton and no `if testing` branch inside `create_session`. ## Waiting costs nothing in a test `sleep` is an operation on `Clock`, not an ambient function. That one decision is why the handler above makes a retry loop or a poller run instantly: ```khora sleep: fn _millis => (), ``` Nothing else is involved — no test runtime, no special mode, no rule about which fiber may advance time. Every language with an ambient `sleep` ends up building a parallel clock the runtime knows about, and documentation that opens by warning you to fork the sleeping code or deadlock. Here the capability *is* the seam, which is the same argument `Random::seeded` makes about the other unrepeatable input. The real clock is `Clock::real()`, and it gives a sleeping fiber's worker back for the whole wait — ten thousand sleeping fibers cost ten thousand entries in a heap rather than ten thousand stacks. ## Override one capability in a larger context When application tests use a named context, replace only the dependency relevant to the test: ```khora let session = create_session(42) with Production { clock: fixed_clock, }; ``` The rest of `Production` remains unchanged while the clock binding is overridden for that expression. See [Effects and capabilities](/docs/v0.4/reference/capabilities/) for handler and context syntax, and [Testing and benchmarks](/docs/v0.4/reference/testing/) for `test`, filtering, and CI commands. # Trace an operation Source: https://khoralang.com/docs/v0.4/cookbook/tracing/index.md Wrap an operation in `around(tracer, name, fn () => ...)` and the span is started before it and finished when it returns, raises, or is canceled. The vocabulary is in `std::trace`; exporters and vendor protocols live in packages, so application code programs against `Tracer` regardless of where completed spans are sent. ## Complete example This module implements a small console tracer and uses `around` to guarantee the span is finished with the lifetime of the operation: ```khora module main; import std::core::{Option, print}; import std::random::{Random}; import std::trace::{Context, Span, Status, Tracer, around, current}; fn console_tracer(random: Random) -> Tracer { handler for Tracer { start: fn (name, _attributes) => { let id = random.int(); let span = match current() { // Inside a span already: keep its trace and record it as the parent. Option::Some(inside) => { context: { trace_high: inside.trace_high, trace_low: inside.trace_low, span: id, sampled: inside.sampled }, parent: inside.span, name: name, }, // At the top: begin a new trace. Option::None => { context: { trace_high: random.int(), trace_low: random.int(), span: id, sampled: true }, parent: 0, name: name, }, }; print("start ${name}: trace=${Context::trace_id(span.context)} span=${Context::span_id(span.context)}"); span }, finish: fn (span, status) => { match status { Status::Ok => print("finish span: ${span.name}"), Status::Failed(reason) => print("fail span ${span.name}: ${reason}"), } }, event: fn (span, name, _attributes) => print("event ${span.name}: ${name}"), } } fn calculate(tracer: Tracer) -> Int { print("doing work"); around(tracer, "inner", fn () => 42) } pub fn main() { let tracer = console_tracer(Random::real()); let result = around(tracer, "calculate", fn () => calculate(tracer)); print("result = ${result}"); } ``` ```text start calculate: trace=bf0d815f869cef498398871c54234801 span=6fdab51de189b88d doing work start inner: trace=bf0d815f869cef498398871c54234801 span=d52ddfe412d2d513 finish span: inner finish span: calculate result = 42 ``` The application decides which tracer implementation to construct. `around` owns the span lifetime: ```khora let result = around(tracer, "calculate", fn () => calculate(tracer)); ``` It starts the span before running `calculate` and registers cleanup so the span is finished when the operation returns, raises, or is canceled. A caller should not rely on a later `tracer.finish(...)` line running after arbitrary fallible work. ## `start` has to ask what it is inside The one thing a handler must do is call [`current`](/docs/v0.4/stdlib/api/trace/) in `start`, and it is why `console_tracer` takes a `Random`: a span needs an id nobody else has, and a handler may be handed to another fiber, so it cannot count with a `mut` field. Capturing a capability is what a handler is allowed to do. A `start` that instead writes ```khora { context: Context::none(), parent: 0, name: name } ``` compiles and produces no trace. `Context::none()` is an all-zero context, so `current()` inside the body still answers `None`, every span is an unparented root, the two spans above land in different traces, and — because [`Log`](/docs/v0.4/cookbook/logging/#correlating-with-a-trace) reads `current()` to decide whether to write `trace_id` and `span_id` — no log line carries any ids either. `Context::none()` is for a *context*, such as an absent or malformed incoming header; it is not a starting point for a span. The parent shows the same thing. Zero is how `Span::parent` says "root", so a nested `around` that writes `parent: 0` starts a second trace rather than a child span. ## Report `Result` failures on the span When an operation reports its domain failure as `Result` and `E: Show`, use `around_result` instead of manually inspecting the result only for tracing: ```khora let result = around_result( tracer, "load user", fn () => repository.load(user_id), ); ``` `around_result` finishes successful results with `Status::Ok` and renders an `Err` into `Status::Failed`. Cancellation and raised failures still use the structured cleanup path. ## Trace context at an HTTP boundary `Context` understands the W3C `traceparent` representation. An HTTP boundary can parse an incoming header without accepting malformed partial context: ```khora let incoming = match Request::header(request, "traceparent") { Option::None => Context::none(), Option::Some(header) => match Context::of_traceparent(header) { Option::None => Context::none(), Option::Some(context) => context, }, }; ``` A valid context can be rendered for an outgoing request with: ```khora let header = Context::to_traceparent(incoming); ``` The tracing model is designed so context associated with structured fiber work survives suspension and scheduler movement rather than depending on an OS-thread-local variable. ## Disabled tracing Use the shipped no-op tracer when tracing is intentionally disabled: ```khora let tracer = Tracer::none(); ``` For exact `Tracer`, `Span`, `Context`, `Attribute`, `Status`, `around`, and `around_result` declarations, see the [tracing API reference](/docs/v0.4/stdlib/api/trace/). For cancellation-safe cleanup generally, see [Cancellation-safe resources](/docs/v0.4/cookbook/cancellation-safe-resources/). # Log, and correlate it with traces Source: https://khoralang.com/docs/v0.4/cookbook/logging/index.md A log line is read by a program before it is read by a person: it goes to a collector, gets indexed, and is queried months later by somebody who was not there. So `std::log` emits one JSON object per line, on standard error. ```khora module orders::main; import std::log::{Severity, Log, info, warn}; fn charge(amount: Int) -> Int with { log: Log } { info("charging"); if amount > 1000 { warn("unusually large") } else { () }; amount } pub fn main() -> Int { with { log: Log::json(Severity::Info) } { charge(1200) } } ``` ```text {"timestamp":1757021376817,"level":"info","message":"charging"} {"timestamp":1757021376819,"level":"warn","message":"unusually large"} ``` ## Why it is a capability Because everything else that reaches outside is. A function that logs writes to a stream somebody else owns, and Khora's argument throughout is that such a function says so: ```khora fn charge(account: Account, amount: Decimal) -> Receipt with { log: Log, db: Db } raises DbError ``` That buys three things. A test installs a logger that collects into a list and asserts on what was said, with no global to reset between cases. A library cannot log without its caller knowing, because the row would say so. And the caller chooses the format and the destination, which is a decision no library should make for the program embedding it. `print` is not a capability. It predates the argument rather than refuting it, and its own documentation says so. ## Structure beats formatting Anything worth searching for later goes in a field rather than in the sentence: ```khora import std::trace::{number, text}; fn charge(account: Account, amount: Int) -> () with { log: Log } { log.record(Severity::Error, "charge failed", [ text("account", account.id), number("amount_minor", amount), ]); } ``` ```text {"timestamp":1757021376823,"level":"error","message":"charge failed","account":"acct_19","amount_minor":1200} ``` A message that interpolates the account id reads the same to a person and is useless to a collector, because every line is a distinct string. A field is what an index can group by. Attributes are `std::trace`'s `Attribute`, deliberately: the same vocabulary annotates a span, so a value does not change shape depending on which of the two you send it to. ## Correlating with a trace Nothing to do. A line logged inside a span carries that span's ids: ```khora import std::trace::{Tracer, around}; fn work() -> () with { log: Log } { info("processing"); } // `tracer` is any `Tracer` — see [Tracing](/docs/v0.4/cookbook/tracing/). around(tracer, "request", fn () => work()) ``` ```text {"timestamp":1757021376823,"level":"info","message":"processing","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7"} ``` `work` says nothing about tracing, takes no span, and is not passed the tracer. The handler asks [`current`](/docs/v0.4/stdlib/api/trace/) what the fiber is inside and writes the ids if there is an answer — and writes neither field if there is not, rather than thirty-two zeros a collector would group every unparented line under. The field names are the ones OpenTelemetry expects, so a collector shows the lines beside the span without being configured to. **This works across a spawn**, which is the case it exists for: a fiber inherits its spawner's current span when it is created, so a request that fans out into four fibers is one trace. It inherits a copy — a span the child opens is the child's, and does not become the parent's when the child returns. A line logged outside every span carries no ids, which is correct and is why the field is absent rather than empty. ## Choosing a level Five, in the order everybody already filters on: | Severity | For | | --- | --- | | `Trace` | The finest detail, off outside a debugging session. | | `Debug` | What a developer wants while working on this code. | | `Info` | What an operator wants while the program is behaving. | | `Warn` | Something is wrong and the program is carrying on. | | `Error` | Something is wrong and something did not happen. | **The handler drops what is below the minimum, not the caller.** A caller that checked the level itself would need to know the configuration, which is exactly what the capability keeps away from it. Take the minimum from the environment where an operator can change it: ```khora import std::core::{Option}; import std::env::{Env, variable_or}; let wanted = Severity::of_name(variable_or("LOG_LEVEL", "info")!); let minimum = match wanted { Option::Some(level) => level, Option::None => Severity::Info }; ``` ## Testing what a function logged A logger is a value, so a test installs one that remembers: ```khora test "a large charge is warned about" { let said = Shared::of(List::Nil); let collecting = handler for Log { record: fn (level, message, _attributes) => Shared::update(said, fn lines => List::Cons(message, lines)), }; with { log: collecting } { charge(1200) }; assert(List::length(Shared::get(said)) == 2); } ``` No global to reset, no capture to install, and two tests running at once cannot see each other's lines — which is the whole reason this is an effect rather than a function that writes to a stream. ## When you want a terminal, not a collector `Log::plain` writes `LEVEL message key=value` instead, for a small program whose log is read by whoever ran it: ```text INFO charging WARN unusually large ``` Anything that ships its logs anywhere wants `Log::json`. ## Timestamps and testing `Log::json` stamps from the real clock. `Log::json_using` takes one instead, which is what makes a log line assertable: a fixed clock produces a byte-for-byte predictable object, and a line with a real timestamp in it can only be matched with a regular expression — which is a test of the regular expression. ```khora let fixed = handler for Clock { unix_seconds: fn () => 1757021376, unix_millis: fn () => 1757021376817, monotonic_millis: fn () => 0, sleep: fn _ms => (), }; with { log: Log::json_using(Severity::Info, fixed) } { work() } ``` ## Just writing to standard error For a usage message, before any capability exists to install: ```khora import std::log::{eprint}; eprint("usage: report "); ``` `print` writes a program's *answer*, and an answer is what `> out.txt` is for. A diagnostic written there goes into the file with it, which is how a command-line tool ends up silently discarding the reason it failed. # Give work a deadline (timeouts and cancellation) Source: https://khoralang.com/docs/v0.4/cookbook/timeouts-and-cancellation/index.md There is no `timeout` function in the standard library. A deadline is built from two things you already have: a fiber, and a clock. ```khora module deadline::main; import std::core::{print, Fiber, Nursery, nursery}; import std::clock::{Clock}; pub type Timeout = | TookTooLong; /// The work. It never finishes on its own. fn worker() -> () with { clock: Clock } raises Timeout { loop { clock.sleep(50)!; } } fn fan_out() -> () with { nursery: Nursery, clock: Clock } { nursery.adopt(Fiber::spawn(fn () => worker()!)); print(" worker adopted"); } /// Runs the fan-out in its own fiber, and gives up on it after `millis`. fn with_deadline(millis: Int) -> () with { clock: Clock } raises Timeout { let hand = Fiber::spawn(fn () => { nursery(fn () => fan_out())!; () }); clock.sleep(millis)!; print(" deadline reached; canceling"); Fiber::cancel(hand); Fiber::wait(hand)! catch { _ => () }; print(" canceled, and the wait returned"); raise Timeout::TookTooLong } pub fn main() -> Int { with { clock: Clock::real() } { print("starting work with a 300ms deadline"); with_deadline(300)! catch { Timeout::TookTooLong => print("gave up: took too long") }; }; 0 } ``` ```text starting work with a 300ms deadline worker adopted deadline reached; canceling canceled, and the wait returned gave up: took too long ``` The whole program takes 308 ms for a 300 ms deadline. The shape is: put the work in a fiber, sleep for the deadline, cancel the handle, `wait` for it, then raise. `wait` rather than `join`, because a canceled fiber has no answer to take. ### When the work finishing early has to be fast **That recipe always takes the whole deadline.** The timing fiber sleeps the full interval whether the work finished in 2 ms or not at all, which is right when the deadline is a budget you intend to spend, and wrong for a call that usually returns immediately — a five-second guard on a health check would make every healthy check take five seconds. Poll instead, when the common case is early completion: ```khora let hand = Fiber::spawn(fn () => work()); let started = clock.monotonic_millis(); let mut done = false; let mut expired = false; while !done && !expired { if Fiber::finished(hand) { done = true; } else if clock.monotonic_millis() - started > millis { expired = true; } else { clock.sleep(25)!; } }; if expired { Fiber::cancel(hand); // **Detach as well as cancel.** A fiber blocked in a call with no // cancellation point -- a connect to an unroutable host, say -- is not // freed by the cancel, and releasing its handle waits for it. `detach` // says the program is no longer interested, so its own exit is not held // up by work it has already given up on. Fiber::detach(hand); raise Timeout } else { Fiber::join(hand)! } ``` The poll interval is the cost: 25 ms means up to 25 ms of latency added to a fast answer, against a deadline that would otherwise cost its whole length. ## Any work can be interrupted A canceled fiber stops at its next cancellation point: a loop going round, a call to a function that has one, or a blocking operation such as a sleep, a channel receive or a wait. Every function has them, whatever its `raises` row, so a lambda with no row is stopped as promptly as a named function: ```khora nursery.adopt(Fiber::spawn(fn () => { let mut n = 0; loop { n = n + 1; } })); ``` Canceling the fiber holding that nursery stops this loop at its next trip. What is not interrupted is a single call into foreign (C) code or a file-system call already in progress, and a blocking connect or a wait for a child process: the fiber finishes the call and stops at the next cancellation point after it. A connect to an unroutable host is the common case, which is why the polling recipe above detaches as well as cancels. ## Canceling a nursery cancels its children A fiber whose body is a `nursery` takes its children with it. Canceling the parent cancels every child it is holding, including the ones it is currently waiting on, so a fan-out under a deadline stops rather than waiting for workers nobody told. This is what makes the example above work: `with_deadline` cancels one handle, and the worker inside the nursery stops. Canceling through a nursery costs what canceling without one costs — measured at 106 ms against 107 ms for the same worker spawned directly. ## What a canceled fiber does on the way out Cancellation is not a kill. The fiber unwinds the way a failure does: `scoped` resources are released, `Region` finalizers run, and a nursery in the unwind path stops its own children first. [Cancellation-safe resources](/docs/v0.4/cookbook/cancellation-safe-resources/) has the resource rules. A canceled fiber's answer is discarded. `Fiber::join` on one unwinds the joiner along with it, which is why every example here uses `wait`. ## What this does not give you - **No `race` and no `select`.** Waiting on the first of several fibers has to be built from a channel the workers send to. - **A deadline is a sleep, not a scheduler deadline.** The timing fiber sleeps the whole interval; it does not wake early because the work finished. For a deadline that ends as soon as either side does, have the work signal a channel and read the channel with its own timeout fiber. - **A canceled fiber is not stopped at an arbitrary instruction.** It stops at its next cancellation point, which is why a worker that spends 200 ms inside one infallible call takes up to 200 ms to notice. - **`Router::listen` can be canceled**, and the process exits when it is. This was a known gap and is fixed; see [Concurrency](/docs/v0.4/reference/concurrency/) — drain first, stop last. # Internals Source: https://khoralang.com/docs/v0.4/internals/index.md How Khora works underneath: what a reference count costs, how a handler is reached, what a fiber is made of, why a build is reproducible. Nothing here is needed to write Khora. The [Language Reference](/docs/v0.4/reference/) and the [Cookbook](/docs/v0.4/cookbook/) describe the language as you use it, and mention the machinery only where it changes what you would write. This section is for when that is not enough — when you are debugging something strange, judging whether Khora suits a problem, or simply want to know what the words mean. ## What is here **[Memory](/docs/v0.4/internals/memory/)** — reference counting without a tracing collector, what the compiler removes, when storage is reused in place, and the one case that leaks. **[Effects and handlers](/docs/v0.4/internals/effects/)** — what `with` compiles to, why a capability costs a parameter rather than a lookup, and how a failure leaves a function. **[Fibers](/docs/v0.4/internals/fibers/)** — what runs a fiber, what suspending costs, and what a nursery guarantees. **[The build](/docs/v0.4/internals/build/)** — from source to a native executable: what is cached, what makes a release build reproducible, and why a linker is the one thing Khora cannot bring with it. ## What is not here Decisions and their alternatives. This section says how Khora works, not why it was built one way rather than another — that argument lives in the repository's design notes, which are written for somebody changing the compiler. The distinction matters for reading: everything here describes the language as it is now. If a page mentions something Khora does not do, it is a limit that is still true, not a stage it passed through. # Memory Source: https://khoralang.com/docs/v0.4/internals/memory/index.md Khora has no tracing garbage collector and no lifetimes to write down. Memory is managed by reference counting, and most of the counting is removed before the program runs. ## Reference counting, mostly at compile time Every heap object carries a count of how many things refer to it. When the count reaches zero the object is freed immediately, at a point you could predict from the source. There is no collector pass and no pause. Counting every reference at run time would be slow, so the compiler does most of the work first. It tracks ownership through each function and emits a retain or release only where it cannot prove one is unnecessary: a value created and consumed in one function usually costs nothing at all, because the compiler can see the whole story and there is nothing to count. This is [Perceus](https://www.microsoft.com/en-us/research/publication/perceus-garbage-free-reference-counting-with-reuse/) reference counting, from Microsoft Research, which is also where Khora's effect system comes from. ### Reuse in place The compiler also knows when a value is the last reference to its storage. When such a value is released and an object of the same shape is allocated immediately afterwards, the allocation reuses the storage rather than asking the allocator for more. The visible effect is that the obvious way to write a transformation is often the efficient one. Mapping over a list whose elements you own does not have to allocate a second list; the cells are rewritten as the walk goes. What you can rely on: - **Reuse never changes behavior.** If the optimization cannot be applied the program allocates instead, and nothing observable differs. - **You cannot detect it from inside the program**, other than by counting allocations. ## The counts are atomic A fiber can run on any worker thread, and a spawned fiber shares at least the closure it was handed — so the counts have to be safe to change from two threads at once. They are. This costs an atomic instruction on each retain and release that survives to run time. There is no cheaper single-threaded variant; the cost is the same in every program. ## Cycles leak Reference counting cannot free a cycle: each object in the loop is referred to by another, so no count reaches zero. Khora has mutable fields, so a cycle is constructible: ```khora pub type Node = { name: String, mut next: Option }; let a: Node = { name: "a", next: Option::None }; let b: Node = { name: "b", next: Option::None }; a.next = Option::Some(b); b.next = Option::Some(a); ``` Those four objects are never freed. **This is a leak, not unsoundness.** Nothing is freed early, nothing is read after being freed, and the program stays correct — the memory is simply never returned. It is also hard to do by accident. Three properties of the language make the reference graph acyclic unless you deliberately write a cycle: - **Values are built bottom-up.** A constructor's arguments are evaluated before the object exists, so a new object can only point at older ones. - **Closures capture by value** at the moment they are created. - **A `let` initializer cannot see its own binding.** `let x = f(x)` refers to an outer `x`, not the one being declared. So a cycle requires assigning through a `mut` field, into an object that already reaches back. Parent pointers and doubly-linked structures are the shapes to watch. Khora has no weak reference today, which is the usual way to break a cycle deliberately. Structures that need one have to be built with an index or a key instead of a direct reference. ## What crosses a fiber A value captured by a spawned fiber must be shareable. Mutable state is not: > A record with a mutable field cannot be captured by a spawned fiber, and nor > can anything holding one. The rule is structural and transitive, and the compiler checks it at the one place a value can cross — the captures of the closure handed to `spawn`. There are no references in Khora, so nothing escapes any other way, and one check covers the whole language. To share state deliberately, use `Shared`, which takes a lock and is safe to capture. Where another language needs `Arc>>`, Khora has `Shared>`: the counting is implicit and there is no lifetime to name. [Sharing](/docs/v0.4/reference/sharing/) has the rules as they apply to code you are writing. ## Resources are not memory Memory is reclaimed when the last reference goes. A file handle, a socket or a transaction has an *observable* end — the other side notices — so those are closed by [regions](/docs/v0.4/reference/memory-and-resources/#region-syntax) rather than by the reference count, at a point the program states rather than one the optimizer chooses. # Effects and handlers Source: https://khoralang.com/docs/v0.4/internals/effects/index.md A Khora signature says what a function needs and how it can fail: ```khora pub fn analyze(id: String) -> Report with { ledger: Ledger } raises DbError ``` Both rows are checked at compile time, and both compile to something ordinary. There is no effect interpreter, no handler stack to search, and no unwinder. ## A capability is an argument A capability row is static: the compiler knows at every call site exactly which handlers a function needs, because the signature says so and the checker has verified it. So the handler is not looked up. It is passed in. The function above compiles to one taking an extra argument — the `Ledger` handler, which is a record of closures and therefore an ordinary heap value. Each call site supplies it from its own capability parameter, or from the enclosing `with` block. Performing an operation is then a field read and a call through a function value. That is the same cost as calling any closure. ```khora with { ledger: Ledger::real() } { analyze(id) // `ledger` is handed to `analyze` as an argument } ``` **Nothing is searched at run time.** There is no stack of installed handlers, no walking of frames to find the innermost one, and no cost that grows with nesting depth. Row polymorphism (`'ef`) specializes the same way generics do. Khora monomorphizes the whole program, so a row variable is concrete at every call site reachable from `main` for the same reason a type variable is. ## A failure is a tagged return A function whose `raises` row is non-empty returns two words: a tag and a payload. The tag is `0` when the call returned normally, and otherwise identifies the *type* of the error that was raised. At each `!` the compiler emits a branch. On the error path it releases what that frame owns and returns the tag upward, which is the same machinery an early `return` already uses. This has consequences worth knowing: **There is no unwinder.** No DWARF tables, no landing pads, no personality routine, no `longjmp`. A raise is a return with a tag, and each frame it passes through runs the releases it was going to run anyway. That is what keeps foreign frames out of the story — see [FFI](/docs/v0.4/reference/ffi/). **`!` is where the branch is.** The mark a reader is taught to read as "control can leave here" is exactly the point where the compiler emits the test. **The tag names the error's type, not just "something failed".** `catch` handles part of a row, so a function raising `DbError + ModelError` whose caller catches only `ModelError` has to know at run time which arrived. A variant index cannot answer that — `DbError::Timeout` and `ModelError::RateLimited` are both variant zero of their own types — so the tag is a program-wide id for the type, assigned once by the whole-program compiler. An error carries the same tag through every frame it crosses, whatever the rows in between look like. The payload is one word because every Khora value is word-sized. Two registers in total, which is what a bare success/failure bit would have cost anyway. ## Handlers do not suspend A handler runs, returns, and control continues. It cannot capture the rest of the computation and resume it later, which is what a first-class continuation would allow. Handlers are therefore single-shot, and the generator-shaped uses of algebraic effects are unavailable: an operation cannot yield more than once, and a handler cannot restart a computation. Stopping and continuing a computation is what a *fiber* does — see [Fibers](/docs/v0.4/internals/fibers/). Because a handler never suspends, performing an operation costs a call rather than a stack switch, and a handler can be passed to foreign code without the foreign frames becoming part of anybody's control flow. ## `with` and `raises` are one mechanism Both are rows. Both are settled at compile time. Both are checked at every call and reported at the call rather than at the definition. They compile to different things because they *do* different things: a capability is called and returns, so it is an argument; a failure leaves and does not, so it is a tag on the return. Keeping them separate in the syntax is what makes that difference legible. # Fibers Source: https://khoralang.com/docs/v0.4/internals/fibers/index.md A fiber is a Khora computation that can suspend — waiting on a socket, a timer, a channel — and continue later, while other fibers run. ## What a fiber is made of Khora ships two implementations of the same interface, and picks one at run time. **Threads, by default.** Each fiber is an operating-system thread. Suspending is the thread blocking; waking is the operating system scheduling it. The kernel does the work, the stacks are the kernel's, and a blocking foreign call blocks only the fiber that made it. **A scheduler, with `KHORA_FIBERS=scheduler`.** Each fiber is a stackful coroutine on a pool of worker threads, with work stealing. Suspending is a stack switch in user space, and a fiber can move between workers. They behave identically except under cancellation: a fiber inside `clock.sleep` is woken by a cancellation under the scheduler, but runs the sleep to completion under threads. [Known limitations](/docs/v0.4/limitations/#the-two-fiber-backends-are-distinguishable) has the measurements. Threads are the default because they are faster at the connection counts a service actually runs at; the scheduler exists for programs that need far more concurrent fibers than a machine has threads. [Known limitations](/docs/v0.4/limitations/#the-fiber-scheduler) has the measurements. The remaining difference is cost and density: | | thread | coroutine | | --- | --- | --- | | stack | 8 MB, reserved and committed as it is touched | 8 MB with a guard page, the same | | suspend | a kernel transition | a stack switch | | how many | thousands | hundreds of thousands | ### What the scheduler does Each worker has its own queue and takes from the front. A worker with nothing to do steals half of another worker's queue from the back, so the owner and the thief contend for the lock but never for the same task. Every so often a worker checks a shared queue first, so a fiber woken from outside the pool — by the reactor, or by another thread — is not left waiting behind a worker's own work. A fiber woken this way can resume on a different worker than it started on. Nothing a fiber owns is tied to a thread: its identity, its cancellation state and its current span travel with it. The one exception is foreign code that is itself thread-affine, which [FFI](/docs/v0.4/reference/ffi/) covers. ## A nursery is a region What makes Khora's concurrency *structured* is that a fiber cannot outlive the block that started it. That is not a separate mechanism — it is [regions](/docs/v0.4/reference/memory-and-resources/#region-syntax), which already run their finalizers on every way out of a block. A nursery opens a region and installs a `Nursery` capability whose `spawn` registers each fiber with it. Every path out of the block runs the finalizers, and those wait for the children: running off the end, an early `return`, a raise passing through, a cancellation. There is nothing extra to enforce, and no way to write a fiber that escapes: the `Nursery` capability is in scope only inside the block, and the block cannot end while a child is still running. ```khora nursery(fn () => { nursery.spawn(fn () => serve(first)); nursery.spawn(fn () => serve(second)); }) // both have finished by here, whichever way the block ended ``` ### Ending normally and ending badly A nursery waits for its children on the way out, and cancels them first if it is leaving because something failed. The distinction is not something the program asks about: cancellation is idempotent, so "cancel then wait" is correct on both paths and the normal one simply has nothing to cancel. ## Cancellation is cooperative, at cancellation points Canceling a fiber does not stop it where it stands. It sets a flag, and the fiber notices at its next cancellation point. They are: - a `!` site, which is also where propagation and suspension are marked; - a loop back-edge — the point where `loop` or `while` goes round again; - a call to a function that can itself reach a cancellation point, and the entry of a function that recurses; and - a blocking operation: channel sends and receives, waiting on or joining a fiber, `clock.sleep`, and socket accepts, reads and writes. Every function has them, whatever its `raises` row. A function that can reach a cancellation point returns a small tag beside its answer, whether or not it can fail, and a canceled fiber unwinds on that tag the same way a raise unwinds, running each frame's releases and each region's finalizers as it goes. A function that reaches none — straight-line arithmetic, a field read — is compiled without the tag, because there is nothing in it to stop at. This is why a canceled program closes its files. The back-edge is why the ordinary shape of a periodic job can be stopped at all: ```khora fn ticker() with { clock: Clock } { loop { clock.sleep(200); } } ``` There is no `!` and no `raises` row in that body. Without the back-edge, a nursery that had to unwind past it would wait for ever. A cancellation is not an error: no `catch` sees one, `_` arm included. It passes through and carries on unwinding. ## Suspending is not a handler's job A handler runs and returns; it cannot capture the rest of the computation. All suspension belongs to fibers, which is what lets handlers be a function call rather than a stack switch — see [Effects and handlers](/docs/v0.4/internals/effects/). The practical consequence is that `with` blocks and nurseries compose without either knowing about the other, and a capability handed to a fiber is just a value it captured. # The build Source: https://khoralang.com/docs/v0.4/internals/build/index.md `khora build` takes a package from source to a native executable in one process: parsing, name resolution, type inference, exhaustiveness checking, whole-program monomorphization, reference-count planning, LLVM, and a link. ## Whole-program, always Khora monomorphizes the entire program. A generic function is compiled once per concrete set of type arguments, and so is a row variable — `'ef` and `'er` specialize at every call site reachable from `main`, for the same reason a type variable does. This is why performing an effect costs a function call rather than a lookup, and why an error's tag can be a program-wide id assigned once. It is also the trade: compile time grows with the program, and there is no separate compilation of a generic across package boundaries. ## LLVM is inside the compiler; the linker is not The `khora` binary carries LLVM within it, and finds `std/` and the runtime archive beside itself. Nothing needs configuring to compile. Linking is different. Turning an object file into an executable needs the platform's C runtime and system libraries, and the driver that knows where those live belongs to the platform — so Khora calls `clang` or `gcc`. That is the one thing the toolchain cannot bring with it, and `rustc` has the same requirement for the same reason. ## Two profiles | | `debug` (default) | `release` | | --- | --- | --- | | optimization | none | LLVM's `default` | | debug information | yes | no | | reproducible | no | **bit for bit** | `release` drops debug information, which is one of the things that makes it reproducible: a debug build embeds each source file's absolute path, so two checkouts of identical content produce different bytes. ### What "reproducible" means here Two `--release` builds of the same source, with the same toolchain, produce byte-identical object files *and* byte-identical executables — on the same machine or on two different ones. Getting there means removing everything that varies with something other than the input: - **Windows** — a PE header carries a timestamp, so the linker is told to write a hash of the content instead. - **macOS** — a Mach-O image carries a UUID, and an `arm64` image is ad-hoc code signed; both are derived partly from the *output file's name*. The link writes to a fixed name and the result is moved into place, so neither depends on what the caller chose to call it. - **Everywhere** — no absolute paths, which is what the debug-information trade buys. ## The build cache A build is cached under a key covering everything that can change the output: | in the key | why | | --- | --- | | every source file's contents | the obvious half | | the compiler binary, hashed | not its version string — a version is constant across every development build | | the linker binary | Khora emits an object and a C driver links it, so the driver's bytes are in the output's | | the runtime archive | every executable links it statically | | the target triple | a different target produces different code | | the profile, and whether debug information is on | the environment can override the profile in both directions | | executable or library | the two emit different artifacts | | the source *paths*, when debug information is on | because a debug build embeds them | Fields are length-prefixed before hashing, so two adjacent ones cannot be run together into a different key with the same bytes. Sources are sorted by content rather than by path, so the key does not depend on where the checkout lives. **Hashing the compiler and the linker is the unusual part**, and it is what makes a hit provable rather than probable. A cache keyed only on sources is a bet that nothing else changed; the way that bet is normally lost is a toolchain difference nobody hashed, and the symptom is an artifact that is subtly wrong with no failure anywhere. Because `release` is reproducible, a cache hit is not merely "an artifact built from the same inputs" — it is the same bytes a fresh build would have produced, and the project's own gate checks that by building both ways and comparing. A cache miss is never a build failure: a corrupt or unreadable entry is treated as absent and the build proceeds. # Deployment Source: https://khoralang.com/docs/v0.4/deployment/index.md Start with [Supported targets](/docs/v0.4/deployment/supported-targets/) to see which platforms are actually supported by the current toolchain. The platform pages then cover how to build, package, and run Khora programs on those targets. A target is only described as supported when the compiler, runtime, linker/sysroot, packaging, and deployment path work end to end. Compiler code generation by itself does not make a platform deployable. ## What you deploy One file. `khora build` links a native executable that carries the Khora runtime inside it — no VM, no tracing garbage collector, no `std/` beside it, and no toolchain on the deployment host. The release workflow proves this on every release: it unpacks the packaged toolchain somewhere else, builds a program with it, and then runs the produced binary directly rather than through `khora run`, because what has to hold is that the *binary* stands on its own (`.github/workflows/release.yml`). What it still expects from the host is the platform C library it was linked against, and the machine's certificate trust store if it opens a TLS client connection ([`std::net::tls`](/docs/v0.4/stdlib/api/net/tls/) verifies against it). [Supported targets](/docs/v0.4/deployment/supported-targets/) says which triples are built and what each expects. ## The build, end to end ```bash khora fmt --check . # formatting, as a gate rather than a rewrite khora check . # types, exhaustiveness, capabilities — no code generated khora test . # the package's tests, one fiber each khora build . --release # optimized, no debug information, reproducible ``` `khora build` writes `build/` — `build/.exe` on Windows — next to your `khora.toml` rather than among your sources. `--out` puts it somewhere else under a name you choose: ```bash khora build . --release --out dist/myservice ``` **Release is not the default profile.** Plain `khora build` is unoptimized and carries debug information, which is what a crash you are about to read wants; `--release` runs LLVM's `default`, drops debug information, and is bit-for-bit reproducible. `KHORA_PROFILE=release` says the same thing to the commands that have no flag of their own, `khora test` and `khora bench`. If the project has dependencies, fetch and lock them first — this is the command to run after cloning, and it needs `git` on the build machine, because a `git` dependency is fetched by shelling out to it: ```bash khora install ``` A bill of materials for what that resolved, as CycloneDX 1.5 JSON, and a pure function of `khora.toml` and `khora.lock` so two runs over unchanged input produce identical bytes: ```bash khora sbom . --out myservice.cdx.json ``` ## Where to go next For native services, see [Linux](/docs/v0.4/deployment/linux/) and [Containers](/docs/v0.4/deployment/containers/). For WebAssembly hosts such as Cloudflare Workers, read [Cloudflare Workers](/docs/v0.4/deployment/cloudflare/) before designing around host-specific APIs: it is not a target of this release. # Supported targets Source: https://khoralang.com/docs/v0.4/deployment/supported-targets/index.md A platform is in one of three states here, and the words mean this: - **supported** — the toolchain builds, links, tests, and releases artifacts for the target; - **experimental** — important pieces work, but production support is not yet promised; - **emission-only** — LLVM can emit the object/module format, but the runtime/linker/deployment path is incomplete. The website must never describe emission-only support as deployable platform support. ## What is supported Three triples, and they are exactly the ones the release workflow builds, packages, and then uses to compile a program before publishing: | triple | state | | | --- | --- | --- | | `x86_64-unknown-linux-gnu` | supported | on glibc 2.39 or newer — [see below](#the-linux-build-needs-glibc-239) | | `x86_64-pc-windows-msvc` | supported | | | `aarch64-apple-darwin` | supported | | A release is not published unless each of them has produced an artifact, unpacked it somewhere else, and built and run a program with it. That test is the reason this list is short: it is what has actually been done, not what is expected to work. **Everything else is out of scope for this release**, which is a statement about what is promised rather than a prediction about what would happen if you tried: - **Linux arm64** is not built or tested by CI, so it is not listed. The compiler may well produce a working binary there; nobody has checked, and an unchecked platform in a supported table is the claim this page exists to prevent. - **Cross-compilation** — building a Linux artifact on a Mac, say — is not supported. The compiler can emit for another target, and `KHORA_TARGET` will verify its code generation, but the linker and sysroot story is unfinished, so a deployable artifact still means building on the platform it runs on. - **Static and musl builds** are not produced or tested. The published Linux artifact is dynamically linked against the system C library, which is what [Containers](/docs/v0.4/deployment/containers/) assumes. - **WebAssembly, and Cloudflare Workers with it**, are not a target of this release. See below. ## The Linux build needs glibc 2.39 **`x86_64-unknown-linux-gnu` means a recent one.** The published Linux toolchain is compiled on GitHub's `ubuntu-latest` runner, which is Ubuntu 24.04, and a glibc program carries the symbol versions of the machine that built it. The published binary asks for `GLIBC_2.39` and will not start against anything older — not with a degraded feature, but with `libc.so.6: version 'GLIBC_2.39' not found` on every command. | distribution | glibc | the published toolchain | | --- | --- | --- | | Ubuntu 24.04 LTS | 2.39 | runs | | Debian 13 (trixie) | 2.41 | runs | | Debian 12 (bookworm), current stable | 2.36 | does not run | | Ubuntu 22.04 LTS | 2.35 | does not run | | RHEL 9 and rebuilds | 2.34 | does not run | | Alpine, and anything on musl | not glibc | does not run | That table is the honest reading of the release rule at the bottom of this page, applied to a detail the rule did not previously ask about: what a fresh CI environment produced was tested on the environment that produced it. Debian stable and a supported Ubuntu LTS are on the wrong side of the line, so the requirement is stated here rather than discovered by somebody installing. `install.sh` refuses before downloading on a system below the floor, and stops with the error rather than reporting success if the unpacked binary cannot run anyway. `scripts/check-install.sh` is what holds the release to that: it installs from the published archive inside each of those images and checks that the toolchain and the installer agree. **Raising the floor is a release-workflow change, not a documentation one.** Building the Linux artifact on an older base image would lower the number; until that is done and checked, the number here is what the release actually is. What this page will not do is list a target as supported and leave the constraint that makes it unusable on half of today's Linux machines to be found at runtime. Nothing here applies to the programs *you* compile. A Khora executable links against the machine that built it, so its floor is that machine's glibc — which is why [Containers](/docs/v0.4/deployment/containers/) builds and runs in the same base image. ## The artifacts, and checking one A published stable release provides one archive per triple and a checksum beside it, named for the tag. These 0.4.0 URLs work only after the stable release is published; for a published release candidate, replace both `0.4.0` strings with its full version, such as `0.4.0-rc.2`: ```text https://github.com/codyspate/khoralang/releases/download/v0.4.0/khora-0.4.0-x86_64-unknown-linux-gnu.tar.gz https://github.com/codyspate/khoralang/releases/download/v0.4.0/khora-0.4.0-x86_64-unknown-linux-gnu.tar.gz.sha256 ``` `install.sh` and `install.ps1` fetch exactly that pair and refuse the archive if the digest does not match. To do it by hand — in a build image that should not pipe a script into a shell, say: ```bash version=0.4.0 triple=x86_64-unknown-linux-gnu base=https://github.com/codyspate/khoralang/releases/download/v$version curl -fsSLO "$base/khora-$version-$triple.tar.gz" curl -fsSLO "$base/khora-$version-$triple.tar.gz.sha256" sha256sum -c "khora-$version-$triple.tar.gz.sha256" tar xzf "khora-$version-$triple.tar.gz" -C /opt /opt/khora-$version-$triple/bin/khora --version ``` Windows ships the same layout as a `.zip` rather than a `.tar.gz`. The archive unpacks to `khora--/` holding `bin/khora`, the runtime archive beside it, `std/` as source, and the licenses. Nothing needs configuring after unpacking: the compiler finds `std/` and the runtime beside its own binary, and `KHORA_STD` and `KHORA_RT_LIB` exist only as overrides for an unusual layout. A release also carries `khora-.cdx.json`, a CycloneDX bill of materials for the toolchain itself, with its own checksum. ## WebAssembly WebAssembly is a distinct runtime environment, not Linux with a different object format. A wasm target must use a std/platform surface appropriate to its host and must not expose filesystem or socket APIs the host does not provide. **No wasm target is advertised**, so none of that has been built. `std` has no Worker-shaped platform surface, there is no no-fibers execution model to test, and no host-provided networking or storage capabilities are modeled. LLVM can emit wasm — the compiler's own tests check that the runtime's symbols resolve there — and that is emission, not a deployment path. Cloudflare Workers is the motivating first wasm deployment target, and its host-provided networking model and single-threaded isolate mean its runtime contract will differ intentionally from native server targets. [Cloudflare Workers](/docs/v0.4/deployment/cloudflare/) says what would have to exist, and tells you not to choose it in the meantime. ## Release rule A target enters the supported table only when a fresh CI environment can produce the release artifact and a deployment/conformance test executes it in the environment users are being told to target. # Containers Source: https://khoralang.com/docs/v0.4/deployment/containers/index.md Khora's native deployment goal is a small, self-contained executable that does not require a language VM or tracing garbage collector in the container image. The compiler is large — it carries LLVM inside itself — and none of it belongs in the image you ship, so the build is two stages and the second one copies a single file. ## A Dockerfile This example pins the stable 0.4.0 release and builds after it is published. For a published release candidate, set `KHORA_VERSION=0.4.0-rc.2` and pin the application and its Khora-repository dependencies to the same candidate. ```dockerfile # --- build ------------------------------------------------------------------ FROM debian:trixie-slim AS build # clang links the executable; the toolchain download cannot bring a linker with # it. git is only needed if khora.toml has a git dependency. RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates clang curl git tar \ && rm -rf /var/lib/apt/lists/* # Pinned. A plain `curl | sh` takes the newest stable release, which makes the # image depend on the day it was built. ARG KHORA_VERSION=0.4.0 RUN curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh \ | sh -s -- --version "${KHORA_VERSION}" --to /opt/khora --no-modify-path ENV PATH="/opt/khora/bin:${PATH}" WORKDIR /src COPY . . RUN khora install \ && khora build . --release --out /out/myservice # --- run -------------------------------------------------------------------- FROM debian:trixie-slim # ca-certificates only if the service opens outbound TLS: a TlsClient verifies # against the machine's own trust store, and a slim image has none. RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* \ && useradd --system --uid 10001 --home /var/lib/myservice --create-home myservice COPY --from=build /out/myservice /usr/local/bin/myservice USER 10001 WORKDIR /var/lib/myservice EXPOSE 8080 ENTRYPOINT ["/usr/local/bin/myservice"] ``` ```bash docker build --build-arg KHORA_VERSION=0.4.0 -t myservice:1 . docker run --rm -p 8080:8080 myservice:1 ``` ## Why both stages are the same base image The published Linux artifact is dynamically linked against the system C library, and static and musl builds are not produced or tested ([Supported targets](/docs/v0.4/deployment/supported-targets/)). So the runtime image has to supply a glibc at least as new as the one the binary was linked against. Same distribution and same release in both stages is the way to stop thinking about it, and **which** release is not free to choose: the published toolchain needs glibc 2.39 or newer, so `bookworm` cannot run it and `trixie` can. That is why both stages above are trixie and not the more familiar bookworm; the table on [Supported targets](/docs/v0.4/deployment/supported-targets/) is the list. A distroless runtime image works for the same reason and is smaller, as long as it is the Debian 13 variant; Alpine does not, because it is musl. Nothing else from the build stage is copied. There is no `std/`, no compiler, and no toolchain in the final image: the executable carries the Khora runtime inside it, which the release workflow checks on every release by running a packaged toolchain's output with nothing else on the path. ## Cross-building **There is none, and this is the part to plan around.** Cross-compilation is not supported: the compiler can emit for another triple under `KHORA_TARGET`, which checks code generation, but the linker and sysroot story is unfinished, so a deployable artifact means building on the platform it runs on. `linux/arm64` is not built or tested by CI at all. In practice that means `docker build` on an `x86_64` Linux machine or runner — `--platform linux/amd64` if your daemon would otherwise pick something else — and not `docker buildx` fan-out across architectures. ## Health and shutdown Expose the readiness and liveness endpoints your orchestrator expects as ordinary routes; there is nothing Khora-specific in them. A probe that sends `HEAD` — a Docker `HEALTHCHECK` running `curl -I`, a load balancer target group configured that way — is answered by the `GET` route without a second mount, with the headers `GET` would have sent and none of the body. So a health route is one route however the orchestrator asks for it. Shutdown works, with edges worth stating plainly. **The `SIGTERM` a runtime sends before its grace period becomes a cancellation at the root of the program**: nursery cancellation runs, `scoped` finalizers run, in-flight requests unwind, and the process exits 130. The grace period stays the orchestrator's — `--stop-timeout`, or Kubernetes' `terminationGracePeriodSeconds` — because a second deadline inside the runtime would be the one number nobody configured. A second `SIGTERM` restores the default disposition and re-raises, so an operator who means it is not waiting on a slow finalizer. Two shapes are **not** covered and a drain at the layer above is still the answer for them: a `main` that reaches no cancellation point at all -- no loop, no blocking call, no call to a function with one -- has nowhere to stop, so the runtime kills the process outright; and a fiber inside a blocking `connect_to` reaches no cancellation point until the kernel gives up. Writing handlers so that being killed between two instructions is recoverable at the next start remains worth doing — `SIGKILL` at the end of the grace period has not gone anywhere. # Linux Source: https://khoralang.com/docs/v0.4/deployment/linux/index.md Linux is a primary native deployment target for Khora services. `x86_64-unknown-linux-gnu` is one of the three triples the release workflow builds, packages, and then uses to compile and run a program before publishing — see [Supported targets](/docs/v0.4/deployment/supported-targets/) for the whole list and for what is deliberately not on it. ## Install the toolchain on the build machine ```bash curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh | sh ``` That downloads the release for this platform, checks it against the published `.sha256`, unpacks it into `~/.khora`, and appends a `PATH` line to whichever of `~/.profile`, `~/.bashrc` and `~/.zshrc` already exist. Nothing is compiled, nothing needs root, and `rm -rf ~/.khora` undoes it. For a build machine, pin the version and leave the shell profiles alone. The 0.4.0 example works after the stable release is published; for a published candidate, substitute its full version (`0.4.0-rc.2`) in both this command and your project's pin: ```bash curl -fsSL https://raw.githubusercontent.com/codyspate/khoralang/main/install.sh \ | sh -s -- --version 0.4.0 --to /opt/khora --no-modify-path export PATH="/opt/khora/bin:$PATH" ``` `sh -s --` is how arguments reach a script that is being piped. `--pre` adds release candidates to what a plain run would take; a plain run never reaches one, because candidates are published as GitHub pre-releases. **One thing is not in the download and cannot be: a linker.** A Khora program is a native object that has to be linked against this platform's C runtime, and the driver that knows where those live belongs to the platform. Install `clang` or `gcc` from the package manager. The installer checks before downloading anything and warns rather than refusing, so a missing linker shows up at `khora build` if you skip it. ## Build ```bash khora install # only if the project has dependencies; needs git khora check . khora test . khora build . --release ``` The executable is `build/`. Run it: ```bash ./build/myservice ``` Nothing else is copied to the target host. The binary carries the Khora runtime and does not need `std/`, the compiler, or a language VM beside it. What it does need is the system C library it was linked against — the published Linux artifact is dynamically linked against glibc, and static and musl builds are not produced or tested — and `ca-certificates`, if the service opens outbound TLS connections, because a `TlsClient` verifies against the machine's own trust store. **Strip the debug information before you ship it.** A release build emits none for your Khora code, but the runtime linked into every program carries its own — most of a release binary's size. `strip --strip-debug` removes that and nothing else, so a program behaves the same and a trap's backtrace still names its functions: ```bash strip --strip-debug build/myservice ``` A bare `strip` saves a little more by removing the symbol table too, and then every backtrace frame reads ``. Keep an unstripped copy if you want a debugger to see the runtime's frames. **Build on the machine family you deploy to.** Cross-compilation is not supported: `KHORA_TARGET` makes the compiler emit for another triple, which checks code generation and does not produce a runnable artifact. ## Running it under systemd Khora contributes nothing special here — the unit is the ordinary one for a native binary, which is the point: ```ini [Unit] Description=myservice After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/local/bin/myservice WorkingDirectory=/var/lib/myservice User=myservice Group=myservice Restart=on-failure [Install] WantedBy=multi-user.target ``` ```bash sudo install -m 0755 build/myservice /usr/local/bin/myservice sudo systemctl daemon-reload sudo systemctl enable --now myservice journalctl -u myservice -f ``` `WorkingDirectory` is worth setting deliberately. A relative path a program opens is relative to where the process was started, not to where its `khora.toml` was, and an `[permissions.fs]` grant written as `./data/**` is resolved against the manifest — so a service started from `/` and a service started from its own directory disagree about which files exist. `std::log` writes one JSON object per line to standard error, which is where `journalctl` and every container log collector already look; there is no log file to rotate unless you make one. ## Shutdown **A `SIGTERM` from `systemctl stop` becomes a cancellation at the root of the program.** Nursery cancellation runs, `scoped` finalizers run, in-flight requests unwind, and the process exits 130. `systemd` already reports that as a clean stop, and `TimeoutStopSec` stays the deadline: the runtime has no grace period of its own, so the number in your unit file is the only one. A second `SIGTERM` — or `TimeoutStopSec` expiring into `SIGKILL` — ends it at once. Two shapes still end the process without a clean stop, and a service that must survive either should be written so that being killed at an arbitrary instant is survivable — commit before acknowledging, and let the next start recover: - a `main` that reaches **no cancellation point at all** — no loop, no blocking call, no call to a function that has one. There is nowhere in it to stop, so the runtime uses the default disposition and the process dies at wait-status 143 with no finalizers rather than hanging. Any `main` that loops, serves or waits has a cancellation point, whatever its `raises` row; - a fiber inside a blocking `connect_to`, which reaches no cancellation point until `connect(2)` gives up. [Cancellation-safe resources](/docs/v0.4/cookbook/cancellation-safe-resources/) is the pattern, for a deploy as much as for a nursery. # Cloudflare Workers Source: https://khoralang.com/docs/v0.4/deployment/cloudflare/index.md Khora does **not yet advertise Cloudflare Workers as a supported deployment target**. **Not "experimental" either**, by the vocabulary on [Supported targets](/docs/v0.4/deployment/supported-targets/): experimental there means important pieces work and production support is not promised yet, and here none of the pieces exist. `std` has no Worker-shaped platform surface, there is no no-fibers execution model to test, and no host-provided networking or storage capability is modeled. LLVM can emit wasm — that is emission, and the same page says the website must never describe emission-only support as deployable platform support. This page is here to say what the target would need, and to be the thing a reader finds instead of guessing. Cloudflare Workers run WebAssembly inside a host-provided execution environment. A Khora Worker target therefore cannot reuse the native Linux runtime model unchanged: networking, request handling, persistence, clocks, and other platform facilities must be provided through Worker host capabilities rather than Unix sockets or a local filesystem. ## When this page becomes a deployment guide This page will contain copy-pasteable build and deploy commands only after the compiler, Worker bindings, runtime/platform split, packaging, and an end-to-end deployed Khora example are working together. There is deliberately no `wrangler.toml` sketch here: a configuration file for a module nothing can produce is a thing somebody will copy and then spend an afternoon on. Until then, do not choose Cloudflare Workers for a Khora application that needs a supported production deployment path. Use one of the targets listed as supported in [Supported targets](/docs/v0.4/deployment/supported-targets/). # Coming to Khora (migration guides) Source: https://khoralang.com/docs/v0.4/migration/index.md These notes map familiar concepts from other ecosystems onto Khora. They assume you have read enough of the [Language Reference](/docs/v0.4/reference/) to recognize the syntax — [Expressions](/docs/v0.4/reference/expressions/), [Failures](/docs/v0.4/reference/failures/) and [Capabilities](/docs/v0.4/reference/capabilities/) are the three that carry most of the difference — and they spend their space on the places where a habit from your last language is the thing that will trip you. They are not syntax cheat sheets. The goal is to explain where a familiar concept has a direct analog, where Khora deliberately chooses a different abstraction, and which habits should not be carried over unchanged. - [From TypeScript + Effect](/docs/v0.4/migration/from-typescript-effect/) — the closest fit in intent, and the one where the vocabulary maps almost term for term: `Effect` is a return type, a `raises` row and a `with` row. - [From Go](/docs/v0.4/migration/from-go/) — the closest fit in *deployment*: one native binary, no VM. The differences are that failure is in the type rather than in a second return value, and that a fiber has an owner. - [From Rust](/docs/v0.4/migration/from-rust/) — native and statically typed without lifetimes or a borrow checker, because Perceus reference counting answers the same question at run time. Khora examples on these pages are compiled as part of the docs build. # From TypeScript + Effect Source: https://khoralang.com/docs/v0.4/migration/from-typescript-effect/index.md If you know Effect, Khora's motivation should feel familiar: failures and capabilities belong in types, resource lifetimes should be structured, and concurrent work should have ownership. The major difference is where that model lives. Khora builds those ideas into a native language/runtime rather than expressing them as a TypeScript library over the JavaScript/Node execution model. ## Where the familiar pieces live | Effect | Khora | | --- | --- | | `Effect` | a function's return type, `raises` row and `with` row | | `Effect.either` | [`attempt`](/docs/v0.4/reference/failures/#attempt) | | `Redacted` | `std::core::Redacted` — same idea, and `Show`/`Encode` make it a compile error rather than a convention: a record holding one derives `Decode` and refuses `Encode`, where Effect's `Schema.Redacted` round-trips the secret on its encode side by design | | `Config` | [`std::config::read`](/docs/v0.4/cookbook/configuration/) over a `Schema`, which `derive(Decode)` writes from the type; see below | | `Schedule`, `retry`, `repeat` | [`std::resilience`](/docs/v0.4/cookbook/retrying/) | | `Clock.sleep`, `TestClock` | `std::clock::Clock` — `sleep` is an operation on the capability, so a fake clock is a handler and needs no fork-or-deadlock caveat | | `Queue` with `dropping`/`sliding` | [`Channel::dropping` / `Channel::sliding`](/docs/v0.4/reference/sharing/#what-a-full-channel-does) | | `Fiber.join` | `Fiber::join`, which re-raises the child's failure with its type | | `Effect.forkScoped` | `nursery.adopt(Fiber::spawn(..))` | | `Ref` | `Shared` | | `Layer` | nothing — see below | Some things are deliberately absent. `Effect.gen`, `pipe`, dual APIs, branded types and `Match` are TypeScript workarounds; Khora has methods, nominal types, `match`, and `let`. ## Direct style `Effect.gen` and `yield*` disappear. A function calls effectful operations directly, inside the capability and failure context its own signature declares — there is no `Effect` value to construct and compose: ```typescript // Effect const loadUser = (id: Id) => Effect.gen(function* () { const db = yield* Database; const row = yield* db.query(id); return User.from(row); }); ``` ```khora // Khora fn load_user(id: Id) -> User with { db: Db } raises UserError { User::of(db.query(id)!) } ``` `raises` is the typed failure dimension, `with` is environmental authority, and postfix `!` is where control can leave — the marks `yield*` was carrying. A *handler* is what supplies a capability for a scope; it is the value a `with` block binds. ## The rows are named the other way round `Effect` calls them Errors and Requirements. Khora writes them as two separate rows, and the conventional names are two letters each so you cannot read them backwards: ```khora fn call(body: () -> A with 'ef raises 'er) -> A with 'ef raises 'er ``` `'ef` is the capability row — Effect's `R`. `'er` is the failure row — Effect's `E`. A single-letter `'e` was ambiguous in exactly the direction that hurts somebody arriving from Effect, which is why it is not spelled that way. ## No `Layer`, and that is the point There is no `Layer`. A capability is built by an ordinary binding in the `with` block, once, in the order written — so there is nothing to memoize, and no way to get two structurally identical instances of the same dependency: ```khora with { config: env_config(), scope: Scope::root(), db: postgres_db()!, store: sql_store(), } { run_server()! } ``` Built once, in the order written, by the ordinary rules of a binding. There is nothing to memoize because there is nothing being rebuilt. ## No `Config` description type either `std::config::read(schema)` reads the whole configuration at once. It walks a `Schema` — the same one that reads a JSON body — for the variables it needs, and answers `Validated`, so every bad key is reported in one pass and each is spelled as the variable it came from. There is no description type to defer the read, because the `Env` handler is already what a test swaps. The schema is there for reuse rather than for deferral. ## Interruption is not only at effect boundaries In Effect, a tight loop inside one synchronous step cannot be interrupted. Khora separates scheduler safepoints from cancellation points and puts a cancellation point at every loop back-edge, every call to a function that can reach one, and every blocking operation, so a spinning loop is cancelable whatever its function's `raises` row says. A cancellation does not travel on the error row: it has a return of its own, and a `catch` does not see it. ## Pipelines Khora's `|>` is call-oriented. `value |> f(a)` means `f(value, a)`, and a single `_` placeholder can select another argument position. It is not limited to piping into unary functions. ## Visibility is `pub`, not `export` The one place the surface will read as *less* familiar than you expect. `import` is spelled the way you already spell it, but what marks a declaration public is `pub`: ```khora pub type Entry = { id: Int, memo: String }; pub fn total(entries: List) -> Int ``` It also appears on methods, which is why it is not `export`: nobody imports `Map::get`, they reach it by having a `Map`. A declaration without `pub` is private to its file, which is closer to a module with no `export` than to TypeScript's default of exporting whatever is written at the top level. # From Go Source: https://khoralang.com/docs/v0.4/migration/from-go/index.md Go and Khora agree about the shape of the artifact: one native executable, no VM beside it, fast start, and an operational story you can hold in your head. They disagree about how much of a service's behavior belongs in the type. Four places where a Go habit needs adjusting, with the Khora each of them becomes. ## Errors: the second return value moves into the type Go returns failure as a value, by convention, and the compiler is happy if you ignore it: ```go user, err := loadUser(id) if err != nil { return Response{}, err } ``` Khora puts the failure channel in the signature. The failure type is an ordinary ADT: ```khora pub type UserError = | NotFound(id: Id) | Unavailable(reason: String); ``` and the function says it uses the channel: ```khora fn load_user(id: Id) -> User raises UserError { // ... } ``` The `if err != nil` becomes a postfix `!` at the call, which propagates: ```khora Id -> User raises UserError ``` Three consequences a Go reader should expect: - **`!` is not `?` and it is not `panic`.** It says *this call may fail and I am letting the failure out of here*, which is only legal if the enclosing function's own `raises` row admits it. A missing `!` is a compile error, not a dropped error. - **`raises` composes with `+`** — `raises UserError + HttpError` — rather than collapsing to one `error` interface, so the arms of a `catch` are checked for exhaustiveness the way a `match` is. - **There is no `errors.Is` / `errors.As` unwrapping ceremony**, because nothing was wrapped into an interface to begin with. [Failures](/docs/v0.4/reference/failures/) has `catch` and `attempt`, which are the two ways back out. The habit to drop: returning a zero value beside an error. A Khora function that fails does not also produce an answer, so there is no zero `Response{}` to invent and no caller reading one by mistake. ## Dependencies: from a struct field to a `with` row Go passes a `*sql.DB`, a logger and a clock down through constructors, or reaches for a package-level variable. Khora makes the requirement part of the type, and it is discharged once, at the boundary: ```khora with { config: env_config(), scope: Scope::root(), db: postgres_db()!, store: sql_store(), } { run_server()! } ``` Everything inside that block can call functions declaring `with { db: Db }` without being handed a `db` argument, and a function that does *not* declare it cannot reach one. That is the part with no Go analog: the absence of ambient authority is checked, so a helper five layers down cannot quietly open a socket. The habit to drop: the `context.Context` first parameter. It is carrying three different things — cancellation, deadlines and request-scoped values — and each of them has its own home here. Values are capabilities in the `with` row. Cancellation belongs to the nursery, below. Deadlines are the piece that does not exist yet: there is no `timeout`, no `race` and no `select`, and [Concurrency](/docs/v0.4/reference/concurrency/) says so plainly rather than leaving you to find out. ## Concurrency: `go f()` always has an owner The Go loop that starts work and hopes: ```go for _, job := range jobs { go handle(job) } ``` has no answer to *who waits for these*, *what happens when one panics*, or *what stops them*. A `sync.WaitGroup` and a `context` are the conventional answers and neither is enforced. In Khora a fiber is adopted by a nursery, and the nursery is a scope that does not return until its children are done: ```khora module main; import std::core::{ChildFailed, Fiber, Nursery, bounded_nursery, print}; fn handle(job: Int) -> () { print("processing job ${job}"); } fn launch_jobs() -> () with { nursery: Nursery } { let mut next = 0; while next < 1000 { let job = next; nursery.adopt( Fiber::spawn(fn () => handle(job)) ); next = next + 1; } } pub fn main() raises ChildFailed { bounded_nursery(64, launch_jobs)! } ``` `bounded_nursery(64, ...)` is also the `WaitGroup` plus the semaphore Go programs write by hand: adoption blocks once the limit is live, so the producer slows at the boundary where it makes work instead of filling a queue. See [Bound concurrent work](/docs/v0.4/cookbook/bounded-concurrency/), including the off-by-one to subtract when the limit is a real resource. **Two of those three questions are answered today.** Every child is waited for and a failure is always reported as `ChildFailed`. The first failure cancels the siblings still running when the nursery sees it, and it sees failures in adoption order: a failing child adopted first stops all its siblings within milliseconds, one adopted last is seen only after the others have finished and stops none. So work that is expensive, holds a resource, or has an effect outside the process should check a `Shared` flag itself rather than expect the group to collapse promptly. [Known limitations](/docs/v0.4/limitations/#a-childs-failure-is-seen-late-unless-it-was-adopted-early) has the measurements. A Go reader coming off `errgroup`, where the first error does cancel the group's context at once, should not assume the same here. Cancellation arrives without a `ctx.Done()` channel to select on, and without a `raises` row either: a canceled fiber stops at its next **loop back-edge**, blocking call, or call to a function that has one, in any function. That is what makes an ordinary polling worker stoppable with nothing in it that looks like a cancellation check: ```khora fn reaper() -> () with { clock: Clock } { loop { clock.sleep(1000); sweep(); } } ``` The habit to drop: a bare `go`. There isn't one — `Fiber::spawn` produces a handle, and you either `join` it for its answer or `adopt` it into a nursery that will. ## Memory: reference counting instead of a tracing collector Go's collector gives you no annotations and a tail. Khora's answer is Perceus reference counting with compiler ownership and reuse analysis: also no annotations, no borrow checker, and no collector thread — the counts are inserted at compile time and a uniquely-owned value is reused in place rather than freed and reallocated. What that changes operationally is the shape of the latency curve and the resident set rather than the code you write. The [Performance](/docs/v0.4/performance/) page has the measured numbers, the ladder they were taken on, and what is explicitly not measured; it is the page to read before quoting anything. The one place a Go programmer gets *less*: **a reference cycle leaks.** A tracing collector is what Khora exists not to have, so there is no cycle collector, and weak references — the usual way to break one — do not exist yet either. A cycle needs a `mut` field deliberately pointing back at something that already reaches it, so it is not accidental; it is also not caught. The leak is bounded and quiet rather than unsound: nothing is freed early and nothing is read after free, the memory is simply never returned. ## What stays familiar Direct-looking code with no monadic plumbing, one binary to ship, a standard library that covers HTTP and TLS, structured logging as JSON on standard error, and a small number of runtime concepts visible in application code. `khora fmt` is `gofmt`'s idea — canonical formatting, not a style argument — and `khora test .` compiles and runs the package's tests one fiber each. The thing that is genuinely missing rather than different: there is no package registry, so a dependency is a `git` URL, and `khora install` fetches and locks what `khora.toml` declares. [Modules and packages](/docs/v0.4/reference/modules-and-packages/) is the page for that. # From Rust Source: https://khoralang.com/docs/v0.4/migration/from-rust/index.md Khora shares Rust's goal of native, predictable software without requiring a tracing GC, but it deliberately does not expose Rust's ownership proof system as the ordinary programmer interface. ## There are no lifetimes to write Memory is reference-counted, and the compiler removes the counts it can prove are unnecessary and reuses storage it can prove is uniquely owned. So ordinary data flow needs no annotation: no `&`, no `'a`, no `Rc>` to get a value into two places. What you give up is the proof — Khora will keep a count where Rust would have shown you why one was not needed. ## `Result` and dependency arguments become rows What Rust puts in the return type and the parameter list, Khora puts in two rows beside them: `raises` for recoverable failure, `with` for external authority. ```rust // Rust fn load_user(id: Id, db: &Pool) -> Result { let row = db.query(id)?; Ok(User::from(row)) } ``` ```khora // Khora fn load_user(id: Id) -> User with { db: Db } raises UserError { User::of(db.query(id)!) } ``` **`?` becomes postfix `!`, and leaving it off is a compile error** — the compiler names the call and tells you to write it. The rows stay visible in the function's type, and neither has to be threaded through every intermediate value: a `?` on every call and a `db: &Pool` on every signature are the two things this is trying not to be. `catch` handles part of a row by name, the way `match` on a `Result` does: ```khora let row = load_user(id)! catch { UserError::Missing(_) => User::anonymous(), }; ``` ## `Arc>` becomes `Shared` ```rust let hits = Arc::new(Mutex::new(0)); // Rust ``` ```khora let hits = Shared::of(0); // Khora ``` A value captured by a spawned fiber must be `Share`. `Shared` is, and takes a lock; a record with a `mut` field is not, and the compiler refuses the capture rather than letting two fibers race. `Shared::update` is the read-modify-write that happens under the lock — `set(get(cell) + 1)` is two steps and can lose an increment, exactly as in Rust. **There is no `Rc`/`Arc` split to choose between.** Reference counts are always atomic, so there is no cheaper single-threaded variant to reach for and no type that changes when a value starts crossing fibers. [Memory](/docs/v0.4/internals/memory/) says what that costs. ## There is no `async fn` Fibers are structured and I/O is direct-style, so a function is not written twice. Suspension is something the runtime does, not a second color of function that divides the library in half. ## Visibility `pub` works the way it does in Rust, in the three places you would expect it. A declaration without it belongs to its module, a method without it belongs to the module that declares the type, and so does a field: ```khora pub type Counter = { n: Int, pub label: String }; impl Counter { pub fn doubled(self) -> Int { Counter::secret(self) } fn secret(self) -> Int { self.n * 2 } } ``` The unit is the module, not the crate: Khora has no `pub(crate)`, so a sibling module in the same package is outside, and a separate `_test` module is too. A tuple struct's field is `type Port = pub Int;` rather than `struct Port(pub u16)`. As in Rust, a method of a *trait* implementation takes no `pub` — what makes it reachable is the trait, not the impl. Writing the keyword on one method of an `impl Show for T` would suggest the others were hidden. Khora's module path separator is `::` as in Rust, but the declaration is `module a::b;` at the top of a file and the import is `import a::b::{X};` — the file is the module, so there is no `mod` tree to keep in step with the directory layout. ## FFI: `extern fn`, and no `unsafe` keyword There is no stable Khora-to-Khora ABI, for the same reason Rust has none: data layout and calling convention are the compiler's to choose, and it reserves the right to change them. The interoperability boundary is the C ABI, and it is spelled the way Rust spells it minus the block. An `extern fn` with no body is a symbol the linker must find; a `pub extern fn` with a body is a Khora function C can call: ```khora extern fn monotonic_ticks() -> Int; ``` What replaces `unsafe` is the manifest. `extern` in `[permissions]` names the **packages** that may declare `extern fn` at all — `std` always may — so the audit is one table rather than a search for a keyword, and a dependency that starts calling into C cannot do it quietly. [FFI](/docs/v0.4/reference/ffi/) has the ABI-safe types, the rules for borrowing a buffer for the duration of one call, what a blocking foreign call costs a fiber, and what a trap does at an exported boundary. [Capabilities](/docs/v0.4/reference/capabilities/) has the permission table. Khora is not intended to replace Rust for every low-level kernel or embedded use case. Its target is native application and service software where ownership complexity costs more than it returns — and the honest version of that trade is on [Limitations](/docs/v0.4/limitations/), which is the page to read before choosing between them. # Performance Source: https://khoralang.com/docs/v0.4/performance/index.md **Khora's HTTP server answers about 174,000 requests a second on a 16-core desktop, in 8.4 MB of memory.** That is a little under Go's standard library on rate, and between two and eighty times less memory than the *managed* runtimes it is measured against. The Rust control uses less — 5.5 MB — which is the honest comparison: Khora is second on memory in the table below, not first. This page says what that measures, what it does not, how to run it yourself, and why every figure published here before September 2026 was between two and twelve times too high. **This page is about Khora's performance, not yours.** The commands below run from the compiler's repository, so an installed toolchain does not have them. If your own program is slower than you want, [Making your program faster](/docs/v0.4/performance/your-program/) is the page you want. A benchmark number without the ladder that produced it is unfalsifiable, and an unfalsifiable number in a language's marketing makes every other claim in the documentation worth less. So the conditions a figure has to meet are written down below, the script checks them on every run, and a server that fails one is reported with the failure instead of with a number. ## What is measured `bench/` holds servers that answer the same request, so that the difference between any two of them is one thing: | | what it is | | --- | --- | | `control.rs` | Rust, a thread per connection, closing after each request | | `control_keepalive.rs` | the same, holding the connection open | | `floor/` | Khora: accept, read, write a fixed string. No parsing | | `render/` | the floor plus `Response::rendered_keeping`. No parsing | | `service/` | a `Router` with one route — the whole of `std::net::http` | | `allocator/` | not a server: what allocation costs as the heap fills | | `iteration/` | not a server: what `for` costs against the loop written out | The comparisons that mean something are the differences. `floor` against `control_keepalive` is what the **runtime** costs. `service` against `floor` is what the **library** costs. `render` sits between them and says how much of the library is building the response rather than reading the request. A single number for `service` on its own says nothing, which is why the list is shaped this way. ## The numbers 16-core Windows desktop, release builds, `bench/loadgen.rs` on the same machine, 32 connections, six-second runs, mean of five, 2 September 2026. **These numbers travel with that sentence or they do not travel.** | | req/s | p50 | p99 | peak RSS | | --- | --- | --- | --- | --- | | C#, ASP.NET Core (Kestrel) | 266,267 | 103us | 253us | 240 MB | | Khora `floor` | > 234,322 | 128us | 274us | 7.4 MB | | Khora `render` | > 234,039 | 127us | 253us | 7.8 MB | | Rust control, thread per connection | 202,182 | 150us | 319us | 5.5 MB | | Go, `net/http` | 185,670 | 102us | 1,538us | 21.8 MB | | Khora, `std::net::http` | 174,201 | 161us | 554us | 8.4 MB | | Java, JDK `HttpServer` | > 114,200 | 251us | 663us | 699 MB | | Node, `node:http` | 39,184 | 682us | 4,101us | 86.8 MB | `floor` and `render` are lower bounds: they are fast enough that the generator was still gaining when given more of the machine. Java's is one too, because its rate was still climbing at the top of the ladder, which is a JIT that had not finished with the handler. **Khora's HTTP server is mid-table on throughput and uses less memory than any managed runtime here, and both halves of that are the headline.** It is about six per cent under Go's standard library and a third under Kestrel on requests a second. It does it in 8.4 MB, against Go's 21.8, Node's 86.8, Kestrel's 240 and the JDK's 699 -- between two and eighty times less than the runtimes it is answering as fast as. The Rust control, with no runtime at all, uses 5.5 MB: that is the floor this is measured against rather than a competitor it beats. That is what "no VM and no tracing GC" is worth, in the one column where the claim can be checked. Each peer is that language's *ordinary* server, not a tuned one. Node's is single-threaded by design and would be several times faster behind `cluster`; the JDK's is not what a Java service ships on; `fasthttp` is faster than `net/http`. Read the table as what you get writing the obvious thing. **Latency is not throughput.** Go answers the median request faster than Khora and the slowest one nearly three times more slowly. A server chosen on peak rate alone would have missed both halves of that. ### The comparisons worth the most The cross-language row is the one people look at and the one that says least, because eight runtimes differ in eight ways at once. The Khora rows differ in one thing at a time and are what the servers were built for: `service` against `floor` is **the library**: the whole of `std::net::http` -- parse, header map, route match, render -- costs about a quarter of the throughput of a socket loop that does none of it. `render` sits with `floor`, which puts what is left in reading the request rather than writing the answer. `floor` against the Rust control is **the runtime**, and the control is a thread per connection, which is not the fastest way to write it in Rust, so read that pair as "the runtime is not what limits either" rather than as a win. ## What was published before this, and why it was wrong Every throughput figure this project recorded before 2 September 2026 came from a load generator that reported one connection's rate multiplied by the number of connections, and was between two and twelve times too high. The old rig ran one process per connection, each timing itself, and divided the total by the duration it was *asked* for rather than the one it took. Forty-eight Python processes on Windows take fifty-two seconds to get through a four-second run, so the workers barely overlapped, each measured a nearly idle server, and the divisor stayed at four. A rig whose output is proportional to its own worker count cannot flatten, which is exactly why no ceiling was ever found and why the spread between sittings looked like 1.85x -- process startup time was what varied. A server that counts what it answers settled it in twenty lines. The full account is in [the errata](https://github.com/codyspate/khoralang/blob/main/docs/errata.md). The replacement is `bench/loadgen.rs`: a few threads each driving many non-blocking connections, rather than a thread or a process per connection. The change that mattered was not the language. A blocking read parks the thread and the kernel wakes it again on every response, about 120 microseconds on a round trip whose median is 29; the same connection spinning on a non-blocking socket answers five times as many requests. ## What a figure has to satisfy before it is published 1. A load generator that is not the bottleneck — a separate machine, or a generator that can saturate the server on this one. 2. A ladder of concurrencies where the rate flattens, so that the top of the ladder is the server's answer and not the client's. 3. The same configuration repeating across sittings, to within a few per cent. 4. The machine, the profile and the date printed beside the number. Conditions 3 and 4 hold for every row in the table above. Conditions 1 and 2 hold for every row **not** marked `>`; the three that are marked — Khora `floor`, Khora `render` and Java — are printed as lower bounds precisely because they failed one. The generator was still gaining when given more of the machine against `floor` and `render`, so the bottleneck was the generator and not the server; Java's rate had not flattened at the top of the ladder. A row that fails a condition is not a measurement of the server, and the `>` is how the table says so. `bench/measure.py` checks all four on every run rather than leaving them to be remembered, and reports what failed alongside the bound instead of a bare number. ## Running them yourself ```bash rustc -O -o bench/loadgen.exe bench/loadgen.rs rustc -O -o bench/control_keepalive.exe bench/control_keepalive.rs cargo run -p khora-cli --features llvm -- build bench/service KHORA_PROFILE=release python bench/measure.py ``` `measure.py` starts each server in turn, walks the ladder, repeats the chosen rung and prints the table with the machine and the date on it. One server on its own: ```bash ./bench/service/build/service.exe & ./bench/loadgen.exe --port 18952 --label service --connections 32 --seconds 6 ``` Ports are fixed so two cannot be measured at once by accident: `floor` is 18950, `render` 18951, `service` 18952. `--watch-pid` samples the server's resident memory while the run is under way. **Build with `KHORA_PROFILE=release`** for anything you intend to quote. A debug build is the default everywhere in this toolchain, deliberately — a language being brought up should give a readable crash before it gives a fast one — and a debug number is not a number about the language. `measure.py` says so and carries on rather than refusing, because a debug comparison between two Khora servers is still a comparison. `bench/README.md` in the repository has the rest, including what each server does and does not do. ## What these do not measure A `/health` route returning a fixed JSON object is the thinnest possible request. It isolates the library from the handler, which is the point, and it resembles no real workload: nothing here has a body worth parsing, a database behind it, or a response worth rendering. Cold start is not measured, and neither is behavior under more connections than the machine has cores. **And every row is a Windows number.** One machine is what makes eight runtimes comparable to each other, and it is also the limit of what the table says: none of it has been reproduced on Linux or macOS, which is where most deployments run. [Known limitations](/docs/v0.4/limitations/) disqualifies this project's fiber-density claim for being measured on Windows only, and the same standard applies here — the table is evidence about this machine until somebody runs `bench/measure.py` on another one, which is the reason the commands above are on the page. # Making your program faster Source: https://khoralang.com/docs/v0.4/performance/your-program/index.md This page is about **your** program. [Performance](/docs/v0.4/performance/) is about Khora's own HTTP server: what it answers, measured how, against what. That page runs commands from the compiler's repository, so a toolchain you installed cannot run them. There is **no sampling profiler**. Nothing in the toolchain will point at a line and tell you it is hot. What there is, in the order you should reach for it: ## 1. Build with `--release` before believing any number ```sh khora build . --release ``` The default profile is `debug`: unoptimized, with debug information, which is what a crash you are about to read wants. `khora test` and `khora bench` have no flag of their own and read `KHORA_PROFILE` instead: ```sh KHORA_PROFILE=release khora bench . ``` A `bench` run in the default profile says so in its output, because a microbenchmark of unoptimized code is a measurement of the wrong program. **How much release buys you depends on where the time goes**, and the relationship is not the one you might expect. A small integer loop differs by roughly a factor of two between profiles. But the better your program gets, the less the optimizer has left to find: one word-frequency counter measured 1.45× between profiles in its first form, 1.12× after the hot loop moved into a `Map`, and 1.09× once it streamed. Work done inside the standard library is already compiled; only your own code changes between profiles. ## 2. Time a whole operation with `khora bench` A `bench` block times a body and reports a distribution, not a single number: ```khora bench "parsing a page of input" { let _ = parse(sample); } ``` ```sh khora bench . --filter parsing ``` `--filter` takes a substring, so one name runs one benchmark. This is the right tool when you can call the thing you care about directly. **Put each benchmark where you can find it again.** Spreading `bench` blocks across files is fine, and so is keeping them beside the code they measure. ## 3. Time stages inside a real program with the clock `khora bench` measures something you can call in isolation. When the question is "which stage of this pipeline is slow", the answer is a capability you already have: ```khora import std::clock::{Clock}; import std::core::{print}; pub fn main() -> Int { with { clock: Clock::real() } { let start = clock.monotonic_millis(); let parsed = parse(input); let after_parse = clock.monotonic_millis(); let counted = count(parsed); let after_count = clock.monotonic_millis(); print("parse ${after_parse - start}ms, count ${after_count - after_parse}ms"); 0 } } ``` `monotonic_millis` is the one to use for durations. It only ever moves forward, so it cannot be pulled backwards by a clock adjustment mid-measurement — which a wall clock can, and then your timing is negative. This is manual instrumentation, and it is the technique rather than a workaround: with no profiler, narrowing by hand is how you find the stage that costs you. Time coarse stages first, then subdivide the one that dominates. ## 4. Count allocations when the time is not in arithmetic A program can be slow because it is allocating, and that does not look like a hot loop. The runtime can tell you how many objects are live, through an `extern` declaration — it is not a `std` function, so you declare the symbol yourself: ```khora extern fn khora_live_count() -> Int; test "counting words allocates a constant number of objects" { let before = khora_live_count(); let _ = count(sample); assert_that(khora_live_count() == before, "the count leaked objects"); } ``` Khora's own standard-library suite uses exactly this to pin down allocation behavior — see `tests/std-suite/src/vector.kh` for the shape, where it asserts that a vector of numbers allocates a constant number of objects however many numbers go in. An operation whose allocation grows with its input, when it should not, is usually a value being rebuilt rather than updated. `String` concatenation in a loop is the common one: each `+` builds a new string, so a loop over *n* items copies *n²* characters. That is the shape behind most "it got slower than it should have" surprises. ## What is not here - **No sampling profiler**, so no flamegraph and no "this line is 40% of your runtime". Sections 3 and 4 are how you find that out. - **No allocation profiler.** `live_count()` gives you a number at a point, not a breakdown by type or site. - **`khora build` emits DWARF on Linux**, so an external tool such as `perf` has symbols to work with. Nothing in CI exercises that, so treat it as something that may work rather than a supported workflow, the same caveat [Debugging](/docs/v0.4/reference/debugging/) gives for a debugger. ## What to measure first In order, because each one is cheaper than the next and rules more out: 1. Is it built `--release`? A factor of two hides a lot. 2. Does the work grow faster than the input? Double the input and time it. A doubling that quadruples the time is an accidentally-quadratic operation, and no amount of constant-factor tuning will save it. 3. Which stage? Coarse `monotonic_millis` brackets, then subdivide. 4. Is it allocating per item where it should allocate once? # Known limitations Source: https://khoralang.com/docs/v0.4/limitations/index.md Khora is pre-1.0. This page exists so users can tell the difference between a language rule, a supported feature, and unfinished work. **The ones most likely to affect you:** - [What a signal does](#what-a-signal-does-and-the-three-shapes-it-does-not-reach) — `SIGTERM` and `SIGINT` unwind the program and run its finalizers, but a `main` with no cancellation point, a blocking `connect_to` and Windows are not covered. - [A bounded nursery runs `limit + 1` children](#what-a-nursery-actually-does), and a limit of zero means no limit at all — so **a bound of exactly one cannot be written**, which is the value a "one at a time" flag wants most. - [A child's failure is seen late unless it was adopted early](#a-childs-failure-is-seen-late-unless-it-was-adopted-early): a failing child cancels its siblings only once every child adopted before it has finished. - [A canceled fiber waiting on a child stops only when the child does](#a-canceled-fiber-waiting-on-a-child-stops-when-the-child-does), if it holds the child's last handle. - [The two fiber backends behave differently](#the-two-fiber-backends-are-distinguishable) in scheduling order. - [There is no `timeout` or `race`](#concurrency-combinators) in `std`; a deadline is built from a fiber and a clock. - [Only Linux has a scalable I/O backend](#io-scaling-on-macos-and-windows) for the fiber scheduler; macOS and Windows wait on sockets with `poll`. - [On one CPU, the scheduler can hang](#the-fiber-scheduler) a program in which a fiber blocks inside `Shared::update` on something another fiber must do. The default thread backend does not. - [A `Map` whose keys a client chooses can be made slow](#maps-keyed-by-untrusted-input): the string hash takes no secret, so colliding keys can be computed ahead of time, and a JSON object built from them costs time in the square of its size. - [The `postgres` package does not speak TLS](#package-ecosystem), so rows cross the network in the clear. - [Database queries trail Go's](#performance-against-other-servers) on the TechEmpower query tests; JSON and fortunes do not. - [Inbound connections are not permissioned](#inbound-connections-are-not-permissioned) — the manifest governs outbound only. ## Target coverage Khora has versioned toolchain artifacts and installers for the platforms the project releases. The normal path is the installer documented in [Installation](/docs/v0.4/getting-started/installation/), not compiling the compiler from source. A target is only called supported when the compiler, runtime, linker/sysroot, packaging, CI and deployment path work end to end. [Supported targets](/docs/v0.4/deployment/supported-targets/) lists the ones that do. There are three: `x86_64-unknown-linux-gnu`, `x86_64-pc-windows-msvc` and `aarch64-apple-darwin`. **Linux on arm64 and Windows on arm64 are not among them** — the compiler can emit an object for `aarch64-unknown-linux-gnu`, but no toolchain is published for it and a build for it stops at the link. **There is no static or musl build**: the Linux toolchain and the programs it builds link dynamically against glibc, so Alpine and other musl distributions cannot run them, and a `scratch` container image is not an option. ## Recursion depth and very large lists Khora does not guarantee tail-call optimization, so a function that recurses once per element uses one stack frame per element. Running out of stack ends the program; it reports ``` khora: the stack ran out ``` on standard error and exits with the platform's stack-overflow status. A spawned fiber has eight megabytes of stack on either fiber backend, which is what `main` gets by default on Linux and macOS and what a Khora program's `main` is linked with on Windows, so a recursion that runs in `main` runs the same on a fiber. [Traps](/docs/v0.4/reference/traps/#running-out-of-stack) has how deep that goes. **Where the message is verified.** On Linux, on every stack Khora code runs on: `main`, a fiber on either backend, a test, and a stack that runs out while the runtime is starting a thread or a process. On macOS and Windows it is verified by continuous integration only, which covers `main`, a fiber on either backend and a test on both, and the thread-start case on macOS. **A thread a C library starts can still overflow silently.** Before the runtime starts a thread or a process it makes sure 16 KB of stack is left, so that an overflow lands where the message can be printed. A C library linked through [`build.link`](/docs/v0.4/reference/ffi/#link-against-a-native-library) that starts threads of its own does not do that, and on Linux the C library blocks signals while it starts one: a Khora stack that runs out during that start ends the process with no message. It needs a call into such a library from very deep in a recursion. Every traversal in `std::core`'s `List` is written as a loop rather than as recursion — `length`, `fold`, `reverse`, `filter`, `take`, `drop`, `any`, `all`, `find`, `contains`, `zip`, `flat_map`, `sum`, and the `merge` inside `sort` — so walking a list of any size is safe. `List::sort` recurses only to divide, which is about `log2(n)` deep. String operations are loops too. `split`, `join` and `repeat` handle inputs of any size, and `join` is linear rather than quadratic in the length of its result. `std::json` reads and writes documents of any length with loops. Nesting is the exception: each level of `[` or `{` takes a stack frame, so `parse` refuses a document nested deeper than 512 levels with a `JsonError` rather than letting the document decide how much stack it uses, and `parse_with_depth` takes another limit. `encode` and `Show for Json` have no limit and recurse once per level of a `Json` the program built itself: one nested 200,000 levels deep runs the stack out in a debug build. A `Json` that came from `parse` is at most 512 levels deep, so only a value the program nests by hand can reach that. A derived `Eq`, `Ord` or `Show` on a deeply nested value of the program's own recursive type recurses the same way. **`fold_lines` copies a long line once per chunk it spans.** It reads a file 64 KB at a time and joins what is left of an unfinished line to the next chunk, so a line longer than a chunk is copied again for every chunk it crosses: the cost grows with the square of the line's length. A file of short lines is unaffected. A 16 MB file with no newline took 819 ms in a release build, against 19 ms for the same bytes in 1 KB lines, and doubling the line quadruples the time. To read a file that may be one long line, use `fold_chunks` and split it yourself, or `read_text` when it fits in memory. Releasing a value costs no stack either: reference counting frees a value's children through a queue rather than by recursing, so letting go of a long list is a loop like walking one. A million-element `List` sorts. What is left is ordinary recursion that somebody writes. A function that calls itself once per element of its input will use a frame per element, and no analysis in the compiler turns that into a loop. `Array` and `Vector` are the better shape for a large indexed collection; a list is for building front-to-back and walking once. ## Package ecosystem Dependencies can be pinned reproducibly to git revisions, but there is not yet a public package registry or broad third-party ecosystem. **Three packages are maintained in the Khora repository**, and they are what "the ecosystem" means today: [`postgres`](/docs/v0.4/packages/postgres/), `ai` — the effect a caller names when it wants model inference — and `otlp`, an exporter for `std::trace` over OTLP/HTTP JSON. [Packages](/docs/v0.4/packages/) lists all three and gives the manifest line each is depended on with. Beyond those there is nothing to install: no registry to search and no third-party publishing. **One database driver is published: `postgres`.** `std::db` defines `Db`, transaction semantics and cancellation behavior, and [`packages/postgres`](/docs/v0.4/packages/postgres/) satisfies that interface — it speaks the wire protocol directly, authenticates with `scram-sha-256`, and supplies the `Db` handler. Depend on it with a git revision and a `subdir`; there is no registry yet. **SQLite and D1 have no driver**, and a program that needs one writes its own handler — `Db` is a record of closures, so that is a day's work and a test double is a few lines — over a native client it links with [`build.link`](/docs/v0.4/reference/manifest/#build--what-to-produce). **`postgres` does not speak TLS.** It authenticates with `scram-sha-256`, so the password itself never crosses the network, but every query and every row read back travel in the clear. Connect over a loopback address or a network you already trust — a private network or a tunnel — and do not point it at a managed database across the public internet. A server whose `pg_hba.conf` admits only `hostssl` connections refuses it. **A dependency cannot link a native library on your behalf.** `build.link` is read from the root package's manifest and nowhere else, so a package that ships an archive and declares `extern fn` against it cannot put a flag on your link line — a transitive package adding a native library to your build would be a supply-chain change with no signal at the place that would have to consent. The package does the work and documents one line for you to add, which means every native library a program links can be read off its own manifest. [Foreign function interface](/docs/v0.4/reference/ffi/#link-against-a-native-library) has the shape. ## Editor tooling `khora lsp` provides compiler-backed diagnostics, hover, formatting, completion, signature help, go-to-definition, references, document/workspace symbols, semantic tokens, code actions, code lenses, and inlay hints. Rename covers a declaration and every file that names it, including the import that brings the name into each file, within the opened project; it does not edit dependency code outside the opened folder or in the package store. It renames the original rather than a file's own alias. A parameter's rename covers the labeled arguments written against it; a method's labels are found by its type and method name, so two modules that each declare a type (or a trait) and a method of the same names can have each other's labels renamed. It refuses a **trait member**, whose name belongs to the trait and to every impl of it, and a **constructor**, which has no recorded range to edit. It also refuses a new name that is not a usable identifier or that is already bound anywhere in the same function, even where that binding is in a separate `match` arm and could not have been captured. Further refactoring operations are editor-tooling work. **A record field has no go-to-definition, references or rename**, inside or outside the module that declares it. Renaming a field is a search and an edit by hand, and since a field is private to its module unless it is marked `pub`, the search can usually stop at that module's edges. **The language server reads lint and formatter settings and dependencies once, when it starts**, from the `khora.toml` at the root of the folder the editor opened. An edit to the manifest or to a lint group file, or a dependency installed while the server is running, takes effect after it restarts. Opening a workspace root reads its members' source files but does not resolve their individual dependencies; open the member directory for dependency-aware editor checks. A workspace's `[workspace.lints]` and member manifests are not consulted for lint levels when the workspace root is opened, so the levels it shows can differ from what `khora check` reports in a member package. The command-line check is the authority. See [Editor setup](/docs/v0.4/getting-started/editor/) for the language-server command and client setup. ## A few build errors name the wrong file Almost every error is found by `khora check`, which runs per file and names the file and line it is in. A small number are found only while generating code — an integer pattern too large for an `Int`, a compiler intrinsic such as `Fiber::wait` taken as a value (`let w = Fiber::wait;`). Those errors carry a line and column but not a file, and `khora build` renders them against the first source file it read, which may be a standard-library file or another file of your package. **Trust the message and the line number less than you would a `check` error.** If `khora check` passes and `khora build` reports an error at a place that cannot be the cause, search your own sources for the construct the message names. `khora check` passing and `khora build` failing is always worth [reporting](#reporting-a-limitation): each one is a rule the checker has not learned. ## Standard-library API docs `khora doc` generates the checked-in standard-library API reference from compiler-resolved declarations plus `///` and `//!` documentation comments. `khora doc --check` is used to detect drift between the source declarations and generated pages. Two important documentation-tooling gaps remain: - An API code block is only type-checked if it declares its own `module`. One of the roughly 1,200 blocks on the generated pages does, and it is type-checked on every CI run; the rest are parsed as fragments and none are executed. - Generated signatures name referenced types but do not yet cross-link those type names to their API pages. See the [Standard library](/docs/v0.4/stdlib/) entry point for the generated reference. ## HTTP surface Assume nothing beyond what this section lists. **The verbs are `GET`, `POST`, `PUT`, `PATCH` and `DELETE`, routed to handlers, plus `HEAD` and `OPTIONS`, which the router answers from what is mounted unless you mount a handler for them.** Anything else — `TRACE`, `CONNECT`, an extension method — is answered `400` and the connection closed, because `Method::of` does not name it and an unparsed request line is a malformed one as far as the reader is concerned. The failure is silent from the client's side: a `400` to a verb the server does not know reads as the client's mistake. **A request is capped at 8 KB by default, headers and body together, and the cap is configurable.** Past it the server answers `413` before parsing anything, so the handler never runs. The number answers "how much may an unauthenticated client make a server hold" rather than "how large can a request be" — nothing in the parser recurses per byte or per line, and a 39,808-byte request carrying 2,001 headers parses when the limit admits it. `Router::holding` sets another; the buffer is allocated once at that size per connection, so it multiplies by the connection bound below when deciding what a full server costs. There is no multipart decoding, and a body must be UTF-8 text. **Chunked transfer is read but not written:** `HttpClient` accepts a response framed by `Transfer-Encoding: chunked` and hands the handler the de-chunked body, so a service that answers that way can be called; the server never writes a chunked response. That is the split real traffic has — a response of unknown length is ordinary and a request of unknown length is not. **The server serves at most 256 connections at once, and the number is not configurable.** `Router::listen` and the TLS form wrap their accept loops in `bounded_nursery(256, ..)`, and an accepted connection is a fiber that is inside your handler for as long as the handler runs. There is no second, smaller pool that handlers queue for, so that one number is both the most connections served at once and the most handlers running at once; the next connection waits for one of them to finish. It is a property of the reference server rather than of the `Router` type, so it does not appear in the generated [`std::net::http` pages](/docs/v0.4/stdlib/api/net/http/); [Serve HTTP requests](/docs/v0.4/cookbook/http-service/) is where it is discussed. Because of the off-by-one below, the peak actually observed is 257. ## Performance against other servers On the TechEmpower read tests, run against PostgreSQL on one machine with Khora on the scheduler backend, the **database query tests trail Go**: the single-query test reached 74% of Go's requests per second and the 20-query test 84%. The JSON test was ahead of Go (114%) and fortunes level with it. Tail latency was at or below Go's in every test except the single query, where the 99th percentile was 6.7 ms against Go's 6.4 ms. These are one run on one machine, not a published benchmark; [Performance](/docs/v0.4/performance/) says what a figure has to satisfy before it is published, and why. ## Inbound connections are not permissioned **`[permissions] network` governs outbound connections only.** The grant list is consulted in exactly one place in the standard library — inside `HttpClient::send`, after the URL is parsed and before anything is dialed — and what it decides is whether that host may be reached. Nothing on the server side consults it: `Router::listen`, `Router::listen_quietly`, `Router::listen_tls` and the `listen_on` under them bind and serve without asking. A program with `default = "deny"` and `network = []` opens whatever port it is given and answers requests on it. So do not read a `network` grant as an authorization to listen, and do not read its absence as a refusal to. `network = ["127.0.0.1:8787"]` written to allow a server to bind 8787 does nothing at all — it neither permits the bind, which needed no permission, nor restricts it. The mistake is easy to make and leaves no trace: the program works, and the line that was supposed to be holding it back is not. The reason it is this way rather than fixed is that the two directions do not share a vocabulary. `network` is a list of hosts a program may *reach*. A port it may *bind* is not a host it reaches, so extending the same list to cover listening would produce a control whose meaning could not be written down — `api.example.com:443` would have to mean one thing as a destination and another as a local port, and a permission that cannot be stated is a permission nobody can audit. Naming inbound authority properly needs a grant of its own, and what that grant ranges over — ports, interfaces, both — is an open design question rather than a fix waiting to be typed. Until there is one, a program's ability to listen is bounded by the operating system and by whatever runs it, not by its manifest. ## What a signal does, and the three shapes it does not reach `SIGTERM` and `SIGINT` become a **cancellation at the root of the program**. There is no new API and nothing to spell: a program observes a signal as the cancellation it already knows how to observe, so nursery cancellation runs, scoped finalizers run, a `std::db` transaction sends its `ROLLBACK`, and the process exits **130**. `Ctrl-C` is `SIGINT` and behaves the same way. **The second signal is forceful.** The runtime restores the default disposition and re-raises, so the process dies the way the platform says — a real `WIFSIGNALED`, not an `exit(143)` that prints the same number. The operator holds the deadline: there is no grace period in the language, because a runtime timer that disagreed with `TimeoutStopSec` or `terminationGracePeriodSeconds` would be the one number nobody configured. Measured, on a program with a scoped finalizer in a fallible loop: ``` kill -TERM -> exit 130; the finalizer ran kill -TERM twice -> killed by signal 15 mid-finalizer ``` ### What this does not cover **A `main` that reaches no cancellation point dies rather than unwinding.** Such a `main` — no loop, no blocking call, no call to a function that has one — has nowhere to stop, so the runtime uses the platform's default instead: default disposition, re-raised at once. The program still answers `kill`, at wait-status 143 with no finalizers. Any `main` that loops, serves or waits takes the graceful path, whatever its `raises` row. **A compute-bound call to foreign code still delays the shutdown.** A fiber is asked to stop at a cancellation point, and a single C call already in progress is not one: it finishes first. The status is the truth about the outcome and not about the latency. **A blocking `connect_to` is not a cancellation point.** `connect_to` is a blocking `connect(2)` on the worker, so a fiber inside one reaches no cancellation point until the kernel gives up — minutes, on an unroutable host. `accept` and `recv` are fine: they are reactor-driven, and an idle `Router::listen` server stops in about ten milliseconds. A non-blocking connect driven by the reactor would make it one, and it is not built. **Waiting for a child process is not a cancellation point.** `process.run`, `process.output` and `process.shell` wait for the program they started in one blocking call, so a fiber inside one stops only after that program exits. The program is not killed when the fiber is canceled; whether it should be is not decided. Put a bound on the program itself (`timeout 10 ...`) where that matters. **A change function that never returns cannot be stopped.** A `Shared::update` or `modify` change function holds the cell's lock, so nothing in it stops at a cancellation point -- not even after `Fiber::abort` or `cancel_within`. A blocking call inside one gives up, but a loop that never ends holds the lock, and the fiber, until the process ends. [Sharing](/docs/v0.4/reference/sharing/) has the rule. **Windows has none of this.** Windows has no `SIGTERM`, and no way for an arbitrary process to ask another to stop: `TerminateProcess` is `SIGKILL` with no notice and nothing to observe. The console events (`CTRL_C_EVENT` and its two siblings) are not wired up in this release. So a program that must not lose work should still be crash-only — durable state advancing by one atomic append or rename — because `SIGKILL`, a power cut and the cases above all remain. The ordinary deploy on Linux or macOS — one `SIGTERM` to a program whose `main` loops, serves or waits — is not one of them. [Running on Linux](/docs/v0.4/deployment/linux/) and [Containers](/docs/v0.4/deployment/containers/) say this in the setting where it bites. ## The fiber scheduler A fiber is an operating-system thread. The M:N scheduler — stackful coroutines on a worker pool — is built and is opt-in with `KHORA_FIBERS=scheduler`. It is not the default, for three reasons, and one of them is a gap rather than a preference: - Threads are faster at the connection counts a service runs at. - The scheduler exists for fiber **density**. A hundred thousand waiting fibers measured about 4 KB each on Linux as well as Windows, but on Linux each fiber's stack and its guard page are two memory mappings, so a process with more than about 32,000 fibers needs `vm.max_map_count` above the 65,530 many distributions set by default. Each fiber also reserves eight megabytes of address space for its stack, so a host with `vm.overcommit_memory=2` runs out of commit long before it runs out of memory. - It is the less-exercised path, and therefore the likelier home of the next runtime bug. The two backends are distinguishable under cancellation — see [The two fiber backends are distinguishable](#the-two-fiber-backends-are-distinguishable) below — so the default cannot change without a breaking-change note. **A known defect: on one CPU, the scheduler can hang a program in which a fiber blocks inside a `Shared` change function.** If a fiber inside `Shared::update` or `modify` waits on a channel, and the fiber that will send on it then needs the same cell, the scheduler hangs when the process may run on only one CPU — a one-vCPU container or VM, or `taskset -c 0`. With two or more CPUs it finishes. The thread backend finishes either way. Measured on x86-64 Linux with the scheduler pinned to one CPU: 10 of 10 runs hung, against 0 of 10 on two CPUs and on four. The change function holds its worker's thread while it waits, and with one worker nothing else can run the fiber it is waiting for. [Sharing](/docs/v0.4/reference/sharing/) already advises keeping blocking work out of a change function; on one CPU under the scheduler, that advice is required. ## I/O scaling on macOS and Windows This matters only under `KHORA_FIBERS=scheduler`. Under the default thread backend each fiber is an operating-system thread waiting on its own socket, and the ceiling is threads rather than the wait. Under the scheduler, a fiber waiting on a socket parks and one reactor wakes it when the socket is ready. **On Linux that reactor uses `epoll`**, which returns only the sockets that are ready. **On macOS and Windows it uses `poll`** (`WSAPoll` on Windows), which hands the kernel every watched socket on every wait, so the cost of one wait grows with the number of open connections. The scheduler exists for fiber density — many mostly idle connections — and that is exactly the case `poll` serves worst. A program built for tens of thousands of long-lived connections should run on Linux. macOS would need a `kqueue` backend and Windows an IOCP one; neither exists. ## Characters and strings A `String` is UTF-8 and is indexed in **bytes**. `String::slice` stops the program if a cut lands inside a character, so ask first: ```khora let safe = String::slice(text, 0, String::next_boundary(text, 20)); ``` `is_char_boundary`, `next_boundary`, `previous_boundary`, `char_at`, `chars` and `char_length` are the character-level API; `byte_length` is the constant-time one and `char_length` walks. The character predicates — `Char::is_digit`, `is_alpha`, `is_whitespace`, `to_upper`, `to_lower` — are **ASCII only** and say so in their own documentation. Unicode case mapping and the full `Nd` category are not in `std`, deliberately: they need tables that would double its size, and a library is the right place for them. ## Maps keyed by untrusted input **A `Map` whose keys come from a client can be made to take time in the square of its size.** `Map` is a hash table, and the hash of a `String` is FNV-1a with no per-process secret, so anybody can compute in advance a set of keys that all land in one bucket. Each insert or lookup then walks a chain as long as the map. Measured in a release build on x86-64 Linux: 8,192 colliding keys took 249 ms to insert against 10 ms for ordinary ones, and 16,384 took 1,032 ms against 17 — twice the keys, four times the time. The path a server meets it on is JSON: `std::json::parse` builds every object as a `Map`, so a request body whose member names were chosen to collide costs the server that time while it parses. A 1.7 MB object of 16,384 colliding names took 1.2 s to parse, against 25 ms for the same size with ordinary names. The default request limit of 8 KB keeps a body far below that. The [JSON API recipe](/docs/v0.4/cookbook/json-api/) raises it to 1 MB with `Router::holding(1048576)`; at that limit a 786 KB body of 8,192 colliding names took 299 ms to parse, against 15 ms for ordinary ones, on every request that sends it. What to do about it: keep `Router::holding` as low as the application allows, and for a structure keyed by client-chosen strings that can grow large, use `Dict`, which is ordered by comparison rather than by hash and has no worst case of this shape. ## Anonymous union types There is no way to write "an `Int` or a `String`" **inline**, as the type of a value, without declaring anything. A named [variant type](/docs/v0.4/reference/types/#variant-types) — a discriminated union, in other languages' words — is how "one of several" is expressed, and it is exhaustively checked: ```khora pub type Answer = | Number(Int) | Text(String); ``` What is missing is the anonymous form. Declaring `Answer` is the cost, and the compiler's exhaustiveness checking is what it buys. `+` joins the failure types of a `raises` row and means nothing outside one; `T: Eq + Show` is the other meaning of the symbol, a trait bound, and works as it does in Rust. Writing `Int + String` in a value's type is refused by name rather than by a parse error. The practical consequence is that `attempt` handles a body raising exactly one type. Use [`catch`](/docs/v0.4/reference/failures/#handle-failures-with-catch) for a wider row — it matches per type and never has to name a combined type. [The unions design note](https://github.com/codyspate/khoralang/blob/main/docs/design/unions.md) records what an anonymous union would mean, what it would cost, and why existentials are not part of the same question. ## A canceled fiber waiting on a child stops when the child does `Fiber::wait` and `Fiber::join` are cancellation points: a parent canceled while waiting stops there and does not run the statement after it, whether or not the child can fail. **But if the parent holds the last handle on the child, it does not finish stopping until the child has**, because the handle is released on the way out, and a canceled fiber releasing the last handle on another passes the cancellation on and waits for it. So that parent's latency is the child's: however long the child takes to reach its own next cancellation point. A child that reaches one often -- a loop, a call, a sleep -- stops at once. A parent that waits on a child somebody else also holds stops at once: its release is not the last, so it does not wait. Releasing the last handle *without* being canceled waits for the child and does not stop it: a `main` that returns while holding a handle on a fiber that never ends does not exit. Cancel it first. `Fiber::outcome` hands back an answer without unwinding the asker when the answer is wanted. ## `abort` can skip a finalizer bound to a name first `Fiber::abort` (and a `cancel_within` deadline that runs out) stops a fiber at its next cancellation point, cleanup included. A finalizer written directly as `Region::defer`'s argument always starts: ```khora Region::defer(region, fn () => give_back(conn)); ``` **A finalizer bound to a `let` first can be skipped whole** when the function around it also calls a function value -- a parameter, a closure, anything called through a variable: ```khora let finalizer = fn () => give_back(conn); Region::defer(region, finalizer); ``` Such a function's lambdas check for a stop as they are entered, so an abort fires at that check, before the finalizer's first line. Under a plain cancel it runs; under `abort` it ran 0 times in 20 on both backends. Write a finalizer in the `Region::defer` call. `std::db::transaction` and the `postgres` pool are written that way. ## Concurrency combinators A fiber carries its answer and its failure row — `Fiber`, with `join` re-raising what the child raised — and `Clock` can `sleep`. The combinators built on top of those do not exist: there is no `timeout`, no `race`, and no bounded parallel map. **Channel fan-in is concurrent.** Two 2000 ms fibers take about 2.1 seconds whether the parent reads their results off a `Channel::bounded(4)`, joins both handles, or uses `join_all` — measured three runs per backend on x86_64 Linux, and indistinguishable under `KHORA_FIBERS=scheduler`. At 500 ms per fiber over 25 runs the three are 518, 514 and 516 ms. **A deadline can be built by hand, and a `race` only partly.** Put the work in a fiber, sleep the deadline, `Fiber::cancel` the handle and `Fiber::wait` for it; [Give work a deadline](/docs/v0.4/cookbook/timeouts-and-cancellation/) has the program, and it returns within a few milliseconds of its deadline when the work has cancellation points. What such a deadline cannot do is stop work that has none — see the section above — and a hand-written `race` waiting on several fibers still has no way to wait on the *first* of them except through a `Channel` the branches send to. Two smaller things a supervisor meets on the way. `Fiber::wait` tells you nothing about how the fiber ended. `Fiber::canceled` answers whether the fiber was *stopped*, without waiting and without unwinding the asker, so a supervisor can tell a shutdown from a fault; `Fiber::outcome` goes one step further and hands back `Answered(value)` or `Stopped`, raising only when the child *failed*. A `join` on a canceled fiber still unwinds its caller, and at the entry point still ends the program at 130, so `outcome` is the call for a caller that wants the answer and tolerates a stop. A stopped fiber's partial progress is never handed back; to keep it, the child writes a `Shared` cell. And a fiber that raised without anybody taking its answer prints `khora: a fiber ended with an error nobody was waiting for` to standard error, once per such fiber, with no way to suppress it and no effect on the exit status. **The line is written when that fiber ends, not at process exit**, and neither `Fiber::wait` nor `Fiber::join` changes whether it appears: the fiber writes it itself the moment it stores its outcome, which is before any joiner can have taken that outcome. So **a failure the program joins and handles is reported too**, and the message's own wording is wrong about that case — somebody was waiting. What it reliably means is narrower than it says: *a fiber ended in a failure*. The timing is the part worth using. The line sits at the point in the output where the failure happened, so a line among a program's first few lines means something failed during startup — a listener that could not bind is the usual one. **A cancellation is excluded from it.** The runtime emits that line only for a fiber that ended in a *failure*; a canceled fiber is silent, so a supervisor that cancels children does not print it — it prints it only when a child failed on its own. That is what keeps the line worth reading: it is not the ordinary noise of a shutdown, and seeing one means a fiber somewhere raised. `Channel` also has no `select` (waiting on the first of several) and no zero-capacity rendezvous. `Channel::bounded(0)` gets a capacity of one rather than a rendezvous, deliberately. ## What a nursery actually does Four things about nurseries differ from what the [concurrency reference](/docs/v0.4/reference/concurrency/) describes. All were found by measuring, and a program built on the prose will meet them as flakiness. | what you write | what happens | what to do | | --- | --- | --- | | `bounded_nursery(4)` | 5 children run at once | subtract one when the limit is a real resource | | `bounded_nursery(0)` | **no limit at all** | check a computed limit before passing it | | a bound of exactly 1 | **not expressible** — `bounded_nursery(0)` is unbounded, `bounded_nursery(1)` admits 2 | accept 2, or guard the work with a `Shared` flag of your own | | a child fails | siblings usually keep running | have long work check a `Shared` flag itself | | a child fails in a bounded nursery | new children still start | as above | | `Fiber::cancel` on a fiber whose body is a nursery | **returns** — it is the following `Fiber::wait` that never does, if a child has no cancellation point | cancel through a `Shared` flag the children read | A nursery does still guarantee the other half: every child is waited for, and a failure is reported rather than lost. The measurements below are `khora 0.2.0 (b16417c)` on x86_64 Linux, 20–25 runs per backend, under both the default thread backend and `KHORA_FIBERS=scheduler`. The first, second, third and fourth rows were re-measured on a compiler built from the tree this page describes, one run per backend, with the same result: `bounded_nursery(1)` peaked at 2, `bounded_nursery(4)` at 5, `bounded_nursery(0)` ran all 24 children at once, and with the failing child adopted last all eleven siblings ran to completion. ### The bound is on children held, not work in flight `bounded_nursery(4)` runs five at once — the peak was exactly `limit + 1` on every run of both backends at limits of 1, 2, 4 and 8. `Fiber::spawn` *starts* the child and `nursery.adopt` is what blocks when the nursery is full, so by the time the bound applies the work is already running. [Bounded concurrency](/docs/v0.4/cookbook/bounded-concurrency/) is built on this number: `bounded_nursery(64, ..)` there is 65 live children. ### A limit of zero or less means no limit `bounded_nursery(0, ..)` is how the unbounded `nursery` is built, so zero is deliberate: 200 children ran at once under a limit of 0, and a negative limit behaves the same way. It is still a trap, because `Channel::bounded` does the opposite — it clamps a capacity below one *up* to one. A limit computed from configuration that comes out zero removes the bound rather than failing. ### A child's failure is seen late unless it was adopted early **Do not rely on a sibling's failure to stop work that is expensive, holds a resource, or has an effect outside the process**, unless the child that can fail is adopted before the siblings it should stop. A nursery reaps handles oldest-first, so a failure is not seen until every child adopted before it has finished; the siblings still running at that point are canceled. Twelve children busy for 400 ms, one raising after 10 ms, on both fiber backends: | the failing child was adopted | siblings canceled, of 11 | `ChildFailed` arrives | | --- | --- | --- | | first | 11, in every run | about 10 ms after the start | | in the middle (6th of 12) | 2 to 6 | after the earlier five finish | | last | 0, in every run | about 405 ms after the start | With the children in `clock.sleep(400)` instead of busy the shape is the same, and a middle-position failure occasionally cancels none. At the last position every sibling runs to completion before the failure is reported. ### New children start after a sibling has failed With `bounded_nursery(3)` over twelve 200 ms children and a failure at 11 ms in the second, a median of 6.5 of the 10 remaining children were *started* after the failure was recorded, and the group did about 400 ms of further work past it. ## The two fiber backends are distinguishable Under cancellation the two backends answer alike: a fiber in `clock.sleep`, on a channel, on a socket read or on another fiber is woken by a cancellation on either one, and stops there. What still differs is scheduling -- which fiber runs when, and how many run at once -- so a program that depends on an order the language does not promise can see which backend it has. Cancellation unwinds at a point, not between arbitrary instructions -- [Concurrency](/docs/v0.4/reference/concurrency/) has the model and the list of points. ## Cross-compilation and WebAssembly The compiler can emit an object for a triple it cannot link, and [Supported targets](/docs/v0.4/deployment/supported-targets/) lists which triples have been carried all the way to a running binary. Only those are called supported. WebAssembly needs more than a triple: a Worker or a browser has no filesystem and no sockets, so `std` would need a platform surface shaped for the host. Cloudflare Workers in particular is **not** an experimental target — none of the pieces exist, and [that page](/docs/v0.4/deployment/cloudflare/) says what they would be. ## Stability Khora has not reached 1.0. Source compatibility across arbitrary development revisions is not promised. Pin the toolchain version for applications where reproducible builds matter, and review migration notes when deliberately moving between incompatible releases. [Compatibility and stability](/docs/v0.4/reference/compatibility/) is the policy: what a `0.x` release promises, what counts as a breaking change, and the four things 1.0 is waiting for. ## Reporting a limitation If the documentation says something should work and the compiler disagrees, treat that as a bug in either the implementation or the docs. Every hand-written example on this site is compiled as a step of the project's build gate, and the generated API pages are checked against their declarations by `khora doc --check`. Two gaps remain. A hand-written fragment is *parsed* rather than type-checked unless it declares its own `module`, so an example can be syntactically valid and still mean the wrong thing — `List Int, unix_millis: () -> Int, monotonic_millis: () -> Int, sleep: (Int) -> (), } ``` What time it is. A capability rather than plain functions, and separate from `Env` because it is a different thing to deny: a library that may read a configuration variable is not thereby a library that may know how long something took, and a test that pins one usually wants to pin the other differently. **Two clocks, not one, and the split is not a detail.** A wall clock says what time it is and can jump: NTP steps it, an administrator sets it, a virtual machine resumes with a stale one -- and it can jump *backwards*, so subtracting two readings of it is not a duration, it is a guess. A monotonic clock cannot go backwards and is what "how long did this take" actually wants; its zero is arbitrary, so it can never be a timestamp. One `millis` would therefore have been the wrong one about half the time, and the half it was wrong for would be the half that only fails in production, at 2am, when the clock is stepped an hour. Naming both makes the caller say which they meant. #### unix_seconds ```khora unix_seconds: () -> Int ``` Seconds since 1970, on the wall clock. Kept, because it is what a caller who wants a date wants, and it is derived from the same source as `unix_millis` so the two can never disagree about what second it is. #### unix_millis ```khora unix_millis: () -> Int ``` Milliseconds since 1970, on the wall clock. A timestamp, not a duration. #### monotonic_millis ```khora monotonic_millis: () -> Int ``` Milliseconds on a clock that only goes forwards, counted from the first time anything in the process asked. The one to subtract. Two readings are always ordered and their difference is always a real elapsed time — which is exactly what `unix_millis` cannot promise. #### sleep ```khora sleep: (Int) ->() ``` Waits this many milliseconds before going on. **An operation on the clock, and that is the whole design.** Waiting is the one thing a program does that a test cannot afford to actually do, and putting it here means a fake clock is an ordinary handler -- no test runtime, no special mode, no rule about which fiber may advance time. The alternative is what every language with an ambient sleep ends up building: a parallel clock the runtime knows about, and documentation that has to open by warning you to fork the sleeping code or deadlock. The capability *is* the seam, which is the argument `Random::seeded` already makes about the other unrepeatable input. Zero or less returns at once. A caller who meant "let somebody else run" wants a safepoint, and every loop back-edge already has one — and every back-edge is a cancellation point too, so a `loop { sleep(short) }` worker stops when it is asked to. **A sleep is a cancellation point.** A fiber inside `sleep` is woken by a cancellation on either fiber backend and stops there; the statement after the sleep does not run. ## Methods ### Clock ```khora impl Clock ``` #### real ```khora pub fn real() -> Clock ``` The real clock. Seconds are milliseconds divided down rather than a second call to ISO C's `time`, so that a program reading both never sees them straddle a tick. # std::config Source: https://khoralang.com/docs/v0.4/stdlib/api/config/index.md Settings read out of the environment, through a schema, and reported all at once. The environment is a source like any other: a JSON body, a query string and a database row are all read by one `Schema`, and so is this. What this module adds is the walk -- which variables a shape needs, and what to call them -- and the report, which spells a path the way a deployment does. ```khora derive(Show, Decode) type Listen = { host: String, port: Int }; derive(Show, Decode) type Settings = { listen: Listen, password: Redacted, debug: Option }; fn settings() -> Validated with { env: Env } { read(Settings::schema()) } ``` reads `LISTEN_HOST`, `LISTEN_PORT`, `PASSWORD` and `DEBUG`, and answers every problem rather than the first: a person fixing a deployment wants the list, not one line of it per restart. ## How a shape becomes variable names A field is its name in upper case; a nested record's fields are joined to it with `_`, so `listen.port` is `LISTEN_PORT`. A list is one variable split on commas, `TAGS=a,b`. A variant is one variable holding the case, `MODE=Remote`, and its payload fields beside it, `MODE_URL`. A key given by `key("http_host", ..)` replaces the field's name in that scheme and nothing else. An optional nested record is present when any of its variables is. Every variable a shape needs is [`variables`](#variables), which is the question a deployment asks and a program should not have to be started to answer. ## Why every value arrives `Untyped` The environment cannot say whether `8080` was meant as a number, so it does not: every value is `Raw::Untyped`, which every primitive reads. Strictness is a fact about the source, and this source has none to offer. ## Denied is not missing A variable the manifest does not grant is a different thing from one nobody set, and the fix is a line in `khora.toml` rather than a line in a deployment script. `Env::variable` raises for it; this catches the raise into `Raw::Denied`, which decodes as `Problem::Denied`, and [`report`](#report) names the file. ## Functions ### read ```khora pub fn read(schema: Schema) -> Validated with { env: Env } ``` Reads the settings a schema describes, every problem reported. The schema's shape decides which variables are read and what they are called; the schema's decoder decides what they mean. A record schema is what this is for: a bare `int()` at the top level has no name to read. ```khora import std::config::{read, report}; import std::env::{Env}; import std::schema::{Decode}; derive(Decode) pub type Settings = { listen_port: Int, database_url: String }; pub fn main() -> Int { with { env: Env::real() } { match read(Settings::schema()) { Validated::Valid(settings) => start(settings), Validated::Invalid(problems) => { print(report(problems)); 1 }, } } } ``` The shape decides the variable names: `listen_port` is read from `LISTEN_PORT`. A record written once with `derive(Decode)` reads from the environment, a request body and a test fixture alike, which is what it means for the schema to be a property of the type. ### variables ```khora pub fn variables(shape: Shape) -> List ``` The variables a shape needs, in the order they would be read. `[LISTEN_HOST, LISTEN_PORT, PASSWORD, DEBUG]` for the settings above. ### describe ```khora pub fn describe(problem: Rejection) -> String ``` One line a person can act on, with the path spelled as the variable it came from: `LISTEN_PORT should be a whole number, and is "eighty"`. ### report ```khora pub fn report(problems: List) -> String ``` Every reason, one per line, in the order they were found. What a service prints just before it stops, and the reason the whole module answers `Validated`: this list is allowed to have more than one thing in it. # std::core Source: https://khoralang.com/docs/v0.4/stdlib/api/core/index.md The vocabulary every Khora program starts with. `Option` and `Result`, the comparison and display traits, lists, arrays, strings, vectors, maps and dictionaries, and the pieces structured concurrency is built out of -- regions, fibers, nurseries and `Shared`. Nothing here is optional in the way the rest of `std` is: two libraries that disagree about what an `Option` is cannot exchange one. ## Where the effects went Khora tracks effects as rows on the signature rather than as a wrapper type, so there is no `Effect` here. A computation's requirements live in its `with` clause and its failures in its `raises` clause; both are inferred on private functions and written out on exported ones. The module is `std::core` rather than `std::effect` because `effect` is a reserved word, so `std::effect` will not lex as a path. ## Types ### Ordering ```khora pub type Ordering = | Less | Equal | Greater; ``` The result of comparing two values. A three-way answer rather than two booleans: one comparison decides all six operators, and a type cannot accidentally disagree with itself about `<` and `>=`. ### Option ```khora pub type Option = | Some(value: A) | None; ``` A value, or nothing. For absence that is ordinary and expected: a key that is not in a map, the head of an empty list. When the absence is a *failure* — something went wrong and the caller should know what — the type is `Result`, and `ok_or` below is the bridge between them. ### Result ```khora pub type Result = | Ok(value: A) | Err(error: E); ``` A value, or the reason there is not one. **The same thing a `raises` row says, said as a value.** A tagged return already *is* "an error or a value"; which of the two readings a function offers is a choice about its caller, and `attempt` is where one becomes the other. ### Redacted ```khora pub type Redacted = | Secret(value: A); ``` A value that a log line cannot get at. A password, a token, an API key. The leak everybody actually has is not a clever one: it is a `Config` record in a structured log, or a `${}` in a message written while debugging something else, and no review catches it because nothing about the line looks wrong. **`Show` prints ``, and there is no `Encode`.** Those two sentences are the whole design, and each is deliberate in a different direction: - *With* `Show`, a record holding one still derives `Show`, so the config a service prints at start-up stays printable and the secret in it does not appear. A `Redacted` with no `Show` at all would look stricter and be worse: it makes the containing record unprintable, and the way people answer that is by not wrapping the secret. - *Without* `Encode`, a record holding one does **not** derive `Encode`, and the build stops. That is the right place to stop: a type that serializes a secret is a bug, and one that serializes `""` is a payload that fails to round-trip somewhere further away. It still derives `Decode`, because a secret arrives from somewhere: it reads, and does not write. This is where a nominal type earns its keep. Elsewhere the same idea is a `toString` override, which any structured logger walks straight past because it reads fields rather than calling it. Here it is a missing impl, which is a compile error. ```khora derive(Show) type Config = { host: String, password: Redacted }; let settings: Config = { host: "db.internal", password: Redacted::of(secret) }; print(Show::show(settings)); // Config { host: db.internal, password: } connect(Redacted::expose(settings.password)); ``` Interpolation is covered by the same `Show`. `"${key}"` calls it like any other hole, so the line compiles and prints ``. **Nothing needs `expose` to print one.** `expose` hands over the secret itself, which is the shortest route into a log for somebody who only wanted their message printed; a hole that already redacts removes the reason to reach for it. So every way a value reaches a log -- `show`, a derived `Show` on something holding it, and a hole -- goes through the one implementation, and there is no second thing to keep in step with it. **Deliberately no `Eq`.** Comparing two secrets is a real thing to want and a derived `Eq` would compare them byte by byte and stop at the first difference, which is how a timing side channel gets written by somebody who was not writing one. Anybody who needs the comparison writes `expose` and says so. ### Validated ```khora derive(Eq, Show) pub type Validated = | Valid(value: A) | Invalid(errors: List); ``` Every reason the input was wrong, rather than the first one. `Result` is the right shape when the next step depends on the last: it stops, because there is nothing sensible to do with a value that is not there. Validation is the other shape. The fields of a form, the keys of a config, the columns of a row -- these do not depend on each other, and reporting them one restart at a time is a person running the program five times to be told five things it knew the first time. `Applicative::map2` is the operation that distinguishes the two, and `std::core`'s own docstring on it has been pointing here since it was written: *"`Option` gives up if either is `None`, and a validation type would collect both failures."* ```khora let settings = Validated::map2( integer("PORT"), string("HOST"), fn (port, host) => Settings { port: port, host: host }, ); ``` **Not an `Applicative` instance**, though it is one in shape. The instance would need `Validated<_, E>` -- the error parameter fixed and the value parameter free -- and Khora has no partial application of a type constructor. The methods are the same functions under their own names, and the instance can arrive later without changing a call site. ### List ```khora pub type List = | Nil | Cons(head: A, tail: List); ``` A singly-linked list: the sequence with no capacity, no reallocation and no index. Cheap at the front and linear everywhere else. `Vector` is the one to reach for when the work is indexing or appending; this is the one to reach for when the work is "each of these, in order", which is most of it. `[a, b, c]` builds one — the literal is `Cons(a, Cons(b, Cons(c, Nil)))` and needs `List` in scope, exactly as `0.01d` needs `Decimal`. ### Halves ```khora pub type Halves = { pub left: List, pub right: List, }; ``` A list split down the middle, for `sort`. ### Parts ```khora pub type Parts = { pub kept: List, pub rest: List, }; ``` A list split by a question, for `partition`. Named rather than reusing `Halves`, whose `left` and `right` say where a piece came from and not what is true of it. A caller of `partition` reads `.kept` and knows which side answered yes; reading `.left` they would have to go and look. ### Step ```khora pub type Step = | Yield(state: S, item: A) | Done; ``` One step of an iteration: the next item and the rest, or the end. Khora has no mutable references, so Rust's `fn next(&mut self) -> Option` is not expressible: there is no way to hand `next` something it may write through. An iterator hands back its successor instead. `for` desugars to this and reads the same. This said "no mutable references and no mutable fields" until `mut` fields arrived -- `Map` is built out of one, `Vector` out of two -- and the second half stopped being true without the first half needing to change. It is the *reference* that is missing, not the mutation. **This is the allocation `for` pays per element.** A `Step` is an ordinary heap object, so a `for` loop is about 3.6 times slower than the same loop written out -- measured in `bench/iteration`. Unboxing is what would close that; desugaring `for` to a closure would not. ### Range ```khora pub type Range = | Of(from: Int, to: Int); ``` `Range::Of(from, to)` yields `from` up to but not including `to`. ### Mapped ```khora pub type Mapped = { inner: I, f: (I::Item) -> B }; ``` `Iterator::map(it, f)`: the same walk, with each item put through `f`. ### Filtered ```khora pub type Filtered = { inner: I, keep: (I::Item) -> Bool }; ``` `Iterator::filter(it, keep)`: the items `keep` answers true for. ### Taken ```khora pub type Taken = { inner: I, left: Int }; ``` `Iterator::take(it, n)`: at most the first `n` items. ### Never ```khora pub type Never; ``` The empty type. A function whose return type is `Never` does not return normally — it raises, diverges, or transfers control to a handler. **The bottom type, which means a `Never` branch takes the other branch's type.** `if index < 0 { stop(index) } else { xs }` is an `A` when `xs` is, including when `A` is a type the caller chose — the branch that cannot return has nothing to disagree with. The name is the compiler's, the way `Int` and `String` are, so it needs no import. This declaration is where it is documented and what an `import` naming it resolves against; nothing resolves *to* it. It is the solver's bottom — the type a `raise` already has. ### Region ```khora pub type Region; ``` A region, and the finalizers deferred to it. The type is here rather than hidden because `scoped` is written in Khora and has to name it. Reference counted like everything else — which is the whole mechanism: releasing the last reference runs the finalizers, and the paths that release a binding are already the paths a region has to end on. ### Fiber ```khora pub type Fiber; ``` A running fiber, and the only way to wait for one or stop it. Releasing a handle *joins*, so a fiber cannot outlive the binding that holds it — on every path out, including a raise passing through. Put one in a region and the region waits; put it in a block and the block does. That is where structured concurrency comes from, and it needed nothing of its own. ### Outcome ```khora pub type Outcome = | Answered(value: A) | Stopped; ``` What a fiber ended as, when the asking must not unwind the asker. `Fiber::outcome` produces one. `Answered` carries what the fiber computed; `Stopped` says a cancellation reached it and carries nothing, because a fiber that was stopped has no answer to carry. **`Stopped` is not a failure.** A child that *failed* raises on the fiber's own row and never appears here — which is what keeps a failure the caller's to interpret rather than one arm among two. ### Array ```khora pub type Array; ``` A fixed number of elements, side by side. Fixed because growing is a *library* question and this is the primitive one is built out of: `{ mut items: Array, mut len: Int }` is a vector, and it can be written in Khora now that a field can be written. Reading an element is a bounds check and a load — no call — because the layout is a contract with the code generator rather than something the runtime hides. What the runtime does own is allocation and release, both of which need the length at run time. ### Split ```khora pub type Split = { pub head: String, pub rest: String, }; ``` What `split_once` found: the text before the separator, and everything after it. ### Ptr ```khora pub type Ptr; ``` An opaque machine address: a C `void *`. **Not counted, not dereferenceable, and never a pointer into Khora's own heap.** It exists so a foreign library can hand back a handle — a `FILE *`, an `SSL_CTX *` — and be given it again later. Nothing in the language turns a Khora value into one, which is what makes a dangling `Ptr` impossible: every pointer that exists came from the other side, and its lifetime is that side's business. Lending a *buffer* across is the harder question and this type deliberately does not answer it. ### Chain ```khora pub type Chain = | Empty | Node(key: K, value: V, rest: Chain); ``` One bucket's worth of entries: the keys that hashed to the same slot. Separate chaining rather than open addressing, because a chain is an ordinary recursive ADT and needs no tombstones, no sentinel value and no second array saying which slots are live. It allocates per entry, which is the price, and it is the shape the language is actually good at. ### Pair ```khora pub type Pair = { pub key: K, pub value: V, }; ``` One key and the value under it. ### Least ```khora pub type Least = { key: K, value: V, rest: Dict, }; ``` The smallest entry of a tree, and what is left without it. ### Dict ```khora pub type Dict = | Empty | Node(size: Int, key: K, value: V, left: Dict, right: Dict); ``` An ordered map that is never written in place. `Map` is the other one: a hash table that mutates its buckets, which is fast and is exactly why it cannot cross a fiber or go in a `Shared`. This is built out of ordinary variants, so it holds nothing writable and is shareable in the ordinary structural way — a `Shared>` is how a cache several fibers use is spelled. Persistent, so an insert gives back a new map and the old one is still there. The two share everything the insert did not touch, which is what makes that affordable: one path from the root is rebuilt, and the rest of the tree is the same objects with one more reference each. Weight-balanced (Adams), so every operation is logarithmic in the size and the recursion here is bounded by the depth rather than by the length — the distinction that `String::slice` got wrong and paid for. ### Map ```khora pub type Map = { mut buckets: Array>, mut count: Int, }; ``` A hash table: unordered, mutated in place, and the fast one. **`Map` mutates and `Dict` does not**, which is the whole choice between them. Insert into a `Map` and the map you were holding has changed; insert into a `Dict` and you get a new one and the old is untouched. So a `Map` cannot go into a `Shared` and cannot cross into a fiber, and a `Dict` can. Keys need `Hash`, and `Hash` requires `Eq` because the two have to agree — see the trait. Entries come back in no particular order, and not even a stable one: growing rehashes everything. ### Vector ```khora pub type Vector = { mut items: Array, mut len: Int, mut wanted: Int, }; ``` A sequence that grows: an array, the part of it that is in use, and a reallocation when those two meet. The container to accumulate into. `Array` is fixed at the length it was allocated with, so a caller that does not know the count in advance cannot use one; `List` grows, but at the front, one allocation and one pointer chase per element, and indexing it is a walk. This is contiguous, indexed in constant time, appended to in amortized constant time, and — the point of the whole exercise — *a constant number of allocations for any number of elements*. Fiber-local, like `Map` and for the same reason: the `mut` fields are what make `push` cheap, and they are exactly what the sharing rules refuse to a second fiber. A sequence several fibers write is a `Shared>`, which is slower and says so. **Why `new` takes no seed element.** `Array::new` needs a value to fill with, and there is no value of an arbitrary `A` to be had: `Vector::new()` has nothing in its hands, and neither has `with_capacity`, whose whole purpose is to allocate *before* there is anything to put in it. A vector of a hundred integers is two live objects — the vector and its array — at that length and at every other. Allocating per element is precisely the property a vector exists to *not* have, and it is the reason to reach for one over a `List`. Holding `Array>` would cost a `Some` per element and give that property away. The other way out is a `new` that takes a seed element, which is C++'s value-initialization: it turns "give me somewhere to collect results" into "produce a result first", and a type with no cheap value has to fabricate one. The answer is `Array::empty`, which is a zero-length array and needs no fill because it has no cells. The storage starts there and costs nothing; the first `push` allocates for real, and it can, because *the element being pushed is the value to fill with*. Every cell of a fresh array holds that element until a later push overwrites it, which for a boxed `A` is one reference count per cell rather than one allocation per cell, and for an `Int` is nothing at all. `with_capacity` is the field that falls out of this: it cannot allocate when it is called, so it records what was asked for in `wanted` and the first `push` honors it. **A capacity is a promise here, not storage** — `capacity` answers with what is really allocated, so a vector that has been asked for a thousand cells and pushed to zero times still reports nought. The alternative, reporting the promise, would be a number no memory corresponds to. What this costs against the `Option` cells is eager release. There is no "empty" to write into a cell a `pop` vacated, so `pop` writes the vector's first element there instead — a value of `A` it is guaranteed to have — which releases the popped one just as promptly and costs a reference count. Popping the last element and `clear` both drop the array outright, which releases everything at once and is why neither keeps the storage; `wanted` remembers the size, so refilling reallocates exactly once. #### wanted ```khora mut wanted: Int ``` How big the first allocation should be. Meaningless once there is one. ### SharedFn ```khora pub type SharedFn; ``` A function two fibers may hold. A closure is never shareable on its own: what it captured is not in its type, so no amount of looking at `(Request) -> Response` says whether the thing behind it can be written. That is fine until a container of closures has to cross — a router holding its handlers, a table of callbacks — at which point the whole container is stuck on this side. This is the proof, reified. `SharedFn::of` accepts only a closure written at the call, where the captures are on the screen and can be checked; what it returns has forgotten how it was made, and is shareable because the check already happened. A `Route` holding one is shareable in the ordinary structural way, with nothing special said about routers. The cost is the wrapper at the mount site, and that is the honest price: the alternative is a bit in every function type, which colors every container of a function all the way up. ### Changed ```khora pub type Changed = { pub state: A, pub result: B, }; ``` A new state, and something to hand back that is not the state. What `Shared::modify` asks its change function for. A record rather than a tuple -- `Validated::zip` shows a tuple literal is perfectly writable -- because named halves read better than positional ones at the call, and the two halves here are the two things a caller is most likely to swap. ### Channel ```khora pub type Channel; ``` A bounded queue one fiber puts values into and another takes them out of. **`Shared` is a cell; this is a hand-off.** The two answer different questions and the difference is what a value *is* while it is in flight. A cell holds one value that anybody may read or replace. A channel holds a queue of values that each go to exactly one taker, and — the part that matters — a fiber waiting at either end **gives its worker back** rather than spinning or blocking a thread. #### What it is for Three things, and they are the same thing seen from different sides. **Owning a resource that cannot be shared.** A handler may not capture anything writable, and a database connection is writable *and* strictly serial — two fibers writing one socket interleave their frames. So one fiber owns the connection and the others send it requests. The handler captures the channel, which is `Share`, and never the connection, which is not. A database driver is the shape this is for. **Backpressure.** A full channel stops the sender until a taker catches up. That is not a limitation to be worked around; it is how a service under more load than it can serve declines work at the edge instead of queueing it until memory runs out. **A pool.** A channel holding the four idle workers *is* the pool: taking one is a receive, giving it back is a send, and waiting for one is what the channel already does. #### Closing `close` says nothing more will be sent and releases everybody waiting. A receive then drains what is already queued **before** answering `None`, because values already sent are still worth having. Closing twice is allowed: the fiber that owns a channel and the fiber that finishes with it are often different code, and neither should have to know which went first. #### What it does not do **No rendezvous.** `bounded(0)` gets a capacity of one rather than a channel where a send waits for a receive. Zero is a useful thing and it is not this thing; building it here would mean a sender waiting on a receiver that is waiting on a sender, and getting that wrong is a hang rather than an error. **No select.** Waiting on the first of several channels wants a primitive of its own, and nothing has needed one yet. ### Handoff ```khora pub type Handoff; ``` A queue that gives values away. **What it is for: handing a value that can be written to another fiber.** A `Channel` shares what it carries, so it carries only `Share` values. A hand-off carries anything, because a send gives the value away: the sending fiber keeps nothing, so the receiver is the only fiber that can reach it. A database connection a pool lends out is the case it exists for. **What the send requires.** The value sent must be the last use of it, and nothing writable it reaches -- a record with a `mut` field, an `Array`, a `Map` -- may be held by anything else. The compiler checks the first. The second is checked when the send runs, by reference count: a send that finds a writable part still held elsewhere **stops the program, naming the type**, rather than let two fibers hold one writable object. A part of the value that is `Share` may be held elsewhere. It cannot be written, so it is marked for the receiving fiber the way a channel send marks what it carries. **What trips the check.** Anything the sending fiber still uses after the send that holds a writable part: a binding read again later, or a value the sent one was taken out of that is read again later. ```khora let got = connect(settings); match got { Result::Ok(c) => Handoff::send(pool, c), Result::Err(_) => false, }; log(got) ``` stops the program at the send, because `got` still holds `c` for the `log` after it. Sending a `match` arm's binding is fine when the value matched on is not used again. The stop happens on every send of a shape that trips it, not by chance, so a test that runs the path finds it. **What it costs.** A walk over every object in the value that is not already shared, at every send: about 2,400 instructions for a record of a dozen objects. Receiving costs what a channel receive does. Like a channel's, a send and a receive are cancellation points, a full hand-off makes the sender wait, and `close` releases everybody waiting. ### Shared ```khora pub type Shared; ``` A value two fibers may both change. The sharing rules refuse a mutable record to a second fiber on purpose, and refuse a handler that captures one, because two fibers writing one value is a race. This is what they refuse them *until*: a cell holding a value, with the reads and writes serialized. **Mutation becomes replacement.** There is no borrow of the inside — a counter is `Shared::update(count, fn n => n + 1)`, not `count.n = count.n + 1` — and that is the whole of why this is safe rather than careful. Handing out the inner value would mean stopping it escaping the critical section, and without lifetimes there is no way to say that. Nothing unshareable goes in or comes out, so there is nothing to leak. The contents must be `Share` for the same reason, which also means a `Map` cannot go in one: it mutates its buckets in place. `Dict` is the ordered persistent map, and is what a shared table is made of. ### Fibers ```khora pub type Fibers; ``` A nursery: the fibers started under it, and the guarantee that none of them outlives it. Releasing one cancels its children and waits for them, which is where structured concurrency comes from — a fiber cannot escape the block that started it, because the binding holding the nursery ends on every path out. `nursery` is the ordinary way to get one. One value means both "wait for the children" and "the answers are no longer wanted", without ever being told which of the two it is: on the ordinary path `nursery` has already waited, so the release finds nothing left to stop. ### ChildFailed ```khora pub type ChildFailed = { pub children: Int }; ``` Raised by a nursery when one or more of its children ended with an error. **Not the child's own error, because there is no such single thing.** A nursery holds `Fiber<(), 'er>` with the row quantified per adoption, so two children may fail with two unrelated types and the nursery has nowhere to put either — `Nursery` says why. What survives the handles is the count, so that is what this carries. A child that a nursery *canceled* is not a failure and is not counted: cancellation is what a nursery does to its children on the way out, and counting it would make every early exit look like a fault. ```khora match attempt(fn () => nursery(gather)!) { Result::Ok(answer) => answer, Result::Err(failure) => retry_smaller(failure.children)!, } ``` ## Traits ### Eq ```khora pub trait Eq ``` Whether two values of a type are the same value. Structural, and written rather than derived for a type whose identity is not its contents. Nothing here defines `!=`: one operator is one thing to get right, and the negation is the negation. **`Float` is not an instance**, because `NaN == NaN` is false and a type whose values are not equal to themselves breaks every container built on this one. [The numbers design note](https://github.com/codyspate/khoralang/blob/main/docs/design/numbers.md) has the argument. #### eq ```khora fn eq(self, other: Self) -> Bool ``` Whether these are the same value. ### Ord ```khora pub trait Ord: Eq ``` Ordering refines equality: anything orderable can be compared for equality, so `Ord` requires `Eq` rather than repeating it. #### cmp ```khora fn cmp(self, other: Self) -> Ordering ``` Where `self` falls relative to `other`. Must agree with `eq`: `cmp` answering `Equal` and `eq` answering false is a sort that loses elements and a search that cannot find them. ### Hash ```khora pub trait Hash: Eq ``` A value that can be a key. Requires `Eq` rather than repeating it, because the two have to agree: a map finds a key by hashing it to a bucket and then comparing, so **equal values must hash equal**. Unequal values may collide — that is what the bucket's chain is for — but two values that `eq` says are the same and `hash` sends to different buckets are an entry that can be inserted and never found. `Float` is deliberately not an instance, because it is not an instance of `Eq`: `NaN == NaN` is false, and a key that is not equal to itself is a lost entry in every language that allows one. #### hash ```khora fn hash(self) -> Int ``` A number to bucket by. Any number: spreading it out is the map's job, not this one's, so an implementation only has to be *consistent*. ### Show ```khora pub trait Show ``` A value rendered for a person to read. **Not a serialization format.** `show` is for a log line, an error message and a test's failure output; `std::json` is for something that has to be read back. Nothing promises that `show` round-trips, and `Float` is the one place it happens to — see [the compatibility design note](https://github.com/codyspate/khoralang/blob/main/docs/design/compatibility.md), which makes that a promise about `Float` rather than about `Show`. #### show ```khora fn show(self) -> String ``` This value, as text. ### Share ```khora pub trait Share ``` Two fibers may hold this at once. A marker: no methods, and implementing it asserts rather than provides. Nothing has to write it for an ordinary type — a record, a variant, a tuple is shareable exactly when everything inside it is, and the compiler can see that. This is for the types it *cannot* see inside: one declared with no body, whose behavior lives in the runtime or across the C ABI. Those default to unshareable, which is the only safe way round. `Array` can be written through `Array::set` and `Ptr` points at memory this language did not allocate; both look like empty structures and neither is safe for two fibers to share. So the author says which is which, and the reader can see where the assertion was made. Fibers are operating-system threads today, so this is `Sync` rather than `Send`: both fibers keep their reference, and neither hands it over. ### Iterator ```khora pub trait Iterator ``` Something a `for` loop can walk. One method, and it hands back a successor rather than mutating: see `Step` above for why that shape rather than Rust's. Implementing it is what makes a type usable in `for`. #### Associated types ##### Item ```khora type Item; ``` What the iteration yields. ##### Effects ```khora type Effects; ``` **What pulling from it requires**, which is what makes this one trait rather than two. `Effects = {}` is an ordinary iterator over something already in memory: `Range` and `List` below, and a `for` loop over either asks its enclosing function for nothing. `Effects = { fs: FsRead }` is a stream of the lines of a file, and a `for` loop over *that* requires `fs` of whoever wrote it, because pulling the next line reads the disk. Rust needs `Iterator`, `Stream` and `AsyncIterator` for this, and Effect needs `Iterable` and `Stream`, because in both of them an effect changes a function's *type*. Here it is a row, and a row can be empty -- so one trait, one set of combinators, written once. #### Functions ##### next ```khora fn next(self) -> Step with Self::Effects ``` The next item and the iterator that follows it, or `Done`. ##### map ```khora fn map(self, f: (Self::Item) -> B) -> Mapped ``` Each item put through `f`. Nothing is pulled until something asks. ##### filter ```khora fn filter(self, keep: (Self::Item) -> Bool) -> Filtered ``` The items `keep` answers true for. ##### take ```khora fn take(self, n: Int) -> Taken ``` At most the first `n`. ##### fold ```khora fn fold(self, init: B, f: (B, Self::Item) -> B) -> B with Self::Effects ``` Walks to the end, carrying `init` through `f`. **The consumer is where the effects happen.** `map` and `filter` build a description and require nothing; this is what pulls, so this is what carries `Self::Effects` -- which is why a pipeline over a file asks for `fs` at the fold and not at the `map`. ##### each ```khora fn each(self, f: (Self::Item) ->()) ->() with Self::Effects ``` `f` on each item, for the effect rather than the answer. ##### count ```khora fn count(self) -> Int with Self::Effects ``` How many items it yields. Walks the whole thing, so it is not free on a stream that reads a socket. ### Functor ```khora pub trait Functor ``` A container whose contents can be replaced one for one. **Higher-kinded**: `Self` is applied to a type here rather than being one, which is what lets a single `map` serve `Option`, `List` and everything else with a shape. `docs/vision.md` names this as a non-negotiable, and it is the thing Rust's trait system cannot say. #### map ```khora fn map(self: Self, f: (A) -> B with 'ef raises 'er) -> Self with 'ef raises 'er ``` Every element replaced by `f` of it, in the same shape. **`f` may fail and may need capabilities**, and the rows say so rather than the signature quietly forbidding it. Without them the only way to map a fallible function over a container was to write the walk out by hand, which for a language whose headline is typed failure is the collection library and the failure system declining to compose. Variables, so nothing that maps a pure function has to change: they instantiate empty and the call site keeps its shape. ### Applicative ```khora pub trait Applicative: Functor ``` `pure` lifts a value; `map2` combines two containers. `map2` rather than `ap` because `ap` would need a container of *functions*, and reading a signature is easier than reading `Self<(A) -> B>`. #### pure ```khora fn pure(value: A) -> Self ``` One value, in the smallest container that can hold it. #### map2 ```khora fn map2(self: Self, other: Self, f: (A, B) -> C) -> Self ``` Two containers combined element-wise by `f`. What "combined" means is the instance's to decide: `Option` gives up if either is `None`, and a validation type would collect both failures. ### Traversable ```khora pub trait Traversable: Functor ``` Walks a structure left to right, collecting the effects of each step. This is the abstraction Rust cannot express and the reason higher-kinded types are a non-negotiable in `docs/vision.md`: one `traverse` per container serves every `Applicative`. #### traverse ```khora fn traverse(self: Self, f: (A) -> F) -> F> ``` `f` over every element, with the containers turned inside out. A `List` and an `(A) -> Option` give an `Option>`: one answer for the whole walk rather than a list of answers. That inversion is the whole point — it is how "all of these, or the first reason not" is written once instead of per container. ## Effects ### Scope ```khora pub effect Scope { defer: (() -> ()) -> (), } ``` Resource lifetimes. A finalizer registered with a scope runs when the enclosing region ends, however it ends: falling off the end, an early `return`, or a raise passing through. **A scope stays on the fiber that opened it**, so a finalizer runs on the fiber that deferred it and may capture a record with `mut` fields that fiber goes on writing. A fiber's body cannot capture one, nor can a handler for another effect; a child is given its own with `scoped(work)`. The operation takes only a thunk. A handler's fields are ordinary closures and a closure is monomorphic, so an operation cannot be generic — and it need not be: `acquire` below is an ordinary generic function that closes over the value and the release, which is where the polymorphism belongs. #### defer ```khora defer: (() ->()) ->() ``` ### Nursery ```khora pub effect Nursery { adopt: (Fiber<(), 'er>) -> (), } ``` Where a spawned fiber goes. **The operation takes a fiber rather than a thunk**, because a thunk cannot be *forwarded* to `Fiber::spawn` at all. A fiber's body has to be written where it starts, so that what it closes over can be checked against the rule that a mutable value may not cross — a thunk that arrived from somewhere else took its captures with it, and that is the one thing the check cannot see. The answer is fixed at `()` because an operation cannot be generic in a *type*, and because a nursery has nothing to do with an answer it cannot hand back. A fiber whose result matters is one you keep the handle to and `join`. The **row** is not fixed, and that is worth a sentence because it is the one place an operation is polymorphic. `'er` here is quantified per call rather than per handler: two children raising two different things are both adopted by one nursery. It costs nothing at run time — a capability crosses as evidence and an error as a tag, so the handler's closure is the same code for every row, which is exactly why a row can be generic where a type cannot. So the idiom is one call inside another, and the body is on the screen: ``` nursery.adopt(Fiber::spawn(fn () => analyze(id)!)); ``` #### adopt ```khora adopt: (Fiber<(), 'er>) ->() ``` ## Methods ### Bool ```khora impl Bool ``` #### to_string ```khora pub fn to_string(self) -> String ``` `"true"` or `"false"`. The same text `show` gives, under the name the other scalars use -- `Int::to_string` and `Float::to_string` are both here, and a reader who has met those goes looking for this one. `Show for Bool` is written in terms of it rather than the other way round, so there is one place the two words are spelled. ### Char ```khora impl Char ``` One Unicode scalar value. **Not a `String` of length one.** A `String` can be empty or a thousand characters, so a function taking one has to decide what those mean; a `Char` cannot be either. It is thirty-two bits and lives in a register, which is the other half: scanning a string a character at a time through one-character `String`s allocates once per character. **A scalar value, which is not every number.** The range stops at `0x10FFFF` and the surrogates `0xD800` to `0xDFFF` are a hole in the middle of it -- they exist only to encode a pair in UTF-16 and are not characters. [`Char::from_code`](#from_code) is checked for both. Written `'a'`, `'\n'`, `'\u{1F600}'`. The escapes are the string ones. #### code ```khora pub fn code(self) -> Int ``` The code point, as an `Int`. Every `Char` has one, so this cannot fail. #### from_code ```khora pub fn from_code(value: Int) -> Char ``` The character with this code point, or a stopped program. Stops rather than raises, for the reason every other narrowing conversion in this library does: a number that is not a character is a mistake in the program rather than a condition to handle, and `U8::of` sets the precedent. Check the range first if the number came from outside. #### to_string ```khora pub fn to_string(self) -> String ``` This character as a one-character string. The UTF-8 encoding, written out here rather than asked of the runtime: four branches on the code point and `String::from_bytes` at the end, so the standard library's own text handling has no intrinsic under it that a reader cannot follow. #### utf8_length ```khora pub fn utf8_length(self) -> Int ``` How many bytes this character takes in UTF-8: 1, 2, 3 or 4. #### is_digit ```khora pub fn is_digit(self) -> Bool ``` `'0'` to `'9'`. ASCII only, and the name says so nowhere else, so: **this is not `Nd`**. Arabic-Indic and Devanagari digits are digits to Unicode and not to this, because a parser that accepted them would then have to decide what `Int::of_string` does with them. #### is_alpha ```khora pub fn is_alpha(self) -> Bool ``` `'a'` to `'z'` and `'A'` to `'Z'`. ASCII only, for the reason on [`is_digit`](#is_digit). #### is_alphanumeric ```khora pub fn is_alphanumeric(self) -> Bool ``` A letter or a digit, both in the ASCII sense above. #### is_whitespace ```khora pub fn is_whitespace(self) -> Bool ``` Space, tab, newline, carriage return. #### to_upper ```khora pub fn to_upper(self) -> Char ``` `'A'` for `'a'`, and everything else unchanged. ASCII only. #### to_lower ```khora pub fn to_lower(self) -> Char ``` `'a'` for `'A'`, and everything else unchanged. ASCII only. ### Ordering ```khora impl Ordering ``` #### is_less ```khora pub fn is_less(self) -> Bool ``` Whether the comparison came out `Less`. #### is_equal ```khora pub fn is_equal(self) -> Bool ``` Whether it came out `Equal`. #### is_greater ```khora pub fn is_greater(self) -> Bool ``` Whether it came out `Greater`. #### reverse ```khora pub fn reverse(self) -> Ordering ``` `Ordering::reverse(Ord::cmp(a, b))` sorts descending without a second comparison. ### Option\ ```khora impl Option ``` #### is_some ```khora pub fn is_some(self) -> Bool ``` Whether there is a value. For a condition. Reaching for the value afterwards means matching twice; `match` once instead. #### is_none ```khora pub fn is_none(self) -> Bool ``` Whether there is not. #### unwrap_or ```khora pub fn unwrap_or(self, fallback: A) -> A ``` The value, or `fallback` if there is none. `fallback` is evaluated either way. `unwrap_or_else` takes a closure for when producing it is expensive. #### unwrap_or_else ```khora pub fn unwrap_or_else<'ef, 'er>(self, fallback: () -> A with 'ef raises 'er) -> A with 'ef raises 'er ``` The value, or the result of calling `fallback`. The closure is called only when there is nothing, which is the whole difference from `unwrap_or` — reach for this when producing the fallback costs something. #### ok_or ```khora pub fn ok_or(self, error: E) -> Result ``` Turns absence into a failure, which is how an `Option` joins a `raises` chain. #### and_then ```khora pub fn and_then(self, f: (A) -> Option with 'ef raises 'er) -> Option with 'ef raises 'er ``` The next step, when it can also come to nothing. `map` with a function that returns an `Option` gives an `Option>`, which nobody wants and everybody writes once. This flattens as it goes. **What its absence costs is a shape, not a keystroke.** Reading a record out of positional fields is one lookup that may fail per field, and without this each is a `match` inside the previous one. #### filter ```khora pub fn filter<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Option with 'ef raises 'er ``` The value if it is there *and* `wanted` says so, and nothing otherwise. The bridge from a predicate back into the chain: `find` gives an `Option`, and asking a second question about what it found should not mean leaving the chain to ask it. ### Result\ ```khora impl Result ``` #### is_ok ```khora pub fn is_ok(self) -> Bool ``` Whether this succeeded. #### is_err ```khora pub fn is_err(self) -> Bool ``` Whether it did not. #### unwrap_or ```khora pub fn unwrap_or(self, fallback: A) -> A ``` The value, or `fallback` if there was an error. **The error is discarded.** That is the right thing exactly when the caller has already decided the reason does not change what they do; when it might, `match`. #### ok ```khora pub fn ok(self) -> Option ``` Discards the error, which is the one direction that cannot fail. #### map_err ```khora pub fn map_err(self, f: (E) -> F with 'ef raises 'er) -> Result with 'ef raises 'er ``` Rewrites the error and leaves the value alone. What a function does at the edge of its own vocabulary: a caller should see the failure in terms of what *they* asked for, not in terms of whatever was reached to answer it. #### map ```khora pub fn map(self, f: (A) -> B with 'ef raises 'er) -> Result with 'ef raises 'er ``` Rewrites the value and leaves the error alone. The other half of `map_err`: a reader who has met one goes looking for this and finds a `match`. **Not `Functor`**, which takes one type parameter and `Result` has two. An `impl Functor for Result<_, E>` needs partial application of a type constructor, which this language does not have and does not need for one method. #### and_then ```khora pub fn and_then(self, f: (A) -> Result with 'ef raises 'er) -> Result with 'ef raises 'er ``` The next step, when it can fail the same way. The error type is shared, which is what makes this a chain rather than a pair of unrelated failures: a step that fails differently says so with `map_err` first, and the conversion is visible at the place it happens. ### Redacted\ ```khora impl Redacted ``` #### of ```khora pub fn of(value: A) -> Redacted ``` Wraps a value so that showing it shows nothing. #### expose ```khora pub fn expose(self) -> A ``` The value back. **The only way out, and it is meant to be visible.** `expose` at a call site is a word a reviewer can search for; the leak this type exists to stop is the one nobody wrote on purpose. ### Validated\ ```khora impl Validated ``` #### of ```khora pub fn of(value: A) -> Validated ``` A value that was fine. #### error ```khora pub fn error(e: E) -> Validated ``` One thing wrong. #### is_valid ```khora pub fn is_valid(self) -> Bool ``` Whether nothing was wrong. #### map ```khora pub fn map(self, f: (A) -> B) -> Validated ``` The same value, transformed. Failures pass through untouched. #### map2 ```khora pub fn map2(self, other: Validated, f: (A, B) -> C) -> Validated ``` Two together, **keeping both sides' failures**. This is the whole type. `f` runs only when both succeeded; when either did not, the answer carries every error from both, left to right, and `f` is not called. Chained through a record's fields it reports the whole form in one pass. #### zip ```khora pub fn zip(self, other: Validated) -> Validated<(A, B), E> ``` Both values together, keeping both sides' failures. `map2` with the pair as its answer, and the one combinator that composes without an arity: `zip(a, zip(b, c))` is three values in a nested tuple, and `let (x, (y, z)) = t;` takes it apart, however many there are. It is what a generated record decoder chains through, which is why there is no `map6` and never will be. #### map3 ```khora pub fn map3(self, b: Validated, c: Validated, f: (A, B, C) -> D) -> Validated ``` Three together, keeping every side's failures. **`map2` stopped at two and a CSV row has five columns.** Building a record out of four validated fields meant nesting `map2` and carrying a tuple through it, or hand-rolling the accumulation -- which two of the four review programs did, separately. Errors come back left to right, whichever sides failed, and `f` runs only when every side succeeded. Written out rather than built from `map2` through a chain of pairs: that version works and reads `p.key.key.value` by the time it reaches five. #### map4 ```khora pub fn map4(self, b: Validated, c: Validated, d: Validated, f: (A, B, C, D) -> F) -> Validated ``` Four together, keeping every side's failures. #### map5 ```khora pub fn map5(self, b: Validated, c: Validated, d: Validated, e: Validated, f: (A, B, C, D, F) -> G) -> Validated ``` Five together, keeping every side's failures. Five is where this stops, because a CSV row is what asked for it and a sixth field is a record built out of two of these. `List::traverse` does the unbounded version and would have done the whole job -- except that `Validated` is deliberately not an `Applicative`, for the reason the type gives above, so the one type in `std` built for accumulating errors cannot use it. #### and_then ```khora pub fn and_then(self, f: (A) -> Validated) -> Validated ``` A second step that can fail, run only if the first did not. **Fails fast, and that is not a contradiction.** `map2` collects because its two sides are independent; this one's second step is written in terms of the first's value, so there is no second answer to collect when there is no first value to write it against. Parsing is the shape: `integer` reads the text and then parses it, and "not set" and "not a number" are never both true of one variable. #### to_result ```khora pub fn to_result(self) -> Result> ``` The failures collapsed into one `Result`, for joining a `raises` chain. `List` rather than `E`, because the point of getting here was that there may be more than one and throwing the rest away at the boundary would undo the collecting. #### unwrap_or ```khora pub fn unwrap_or(self, fallback: A) -> A ``` The value, or `fallback` if anything was wrong. ### List\ ```khora impl List ``` #### sort ```khora pub fn sort(self) -> List ``` The same elements, in order. A merge sort, because a linked list is what it was invented for: it needs no indexing, no copying into an array and back, and it is stable — equal elements keep the order they were given, which is the property a caller notices the absence of when sorting by one field and expecting the previous sort to survive. `A: Ord` rather than a comparison function, so that `<` on the elements and `sort` on the list cannot disagree. Sorting by something else is `sort_by`, which takes the comparison. **Its own recursion is the divide, so it goes about `log2(n)` deep** — twenty frames for a million elements. The `merge` underneath it is iterative and so is the release of the intermediate lists, so neither adds a frame per element and the depth is the only depth there is. #### split ```khora pub fn split(self) -> Halves ``` The first half and the second, each in the order it was given. **By counting to the middle, not by dealing alternately.** Dealing is one pass and needs no length, and it is what this did first — but it is not stable, and the test said so. Two equal elements at positions 1 and 2 land in *different* halves with the later one on the left, and a merge that breaks its tie towards the left then puts them back the wrong way round. The cost is a length and a reversal, both linear, against a sort that is already linearithmic. #### min ```khora pub fn min(self) -> Option ``` The smallest element, or nothing if there are none. `Option` rather than a fallback, because the smallest of nothing is not a number a caller can be handed -- and a library that picked one would be picking for whoever forgot the list could be empty. Ties go to the *first*, which matches `sort` being stable: `min` and `head(sort(xs))` name the same element. #### max ```khora pub fn max(self) -> Option ``` The largest element, or nothing if there are none. Ties go to the first as well, for the same reason. ### List\ ```khora impl List ``` #### is_empty ```khora pub fn is_empty(self) -> Bool ``` Whether there is nothing in it. Constant time, which `length() == 0` is not. #### length ```khora pub fn length(self) -> Int ``` How many elements there are. **Walks the list.** Nothing here caches it, so a loop that asks each time round is quadratic — take it once. #### head ```khora pub fn head(self) -> Option ``` The first element, or `None` for an empty list. #### push ```khora pub fn push(self, item: A) -> List ``` Prepends, which is the cheap end. Appending walks the whole list, so it is spelled `concat` and its cost is visible at the call site. #### concat ```khora pub fn concat(self, other: List) -> List ``` `self` followed by `other`. Copies `self` and shares `other`, so the cost is the length of the *left* argument. Building a list by repeatedly concatenating onto the end is the quadratic way to do it; push and `reverse` once. #### reverse ```khora pub fn reverse(self) -> List ``` The same elements, back to front. #### reverse_onto ```khora pub fn reverse_onto(self, acc: List) -> List ``` `self` reversed, in front of `acc`. The accumulating half of `reverse`, and useful on its own: building a list front-to-back and reversing it at the end is the linear way to do what `concat` in a loop does quadratically. This is the "at the end". #### fold ```khora pub fn fold(self, start: B, step: (B, A) -> B with 'ef raises 'er) -> B with 'ef raises 'er ``` Combines every element into one value, left to right. `step` is called with what has been accumulated so far and the next element. Summing is `fold(0, fn (a, b) => a + b)`. #### any ```khora pub fn any<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Bool with 'ef raises 'er ``` Whether any element satisfies `wanted`. **Stops at the first one**, which is what makes it worth having over a `fold`: a fold visits everything, and the whole point of asking whether *any* element matches is that the answer often arrives early. #### all ```khora pub fn all<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Bool with 'ef raises 'er ``` Whether every element satisfies `wanted`. Stops at the first that does not. True of the empty list, which is the convention that makes `all` compose: splitting a list in two and asking each half gives the same answer as asking the whole, and that fails at the empty case under any other rule. #### find ```khora pub fn find<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Option with 'ef raises 'er ``` The first element satisfying `wanted`, or nothing. #### take ```khora pub fn take(self, count: Int) -> List ``` The first `count` elements, or all of them if there are fewer. Below one is empty rather than an error: `take` is what a caller reaches for when the count came from somewhere else, and refusing zero would make every such caller check it first. #### drop ```khora pub fn drop(self, count: Int) -> List ``` Everything after the first `count` elements. #### nth ```khora pub fn nth(self, index: Int) -> Option ``` The element at `index`, counting from zero, or nothing. #### last ```khora pub fn last(self) -> Option ``` The last element, or nothing. #### filter ```khora pub fn filter<'ef, 'er>(self, keep: (A) -> Bool with 'ef raises 'er) -> List with 'ef raises 'er ``` The elements `keep` accepts, in the order they were in. Inherent rather than a trait method, unlike `map`: `Functor` says how to change what is *in* a container, and dropping elements changes the container itself, which is why no lawful `Functor` has a `filter`. There is a class that does -- `Filterable` -- and one method is not a reason to introduce one. `filter` then `map` is two walks; `fold` is one, and is what to reach for when that matters. #### sort_by ```khora pub fn sort_by(self, order: (A, A) -> Ordering) -> List ``` The same elements, in the order `order` asks for. **What `sort` cannot do**, and its doc comment already said so: `sort` takes `A: Ord` on purpose, so that `<` on the elements and `sort` on the list cannot disagree. Sorting a record by one of its fields is a different question, and this is where it is asked. Stable, so equal elements keep the order they were given -- which is what makes sorting twice, by one key and then another, do what everybody expects. **Sorted in an array, and the list rebuilt from it.** The elements are copied into one array, sorted there by [`Array::sort_by`](#sort_by-1)'s bottom-up merge, and read back into a new list: two arrays and one new cell per element, however many rounds the merge takes. It recurses nowhere, so a long list costs no stack. `order` must be consistent: if it says `a` is less than `b` it must not also say `b` is less than `a`. One that is not produces some order and no crash, which is the most any sort can promise about a broken comparison. #### partition ```khora pub fn partition<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Parts with 'ef raises 'er ``` The elements `wanted` accepts and the ones it does not, in one walk. **Two `filter`s with opposite predicates is the alternative**, and it walks twice and calls the question twice per element -- which is wrong when the question is expensive and wrong in a different way when it is not pure. Both sides keep the order they were given. ### List\ ```khora impl List ``` #### contains ```khora pub fn contains(self, wanted: A) -> Bool ``` Whether `wanted` is in the list. `A: Eq` rather than a predicate, because `contains` is the question somebody asks about a value they are holding -- `List::any` is already there for the predicate. ### List\ ```khora impl List ``` #### map ```khora pub fn map(self, f: (A) -> B with 'ef raises 'er) -> List with 'ef raises 'er ``` Each element through `f`, in order, answering a `List`. **`List` is the one type in `std` that implements both `Functor` and `Iterator`, and both of them declare a `map`.** They are not the same operation: `Functor::map` answers a `List`, and `Iterator::map` answers a lazy `Mapped` that computes nothing until something walks it. With only those two, `xs.map(f)` was a hard error naming both traits, and no answer a reader could guess -- the most ordinary line in the language, made ambiguous by an abstraction most programs never mention. So `List` has its own. A type's own method outranks a trait's, so this is what `xs.map(f)` means, and it means the eager one: a list in, a list out, which is what a reader arriving from Go, TypeScript or Rust's `Vec` expects. The lazy pipeline is still there and is now asked for by name -- `Iterator::map(xs, f)`, or `.iter()` on something that has one -- which is the right way round: the abstraction pays for itself, not the boring call. The body is `Functor`'s, because two implementations of one operation is the shape half of this repository's errata is about. `f`'s effect and failure rows pass straight through, so a `map` over a list of requests that can fail is still one call and still fails where it is written. #### flat_map ```khora pub fn flat_map(self, step: (A) -> List with 'ef raises 'er) -> List with 'ef raises 'er ``` Each element's list, laid end to end. `map` and then a flatten, written as one pass because the intermediate list of lists is the only thing the two-step version allocates that nobody wants. #### zip ```khora pub fn zip(self, other: List) -> List> ``` Pairs, one from each, stopping at the shorter. **Stopping rather than padding**, because there is nothing to pad a `List` with: `A` is any type and this library has no notion of a default. A caller who wants the tail of the longer one still has it. ### List\ ```khora impl List ``` #### sum ```khora pub fn sum(self) -> Int ``` Every element added together. Empty is zero. `Int` only, and deliberately: a `sum` over anything addable wants a `Monoid`, and `std::core` has `Functor` and `Applicative` and not that one yet. Naming the concrete case is honest; a half-general one would have to be taken back. ### Region ```khora impl Region ``` #### open ```khora pub fn open() -> Region ``` A new region with nothing deferred into it. `scoped` is the way to reach one from ordinary code; this is what `scoped` calls. Opening one by hand means being responsible for the binding that ends it. **The region ends with the block the binding is in**, not with the function and not at the next statement. So ```khora { let region = Region::open(); Region::defer(region, fn () => print("released")); work(); }; ``` releases before the statement after the block, on every way out of it including a cancellation. Putting the `let` at the top of a function therefore holds everything until the function returns, which is the difference between a lease that ends with the call and one that ends with the caller -- `packages/postgres` had exactly that bug, and a pool of `n` behaved like a pool of `n` uses. #### defer ```khora pub fn defer(self, finalizer: () ->()) ->() ``` Registers `finalizer` to run when this region ends. **Reverse order**: the last deferred is the first run, so a finalizer may depend on one registered before it — a transaction rolled back before the connection it ran on is closed. Cleanup runs with cancellation held off, so a finalizer caused by a cancellation is not cut short by it. #### root ```khora pub fn root() -> Region ``` The region that ends when the program does, released by the entry point after `main` returns — on the failing path as well as the ordinary one. **It belongs to the program's own fiber.** A spawned fiber that calls this traps: a finalizer it deferred here would run at exit, on the main fiber, while the child might still be writing what it captured. A `test` block has a root region of its own, released when the test ends. ### Scope ```khora impl Scope ``` #### root ```khora pub fn root() -> Scope ``` The outermost scope: the one that ends when the program does. An entry point has to get a `Scope` from somewhere, and there is nothing above it to ask. Everything else should take one rather than reach for this — a library that roots its own scope has decided its caller's lifetimes for it. Like `Region::root`, it belongs to the program's own fiber, and a spawned fiber that calls it traps. A child is handed `scoped(work)`. ### Fiber\ ```khora impl Fiber ``` #### spawn ```khora pub fn spawn(body: () -> A raises 'er) -> Fiber ``` Runs `body` on a fiber of its own. The fiber can be stopped whatever the thunk's row says: `Fiber::cancel` stops it at its next loop, call or blocking operation, and its cleanup runs on the way out. `A` must be `Share` for the reason every value crossing a fiber must be: it is computed on one and read on another, so a thing that cannot be held twice cannot be an answer. #### join ```khora pub fn join(self) -> A raises 'er ``` Waits for the fiber to finish, and answers what it answered. **A failure comes back out here, with its type.** If the fiber raised, so does this — the whole point of a fiber that returns a value is that the *caller* decides what a failure means, rather than the runtime printing a line nobody reads. Joining twice is joining once, and gets the answer twice. **`'r` is the row the body raised**, carried on the handle. That is what makes this pleasant rather than merely possible: ```khora let count = Fiber::join(Fiber::spawn(fn () => tally(rows))); // no `!`, it cannot fail let row = Fiber::join(worker)! catch { // by name, because the row is here DbError::Disconnected(_why) => { columns: List::Nil, cells: List::Nil }, }; ``` An erased handle would have needed a `!` on every join — including on a fiber that provably cannot fail — and `catch { _ => .. }` as the only arm. [The effect-survey design note](https://github.com/codyspate/khoralang/blob/main/docs/design/effect-survey.md) §3.1 records what that cost and why the row is on the type instead. #### wait ```khora pub fn wait(self) ->() raises 'er ``` Waits for the fiber, and does not take its answer. **`join` is for the answer; this is for the ordering.** They are not the same wait, and the difference shows up on a fiber that was *canceled*: a canceled fiber has no answer, so a `join` has nothing to hand back and unwinds the joiner along with it. This one just waits. Reach for it when what you needed was "not before that finishes", which is most of the time a fiber is spawned for what it does rather than for what it computes. Letting the binding go waits too — that is where structured concurrency comes from — so this is for the case where the waiting has to happen at a particular line rather than at the end of a scope. **A cancellation point.** A fiber parked here is asked to stop like any other, and stops here, whatever either fiber's row says. The row is the child's: a waiter over a fallible child writes `!`, and one over a child that cannot fail writes nothing. The child is not canceled by the waiter giving up. Letting the handle go still waits, which is what keeps it from outliving the binding. #### finished ```khora pub fn finished(self) -> Bool ``` Whether the fiber has finished, without waiting for it to. **For the loop that supervises rather than waits.** `join` and `wait` both block, and letting the handle go blocks, so a program that spawns a server and then watches a stop flag has no way to ask the one question it needs: is the thing I spawned still running? Without it, a listener whose port would not bind leaves its supervisor turning over a fiber that died before the first pass — no error, no exit, and a process that looks healthy to anything watching it. ```khora let server = Fiber::spawn(fn () => Router::listen_quietly(router, port)!); loop { clock.sleep(50); if Fiber::finished(server) { // It stopped on its own, which for a listener is never good news. Fiber::join(server)!; break }; if Shared::get(stop) { break } } ``` A `true` here means `join` has an answer ready and will not block. False is a fact about the instant it was asked, so this belongs in a loop that sleeps between looks rather than one that spins. #### canceled ```khora pub fn canceled(self) -> Bool ``` Whether the fiber was stopped, rather than allowed to finish. **`finished` says a supervised fiber is gone and cannot say why.** The call that would say why is `join`, and on a canceled fiber a `join` unwinds its caller — at the entry point that ends the program with status 130, which is not something a supervisor can catch. So the loop that notices its listener has stopped has no way to tell a listener that returned from one somebody canceled, and those are the two cases it would act on differently. ```khora let server = Fiber::spawn(fn () => Router::listen_quietly(router, port)!); loop { clock.sleep(50); if Fiber::finished(server) { if Fiber::canceled(server) { // Somebody asked it to stop, so this is a shutdown and not a fault. break }; // It ended on its own, which for a listener is never good news. Fiber::join(server)!; break }; if Shared::get(stop) { break } } ``` **It reports that the fiber was stopped, not what it had computed.** A `true` says a cancellation reached the fiber and the fiber gave up on it; it says nothing about how far the work got, and there is no way here to take a partial answer. For that, have the fiber publish to a `Shared` cell and read the cell. **`join` still ends the program on a canceled fiber.** This does not change that, and asking first does not make the join safe — between the question and the join is another instant in which a cancellation can arrive. Reach for `Fiber::wait`, which waits without taking the answer. A `false` is a fact about the instant it was asked, exactly as it is for `finished`, so this belongs in a loop that sleeps between looks. Asking does not consume the handle: a supervisor asks and keeps supervising. **A detached fiber answers `false`, whatever happened to it.** `Fiber::detach` releases the shared state this reads, so after a detach there is nothing left to ask and the honest answer is the one a released handle always gives. `Fiber::finished` has the same shape for the same reason. Ask before detaching, or keep the handle. #### outcome ```khora pub fn outcome(self) -> Outcome raises 'er ``` What the fiber ended as, waiting for it without unwinding this frame. **`canceled` says a fiber was stopped and cannot hand back what it computed; `join` hands back the answer and, on a stopped fiber, unwinds the joiner.** At the entry point that ends the program with status 130, which no `catch` can name. So a caller that wants the answer *and* tolerates a stop had neither call, and this is that one: the stop comes back as a value. ```khora let f = Fiber::spawn(fn () => tally(rows)); Fiber::cancel(f); match Fiber::outcome(f) { Outcome::Answered(n) => n, Outcome::Stopped => 0, } ``` **The ceremony scales with the fiber's own row, which is the rule `join` follows.** On a `Fiber` this is infallible and needs no `!`. On a fallible fiber it raises `'er`, and **a child that *failed* still comes out by name** — only a child *cancellation* comes back as a value, so the caller who decides what a failure means keeps deciding it. ```khora match Fiber::outcome(worker)! catch { DbError::Disconnected(_why) => Outcome::Stopped, } { Outcome::Answered(rows) => rows, Outcome::Stopped => List::Nil, } ``` **It does not make `join` safe.** `join` on a canceled fiber ends the program as it always has; this is an alternative to that call and not a repair of it. Nor does asking first make a later join safe: between the question and the join is another instant in which a cancellation can arrive. **A cancellation reaching *this* frame while it waits still stops it.** The fiber asking is cancelable like any other, and that cancellation travels the `!` rather than becoming an `Outcome::Stopped` — the value reports what the *child* ended as, and a frame cannot swallow a cancellation aimed at itself. **A fiber that was stopped after computing something answers `Stopped`, and the value is not handed back.** A fiber that was canceled did not finish the work its answer stands for, so handing back whatever it had computed would be an answer wearing the type of one that was complete. For partial progress, have the fiber publish to a `Shared` cell and read the cell. Asking consumes nothing a second question would need: like `join`, asking twice is asking once and gets the answer twice. #### cancel ```khora pub fn cancel(self) ->() ``` Asks the fiber to stop at its next cancellation point. Returns at once. A cancellation point is every loop iteration, every call to a function that can reach one, every blocking `std` operation and every fiber or channel operation. The fiber's `scoped` cleanup and `Region::defer` finalizers run on the way out, to completion; `catch` does not see the cancellation. A single foreign (C) call or file-system call already in progress finishes first. **Asking twice is asking once.** Cleanup is not cut short by a second cancel. `Fiber::abort` is the request that does, and `Fiber::cancel_within` escalates to it after a deadline the caller chooses. A fiber whose body is a `nursery` cancels its children with it, so a canceled fan-out stops rather than waiting on workers nobody told. `Fiber::detach` is the way out that does not wait. #### abort ```khora pub fn abort(self) ->() ``` Stops the fiber at its next cancellation point **even inside its cleanup**, and its nurseries' children with it. `cancel` lets cleanup run to completion, and asking again changes nothing. So a finalizer that blocks for ever -- a `receive` nobody will answer -- holds its fiber, and every fiber waiting on it, for ever. This is the way to end that. It cancels the fiber too if nobody had. Asking twice is asking once. **What it costs is what it says: cleanup cut off part-way.** A `ROLLBACK` may not be sent; a connection may be dropped rather than returned. Reach for it when waiting any longer is worse than that. A change function running under `Shared::update` still finishes, because it holds the cell's lock; a blocking call inside one gives up at once instead. A single foreign (C) call or file-system call already in progress finishes first. `Fiber::cancel_within` asks for this after a deadline you choose, which is usually what a shutdown wants. #### cancel_within ```khora pub fn cancel_within(self, millis: Int) ->() ``` Cancels the fiber now, and aborts it if it is still running `millis` milliseconds later. **A bound on how long cleanup may take, chosen by the caller.** Khora has no default grace period: cleanup runs to completion unless something in the program says otherwise, and this is that something. ```khora Fiber::cancel_within(server, 5000); Fiber::wait(server); // at most five seconds of cleanup, then abort ``` Returns at once. A fiber that finishes before the deadline is not aborted. The deadline is kept by a thread of its own until it expires. #### detach ```khora pub fn detach(self) ->() ``` Stops waiting for the fiber, and asks it to stop. **The valve, and there has to be one.** Every other way out of a handle waits: `join` waits, and so does letting the binding go, which is where structured concurrency comes from. That is right, and it is also how a program hangs — one finalizer that never returns holds its nursery, which holds its parent, up to `main`. [The scheduler design note](https://github.com/codyspate/khoralang/blob/main/docs/design/scheduler.md) promises both bounded cancellation latency and that a nursery exit leaves every child stopped or joined, and those two are in tension exactly here. So: signal, and go. The fiber keeps running, its answer is dropped when it arrives, and a failure it reports afterwards is silent — the program said it was no longer listening. Without this, a `timeout` over a body with an uninterruptible tail is a lie: it would promise to return in half a second and then block on the tail. **It cancels as well as detaching**, because a detached fiber nobody asked to stop is a leak with a nicer name. ### Array\ ```khora impl Array ``` #### empty ```khora pub fn empty() -> Array ``` No elements at all. `new` needs a value to fill with and a generic container has none to give until something has been put in it, so without this there is no way to write down an empty `Array` — `Array::new(0, fill)` still demands the `fill` it will never write. That gap is what `Vector` is built on: its storage starts here and is allocated for real by the first `push`, which is also the first moment there is a value of `A` to fill with. #### new ```khora pub fn new(length: Int, fill: A) -> Array ``` `length` elements, every one of them `fill`. **Every one of them the *same* `fill`, which matters when it can be written.** A record is a value and two cells holding one that nobody can change are indistinguishable from two copies -- but give it a `mut` field and they are one object with two names: ```khora type Agg = { mut count: Int }; let slots: Array = Array::new(3, { count: 0 }); Array::get(slots, 0).count = Array::get(slots, 0).count + 1; Array::get(slots, 1).count = Array::get(slots, 1).count + 1; // every slot now reads 2, including the one nothing touched ``` An array of mutable accumulators is the natural way to group by a small key and it is the *fast* way -- thirteen nanoseconds an event -- so this is a shape people reach for. It reports counts multiplied by the number of buckets, silently, with nothing to say so. Use [`Array::from_fn`](#from_fn) below, which builds each cell separately. **The one place `std` disagrees with its own naming rule.** `new` means "an empty one of something that grows", and an array does neither -- `Array::empty` sits above this for the empty case. `Array::filled(length, fill)` is what the rule asks for, and renaming a published `std` signature is a compatibility decision rather than a style one. [The naming design note](https://github.com/codyspate/khoralang/blob/main/docs/design/naming.md) records it. #### length ```khora pub fn length(self) -> Int ``` How many elements it has. Fixed at construction; nothing grows an array. #### get ```khora pub fn get(self, index: Int) -> A ``` The element at `index`. An index outside the array stops the program and says which index and what length. Same reasoning as trapping on integer overflow: a program that runs off its own array is wrong, and reading whatever is next in memory is the least useful possible response. #### set ```khora pub fn set(self, index: Int, value: A) ->() ``` Writes `value` at `index`, in place. **The one mutation in `core`**, and the reason `Array` is not `Share`: two fibers writing one array is a data race, so an array crosses into a fiber only inside a type that says it is safe. An index outside the array stops the program, as `get` does. #### from_fn ```khora pub fn from_fn(count: Int, make: (Int) -> A) -> Array ``` `count` elements, each one built by calling `make` with its index. **The answer when the cells must not be the same object.** [`Array::new`](#new) fills every cell with one value, which is right for a number and wrong for anything with a `mut` field -- an array of accumulators built that way is one accumulator with `count` names, and it reports every total multiplied by the number of buckets with nothing to say so. `make` is called exactly `count` times, in order, so it may be the source of the difference rather than only of the shape: ```khora let slots: Array = Array::from_fn(16, fn _i => { count: 0 }); let squares: Array = Array::from_fn(10, fn i => i * i); ``` A count below one is [`Array::empty`](#empty), and `make` is not called at all -- which is the reason this is not `Array::new(count, make(0))` with a loop after it. #### sort_by ```khora pub fn sort_by(self, order: (A, A) -> Ordering) ->() ``` Sorts the whole array in place, stably, by `order`. #### is_utf8 ```khora pub fn is_utf8(self) -> Bool ``` Whether these bytes are well-formed UTF-8, and so whether `String::from_bytes` will accept them. Only an `Array` has an answer. #### prefix ```khora pub fn prefix(self, count: Int) -> Array ``` The first `count` elements, copied. What a caller does with a buffer it only partly filled: a socket read says how many bytes arrived, and the rest of the buffer is not part of the message. #### with_data ```khora pub fn with_data(self, body: (Ptr, Int) -> B with 'ef raises 'er) -> B with 'ef raises 'er ``` Lends the elements to `body` as a pointer and a count, for the duration of the call and no longer. This is how a buffer reaches a foreign function. The count is the number of *elements* — the same number `length` gives — so a body that wants bytes multiplies by the element's width itself. **The lifetime is the call, and that is the design rather than a limitation.** A plain `data(self) -> Ptr` would be a dangling pointer waiting to happen: the array is released at its last use, and that use would be the `data` call. No wider scope is right either — the innermost one is wrong for a pointer taken in one branch of an `if`, and the function's own is wrong for a loop, which would hold one buffer alive per iteration. Only an array of numbers can be lent. An `Array` of Khora objects holds reference-counted pointers, and handing those across is the mistake the whole boundary exists to prevent. ### Array\ ```khora impl Array ``` #### copy_into ```khora pub fn copy_into(source: Array, from: Int, target: Array, at: Int, count: Int) ->() ``` Copies `count` bytes of `source`, starting at `from`, into `target` starting at `at`, in one move. **What it is for: a byte buffer copied a byte at a time.** A loop of `get` and `set` checks two bounds and passes a cancellation point per byte, and a network protocol copies every byte it reads or writes at least once. This checks the two ranges once and moves the bytes with the C library's `memmove`. `source` and `target` may be the same array and the ranges may overlap: the bytes arrive as they were before the copy began, which is what moving the unread tail of a buffer to its front needs. A range that does not fit, on either side, or a negative `count`, stops the program and names the index, as [`Array::get`](#get) does. A `count` of zero copies nothing, anywhere a zero-length range fits -- including at either array's end. ```khora let buffer: Array = Array::new(8, 0); Array::copy_into(String::bytes("hello"), 0, buffer, 3, 5); // buffer is 0 0 0 h e l l o ``` ### Int ```khora impl Int ``` Arithmetic that does not trap, and the bits underneath it. Ordinary `+`, `-` and `*` **stop the program** when they do not fit: a program that passes its tests and then wraps in production is the failure worth a branch to prevent. These are how you ask for the other behavior in the places that genuinely want it — a hash, a checksum, a pseudo-random number — by name, so that the trap stays the default without being in the way. Methods rather than operators for now. `^`, `&`, `|`, `<<` and `>>` are five new tokens and `>>` has to be told apart from the end of two nested type arguments; none of that is hard and none of it is what a hash was waiting for. #### wrapping_add ```khora pub fn wrapping_add(self, other: Int) -> Int ``` The sum, wrapping at the type's own range instead of trapping. #### wrapping_sub ```khora pub fn wrapping_sub(self, other: Int) -> Int ``` The difference, wrapping. #### wrapping_mul ```khora pub fn wrapping_mul(self, other: Int) -> Int ``` The product, wrapping. #### xor ```khora pub fn xor(self, other: Int) -> Int ``` Bit by bit: one wherever the two differ. #### and ```khora pub fn and(self, other: Int) -> Int ``` Bit by bit: one wherever both are one. #### or ```khora pub fn or(self, other: Int) -> Int ``` Bit by bit: one wherever either is one. #### shl ```khora pub fn shl(self, other: Int) -> Int ``` Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone. #### shr ```khora pub fn shr(self, other: Int) -> Int ``` Arithmetic, so a negative number stays negative. A logical shift is what `Int` cannot express and what an unsigned fixed-width type will. ### String ```khora impl String ``` #### slice ```khora pub fn slice(self, from: Int, to: Int) -> String ``` The bytes from `from` up to `to`, clamped to what is there. **One allocation and one copy**, which is why it is a compiler intrinsic rather than the six lines it reads like. Written in Khora it went through an `Array` and `String::from_bytes`, so every slice was *two* allocations, *two* copies and a walk of the bytes to re-check that they were UTF-8 — 2,915 nanoseconds for eighty bytes. `String::split_once` is two slices and the request parser is a dozen splits, so this one function was most of what a server spent its time on. **Stops the program if the ends fall inside a character**, the same bargain `String::byte` makes about an index. The check is two byte reads rather than a walk: the input is already UTF-8, so the only way out of it is a cut through a multi-byte character, and that shows up as a continuation byte at one end or the other. Indices outside the string are clamped rather than refused, because "the rest of it" is what every caller of `slice(i, byte_length(s))` means. #### is_char_boundary ```khora pub fn is_char_boundary(self, at: Int) -> Bool ``` Whether `at` is where a character starts. **The question [`slice`](#slice) gives no way to ask, and it is the one that matters.** `slice` counts bytes and stops the program when the cut lands inside a character: ```text khora: this slice cuts a character in half, so it is not a String ``` Which is right -- the alternative is a `String` that is not UTF-8 -- but until there was a way to *ask*, a program truncating text it did not write was one non-ASCII input from dying. `std::json` copes by reasoning that every byte it escapes is below 128; an application should not have to. The end of the string is a boundary, and so is 0. Anything outside is not. #### next_boundary ```khora pub fn next_boundary(self, at: Int) -> Int ``` The first character boundary at or after `at`, or the string's length. For walking to a safe place to cut: `String::slice(s, 0, s |> next_boundary(20))` takes about twenty bytes and never lands in the middle of anything. **At or after, so it does not advance.** Given an offset that is already a boundary this answers that offset, which means ```khora while at < stop { at = String::next_boundary(s, at); // never ends } ``` is an infinite loop on every string. To step a character at a time, pass `at + 1` — or better, ask `char_at` and add `Char::utf8_length`, which says what it is doing. The name is the trap: `next` reads as *after*. It is kept because the behavior is the one a cut wants — rounding an arbitrary offset outward to something safe — and because `String::chars_between` and `String::chars` are the two loops most callers actually want, and both are here already. #### previous_boundary ```khora pub fn previous_boundary(self, at: Int) -> Int ``` The last character boundary at or before `at`, or 0. **At or before**, like its sibling: on a boundary it answers where it already is. See `next_boundary` for what that costs a caller who steps with it. #### char_at ```khora pub fn char_at(self, at: Int) -> Option ``` The character beginning at byte `at`, or `None`. `None` when `at` is outside the string or is not a boundary -- asking for the character in the middle of one has no answer, and inventing the replacement character would hide a caller that is stepping by the wrong amount. **Byte offsets, not character indices.** `char_at(s, 3)` is the character three *bytes* in, and `utf8_length` is how far to step for the next one. A character index would mean walking the string from the start for every access, which is the quadratic loop this signature refuses to hide. #### chars ```khora pub fn chars(self) -> List ``` Every character, in order. Allocates a list as long as the string has characters, which is what makes it the convenient answer rather than the fast one. A scan that cares walks with [`char_at`](#char_at) and [`Char::utf8_length`](#utf8_length) and allocates nothing. #### char_length ```khora pub fn char_length(self) -> Int ``` How many characters, which is not how many bytes. Walks the string, so it is `O(n)`. `byte_length` is the constant-time one and is what a buffer size wants. #### chars_between ```khora pub fn chars_between(self, start: Int, end: Int) -> Int ``` How many characters lie between two byte offsets. **What a caret needs.** A tool reporting on text somebody else wrote places the caret by counting the characters before the span and draws it by counting the characters inside it, and `char_length` counts a whole string while `byte_length` counts the wrong unit. `examples/khq` wrote this for itself before it was here. `start` is rounded down to a boundary and `end` up, so a range that splits a character counts it once rather than trapping. A caller who wants to know whether it split one has `is_char_boundary`. #### find ```khora pub fn find(self, needle: String, from: Int) -> Int ``` Where `needle` first occurs at or after `from`, counting in bytes, or **-1** if it is nowhere. **The one place in this library that answers with a sentinel**, and it earns it the way `String::byte` earns being able to stop the program: this is the bottom of the string library, `index_of` is one line above it and is what everything else calls. A search is the C library's `memmem`, which says -1, and turning that into an `Option` is work the compiler cannot do inside an intrinsic — so it is done here, once, in the open. `from` exists so that scanning a string does not mean copying it. Walking a header block by slicing off the front costs a copy of everything left over, once per header; searching from a cursor copies only the pieces that are kept. An empty needle is found at `from`, which is what every other language says and what stops `split_once` on an empty separator from running forever. #### index_of ```khora pub fn index_of(self, needle: String) -> Option ``` Where `needle` first occurs, or nothing. **A compiler intrinsic underneath**, and the reason is a measurement. Written as a loop over `String::byte` this took 3,180 nanoseconds to find a six-byte word in eighty bytes — a call and a bounds check per byte, at every candidate position. Bound to `memmem` and reached through two nested `String::with_data` calls it took 500, measured with each of their closures built on the heap. A body written in place is built in the caller's frame, which removes those two allocations but not the two calls through a closure. Going straight to the runtime call is 40. That matters because this is the hottest thing in the request parser: `split_once` is built on it, and parsing one request is about eight searches. #### matches_at ```khora pub fn matches_at(self, needle: String, at: Int) -> Bool ``` Whether `needle` sits at exactly `at`, without cutting a string out to ask. The primitive `find`, `starts_with` and `ends_with` are all written over: comparing in place costs nothing, and `slice` would allocate once per position tried. #### starts_with ```khora pub fn starts_with(self, prefix: String) -> Bool ``` Whether the text begins with `prefix`. A prefix longer than the text is simply `false` rather than an index that would go past the end. #### ends_with ```khora pub fn ends_with(self, suffix: String) -> Bool ``` Whether the text ends with `suffix`. #### contains ```khora pub fn contains(self, needle: String) -> Bool ``` Whether `needle` occurs anywhere. The empty string is in everything, which is what `find` already answers and is the convention every library that has thought about it settled on: it is the identity of concatenation, and it is at position 0 of anything. #### is_empty ```khora pub fn is_empty(self) -> Bool ``` Whether there are no bytes at all. #### trim ```khora pub fn trim(self) -> String ``` The string without leading or trailing whitespace. Space, tab, carriage return and newline -- the four a line of text picks up on its way through a file or a socket. Not Unicode's full set of space characters: that list changes with the standard, and a `std` that silently trimmed a non-breaking space would be surprising in the other direction. The ends move inwards over the original bytes and only what survives is copied: one allocation, and no second walk of the bytes. The copy is `String::slice`, which always makes one — trimming nothing still hands back a new string of the same length rather than the one it was given. #### lower ```khora pub fn lower(self) -> String ``` The string with `A`-`Z` written in lower case. **ASCII only, and deliberately.** Unicode case folding is locale dependent -- Turkish `I` lowercases to a dotless `ı`, and German `ß` upper-cases to two characters -- so a correct `lower` needs a locale and a table, and neither belongs in `core`. What this is for is comparing a header name or a file extension, where the input is ASCII by specification. Bytes outside `A`-`Z` are untouched, so text in any encoding passes through rather than being mangled. #### upper ```khora pub fn upper(self) -> String ``` The same text with `a`-`z` shifted to `A`-`Z`. ASCII only, for the reason `lower` gives: a case mapping that is right for every script is a table that changes with the Unicode standard, and one that is right for *some* scripts silently is worse than one that is honest about being ASCII. Turkish dotless i is the example everybody learns this from. #### escape_html ```khora pub fn escape_html(self) -> String ``` The text made safe to put in an HTML page: `&`, `<`, `>`, `"` and `'` replaced by `&`, `<`, `>`, `"` and `'`. **For text going into an element or a quoted attribute value**, which is where a user's input goes in a page. All five are escaped, not only `<` and `&`, so the same call is safe in `title="..."` and in `title='...'`. It is not enough for an unquoted attribute, a URL, a `