Getting started with the Kio tooling
Start here when you want to run the Kio development loop on a real package. You will create a tiny greeter package, edit it, format it, typecheck it, test it, build it, and record its contract surface with kio sig.
Every command below is meant to be run from the package root as you read. The tutorial assumes only the language used in the examples; for a fuller language tour, read the language tutorial. For exact command-line behavior, see specs/cli.md.
The whole loop uses one program, kio, with a subcommand per step.
Chapter 1 — Scaffold a package with kio init
A Kio program is a package: a directory holding a package file (the package's contract with its host, plus its build settings) and one or more modules (the code). kio init writes the smallest working package for you.
mkdir greeter
cd greeter
kio init
With no argument, kio init names the package after the current directory; pass a name to override it (kio init greeter). The package name must be a lowercase identifier containing at least one ASCII letter: _a and _1a are valid, but _ and _1 are not. Invalid names are rejected before any files are created. The command writes two files at the package root and prints what it created:
created Kio package `greeter`
greeter.pkg.kio
main.kio
So the package starts as:
greeter/
├── greeter.pkg.kio
└── main.kio
kio init refuses to overwrite an existing package file, so it is safe to run in an empty directory and loud in one that already looks like a package.
What the package file declares
greeter.pkg.kio is the package file. Its filename stem (greeter) is the package name.
package greeter;
build {
cache "out/.kio-cache/";
target js {
out "out/js/"
}
}
bridge {
main
}
Two blocks do the work:
build { ... }names a compilation target (js, writing underout/js/) and a cache directory. This is whatkio buildreads.bridge { ... }lists the modules whose public surface forms the package's contract with its host. Here it admitsmain, the one module we have.
The starting module
main.kio is the package's root module — just a module <name>; file at the package root. The generated one declares a string type and a print capability it asks the host to supply, then exports a main that prints a greeting:
module main;
host type String role(str);
host fn print(p0: String) -> .;
pub fn main() -> . { print("hello from Kio\n"(String)) }
A host type / host fn is a capability the host provides: the package declares the shape it needs and the host supplies the implementation when it loads the package. pub marks main as part of the package's exported surface. That is all the language you need to follow the rest of this tutorial.
Chapter 2 — Write a little code
Let's add one function and call it from main: a greeting function that builds a string, with main printing its result.
First, the new function — deliberately typed on one cramped line so we have something for the formatter to fix in the next step:
fn greeting(name: String) -> String {string_concat("hello, "(String),name)}
greeting concatenates a literal prefix with its argument. That call needs one more host capability — string concatenation — so add its declaration alongside the existing host lines in main.kio:
host fn string_concat(p0: String, p1: String) -> String;
Finally, rewrite main to call greeting:
fn greeting(name: String) -> String { string_concat("hello, "(String), name) }
pub fn main() -> . { print(greeting("Kio"(String))) }
The module now reads, top to bottom: a module main; header, three host declarations (String, print, string_concat), then greeting and main.
Chapter 3 — Format with kio fmt
Kio has exactly one canonical style and no knobs to configure it. kio fmt rewrites your source to that style in place:
kio fmt
It prints the path of every file it changed; silence means everything was already canonical. Because we wrote greeting on one cramped line, kio fmt reports:
main.kio
Open the file again and the body has been spaced to the house style (the greeting line above already shows the canonical form). Writes are atomic, so an interrupted kio fmt never leaves a half-written file.
In CI you want the check-only mode, which writes nothing and fails if any file is off-style:
kio fmt --check
--check exits 60 when at least one file would change (and lists those paths), 0 when everything is already canonical. Wire that into your pipeline so unformatted code can't merge.
Chapter 4 — Typecheck with kio check
kio check runs the whole front end — parse, resolve, typecheck — and reports any errors. It writes no output files and touches no network; it is the fast command to run after every edit.
kio check
A clean run is silent and exits 0. If you have a type error, kio check prints it and exits with a code identifying the category of error (parse, type, name resolution, …) so scripts can branch on the kind of failure without scraping messages; the table is in specs/exit-codes.md.
Try breaking it on purpose: pass greeting an integer instead of a string and re-run kio check. You will get a type error pointing at the offending call. Fix it back, re-run, and you get silence again.
Carets mark the error in a source excerpt; secondary underlines show related context such as a type annotation. Tabs expand for display so these marks stay aligned. The header's column still counts characters in the original source, including each tab as one character.
For an imported function, the related annotation can be in another module. The diagnostic names that file even when you call through a local alias; the function's signature explains the requirement, not the type's definition.
The module header is one path, such as module main;, not a package name followed by a module name. Package declarations belong in the separate *.pkg.kio file. Diagnostics explain this distinction when a header contains adjacent names.
Types and values also have different punctuation: () is the unit value, while . is its type; (a, b) is a tuple value, while A & B is a product type. In an editor using kio lsp, the corresponding type-position diagnostics offer quick fixes that preserve comments, nested type arguments, and component grouping. Applying a fix rechecks the edited document.
Invalid-character diagnostics identify the complete source character. Unicode text is valid in strings and comments, while identifier spellings use ASCII.
Chapter 5 — Test with kio test
Kio's built-in testing primitive is the equiv declaration: a claim that two or more expressions reduce to the same value. kio test runs the same front end as kio check, then evaluates every equiv in the package and reports which ones hold.
Add a claim to main.kio that pins down greeting's behavior:
equiv greeting_kio() {
greeting("Kio"(String));
string_concat("hello, "(String), "Kio"(String))
}
The claim says "calling greeting with "Kio" is the same as concatenating the prefix with "Kio" directly." Run the tests:
kio test
Each equiv prints a pass or fail line, and the run ends with a summary:
pass equiv `greeting_kio` in greeter/main
result: 1/1 equiv block passed
A package with no equiv blocks prints no equiv blocks found and exits 0 — having no tests is not a failure. A genuine equivalence failure exits 50. For more on writing claims, see Testing with equiv.
Chapter 6 — Build with kio build
kio build reads the build { ... } block from the package file, hands each declared target to its backend, and writes output under that target's out directory. It typechecks first, so a package that fails kio check never reaches codegen.
kio build # build every target in the build block
kio build js # build only the target whose id is "js"
With no argument every target is emitted; naming target ids builds only those (an unknown id is an error). Our package declares one js target, so both commands here do the same thing. The emitted JavaScript lands under out/js/. What the artifact looks like, and how to call into it, is backend-specific — see the host guide for your target: JavaScript, TypeScript, Python, Java, Rust, Go, Swift, or Haskell.
Chapter 7 — Seal the contract surface with kio sig
The package's contract surface is everything its bridge block exposes: the host capabilities it requires and the pub items it offers back. When that surface changes, consumers that loaded the package care. kio sig records each version of the surface in a changelog file (greeter.sig.kio) and gates changes on backward compatibility. The full contract is in specs/versioning.md; here we walk the everyday path.
Run it bare first. With no changelog yet, kio sig is non-mutating and points you at the first step:
kio sig
kio sig: `greeter` has no compatibility changelog yet — `kio sig commit` seals the first version
Stage, then commit
A version is a mutable draft until you seal it. kio sig stage records the current surface as a compatible delta into that draft; kio sig commit seals it and bumps the version number.
kio sig stage
kio sig commit -m "Initial contract: String, print, string_concat, main."
stage writes the draft (the host type String, the host fns print and string_concat, and the export main). commit seals it as v(1); the -m message is stored with the version and shown by the log. A bare kio sig stage only ever records a compatible change — if your edit broke the last sealed surface, it errors until you acknowledge the break with kio sig stage --force.
Review the history
In that history, add introduces a declaration, modify changes one that is still present, and remove drops one. Bringing back a removed declaration is another add, not a modify, even though its earlier declaration stays in the history.
kio sig log pretty-prints the changelog, each version with its commit message:
kio sig log
It takes read-only display filters: kio sig log --breaking shows only versions that carry a breaking change, and kio sig log --since 1 scopes the output to versions after v(1). (When the history grows unwieldy, kio sig compact <version> collapses the additive history before a version into a single boundary block — a detail for later.)
The CI gate
kio sig status is the command CI runs to enforce the changelog. It compares the live surface against the last sealed version and exits:
kio sig status
0— the changelog is up to date with the source;81— there is an unrecorded but compatible drift (runkio sig stage);82— a break is recorded but not yet sealed (runkio sig commit);80— the source breaks the sealed contract and the break is unrecorded (acknowledge withkio sig stage --force, or reconcile the source).
Right after our stage / commit, kio sig status is clean and exits 0. Add a new pub fn later and it reports 81; kio sig stage then records it and clears the gate again. Bare kio sig prints the same status summary without changing anything, so it is the safe command to run when you just want to look.
Chapter 8 — The loop, end to end
An editor connected to kio lsp can rename a declaration and its references together. Renaming a newtype updates its uses in signatures, aliases, payloads, annotations and explicit call type arguments. A same-named type parameter is a separate binding and stays unchanged.
Renaming an explicit newtype constructor to its projector's name (or the reverse) is refused: those two members share one newtype's namespace. Members of a different newtype do not reserve names for ordinary functions or local variables.
That is one full lap. Day to day you'll spend most of your time in the first two steps:
- Edit a module.
kio check— catch type errors fast.kio fmt— canonicalize before committing (kio fmt --checkin CI).kio test— confirm theequivclaims still hold.kio build— emit the artifact when you're ready to run it in a host.kio sig stage→kio sig commitwhen the contract surface changes;kio sig statusas the CI gate.
Chapter 9 — Where to go next
- The Kio language — a guided tour of the language itself: types, functions, sums, polymorphism, and structural glue.
specs/cli.md— the exact command-line contract.- Exploring a package with
kio repl— load a package interactively and query its types, docs, and source. - Host guides — run a built package from JavaScript, TypeScript, Python, Java, Rust, Go, Swift, or Haskell.