Cheat sheet: what doesn't exist and common mistakes
kola is a small language, and most beginner mistakes come from habits carried over from C, C++, Rust or Python. This page lists what is deliberately missing, what replaces it, and the places where people stumble most often. For an unfamiliar term, see the glossary.
What doesn't exist, and what replaces it
Habit | In kola |
|---|---|
|
|
Exceptions, |
|
| Regions. A region is a block of code that owns an arena, a chunk of memory handed out piece by piece and freed all at once: you allocate with |
Destructors, RAII, | Linear resources. Each one is consumed exactly once on every path, and the compiler checks it. |
Borrow checker, lifetimes | Region liveness is checked lexically. Within a region, shared mutation is allowed. The |
Traits, interfaces, | Generics in square brackets, compiled into a separate copy per type they are used with, and fn references. |
Closures, lambdas, | Fn references, which capture nothing, and ordinary |
Macros, | None. Every piece of meaning is in the text the reviewer reads. |
| None, and no other way to leave the checked language. Hand-tuned machine code enters only as a named per-architecture leg ( |
Global variables, | No global state. Every access to the world goes through a capability passed as a parameter. |
Implicit conversions, | Three explicit ones: |
| kola never computes a layout; it claims one. |
| Not there. The surface is |
Overloading, default arguments, | No overloading and no defaults. Generics and regions use square brackets: |
| No |
What does exist, and is often assumed missing
Octal and binary literals:
0o644,0b1010_1010, alongside decimal and hex._between digits is a separator.Bitwise NOT:
~x, which is exactlyx ^all-ones of its width, signed included, and never traps.f64: IEEE 754 binary64, bit-identical on every backend, with explicit conversions only (f64::of_i64,f64::to_i64returningOption). NaN in any operation traps,f64is banned in act fn, and there are no transcendental functions.Byte strings:
b"GET "is a[u8; 4], and&b"GET "is the&heap [u8]you pass to a byte API. Raw literalsr"...",r#"..."#andbr#"..."#hold text exactly as written, which is convenient for JSON and shell snippets.Owned vectors:
let mut v = Vec::new();is an ownedVec(a growable array) whose storage is onheap, the region that lives as long as the program. Growing it isalloc(heap). For memory that comes back at a brace, allocate theVecin a region instead.Trap domains:
domainandrecover(t: Trap), on every executor.Atomics, for memory shared with another core or a device. An atomic operation reads or updates a memory cell in one indivisible step, so another CPU core never sees it half done.
Atomic::add64_relaxed(cells, i, 1u64)and its siblings work on a shared slice plus an index, on thehweffect. They are not how two kola threads talk; those move ownership.The environment (the process's
NAME=valuevariables):sys.env.get(b"HOME")returnsOption[&heap [u8]]. It returns bytes, because an environment need not be UTF-8, and it is empty underkola test.Unguessable randomness:
Entropy::draw[[u8; 32]](&mut rng)andRng::fill_osread OS entropy on every call.Rng::weak_u64is a fast seeded stream for jitter and shuffling. Its state can be recovered from one output, so never use it for keys, tokens or nonces.External programs:
sys.proc.spawn(...)runs a binary the operator granted by name (--proc-bin ffmpeg=/abs/path). The returnedChildis linear.Attenuated capabilities:
dial.to_host(host, port),dir.ro(),w.ro(),w.atomic(), andsplit()onClock,RngandDial. A capability moves once, so you narrow it rather than copy it.Sorting and copying:
seq::sort_scratchis the O(n log n) stable sort; plainseq::sortis an insertion sort.bytes::copy(dst, src)copies slice to slice.Integer limits:
std::num::limitshasU8_MAX…U128_MAXandI8_MIN…I64_MAX. There is noUSIZE_MAX, becauseusizeis 32 bits on a 32-bit target and the constant would mean different things in two builds of the same source.Option helpers:
o.take()moves the value out of a writableOptionplace and leavesNone;o.with_mut(f)runs a named function on the payload in place.The data-trust markers:
untrustedon a parameter (data you must not trust),Secret[T](data you must not reveal) andct fn(code whose running time must not depend on secret values, because an attacker who can measure timing could otherwise recover them).SIMD lanes: types that hold several numbers and operate on all of them in one instruction (SIMD: single instruction, multiple data). There are the constant-time lanes
u32x4/u32x8, and the non-ct media lanesi16x8/u8x16/i32x4with 256-bit twins.Reports:
kola reporthaschecks,reachable,leaks,regions,effects,secrets,tainted-reqs,wipe-windows,stack-frames,deps,perfand more;kola help reportlists them all.
Common mistakes
Most of these come with a compiler message that names the rule and the repair. The example below collects several of the repairs in one test:
use std::log;
fn count_spaces(line: &[u8]) -> u64 !{} {
let mut n: u64 = 0;
for b in line {
if b == 32u8 { n = n + 1; } // 32 is ' '
}
n
}
test the_usual_repairs {
let mut v = Vec::new();
v.push(7u64);
v.push(9u64);
let small: Option[u8] = narrow[u8](v[1]);
assert small == Some(9u8);
let wide: u64 = widen[u64](3u8);
assert wide == 3;
assert count_spaces(&b"GET / HTTP/1.1") == 2;
region r {
let s = f"len={v.len}";
assert s.len == 5;
}
log::debug(f"total {v[0] + v[1]}"); // passed straight to a call: no allocation
}Interpolating in a plain string. A plain
"..."is text and its braces stay braces. Interpolation needs thefprefix:log::info(f"x={v}"). Writinglog::info("x={v}")withvin scope is refused, and the help line offers thef.Binding an interpolated string with no region open.
f"..."allocates when you bind it. Its region comes from the destination's type, or else from the innermost openregion. With neither, the compiler refuses it rather than putting it onheapsilently. Passing it straight to a call-scoped&strparameter, aslog::info(f"n={n}")does, allocates nothing. If you mean program lifetime, say so:let s: &heap str = f"...";.The wrong kind of reference in a parameter. Default to
&T, which accepts stack and region values alike and allocates nothing. Use&r Tonly when the function stores or returns the reference.Expecting a
regionto be an expression.let x = region r { ... };is refused. A region that is a function's last statement hands its tail back as the return value; anywhere else, assign an outerlet mutinside the block.Forgetting
muton a growingVec.let v = Vec::new(); v.push(x);fails witherror[card §4]: `push` mutates an immutable receiver, and the help line tells you to writelet mut v. Growing an ownedVecalso needsalloc(heap)in the effect row.Reading a value after passing it by value. An owned
Vec, aMap, and any value that carries one move at every by-value position: a call argument, alet, a struct field,Some(v), a channel message, a return. Reading the source afterwards is an error. Borrow instead (&v,&mut v) or stop reading it. Out of a reference (h: &H), a field cannot be moved at all; borrow&h.v.Reaching for a character literal.
'v'does not exist. Write the number with a comment,118u8 // 'v', or compare against a byte string.Treating
stras a byte buffer. Astris an immutable UTF-8 view. It has.lenand a no-copy.bytesview, and no&mutform. Parse wire data as&[u8]. The only way to make astrfrom bytes isutf8::to_str, which validates and returnsOption.An unsuffixed literal in a shift, or a counter that goes negative. An unsuffixed literal takes its width from context, and a width nothing pins is
u64(ori64if a negative value ever enters it). So1 << nshifts in 64-bit space; write1u32 << nif you mean 32. Andlet mut c = 0; c = c - 1;traps at zero, because-on an unsigned width is checked. Annotatei64where a value can go negative.Mutation inside
assert. A proven assert is removed, so its expression must not change anything.assert get(&mut n);is refused witherror[card §8]: assert expression mutates state. Bind first:let ok = get(&mut n); assert ok;.Shadowing a live resource. Ordinary
letshadowing works as in Rust, but a live resource cannot be hidden this way. Consume it first...in a struct pattern. Every field must be named. Trailing commas are legal in any list.Looking for
print. The compiler answerserror[card §7]: `print` no longer exists — and neither does the `Io` capability. kola splits whatprintconflated. A diagnostic goes to the log, which needs no capability:use std::log;thenlog::info("..."), effectlog. The program's output, the bytes another program reads, goes to a stream and needs one:sys.stdout.write(&bytes), effectio. A stray diagnostic on stdout would corrupt the output, which is why they are separate.A contract on attacker-supplied bytes. Mark a parameter
untrustedwhen it carries data an attacker may control: bytes from the network, a file, standard input or another process. Areqon it is then refused:error[card §8]: `req` depends on untrusted input `p`. A contract is a claim about the caller, and these bytes have no caller, so a violated one would be a remote denial of service. Check the value at run time and returnResultorOption.fn main() -> i32.mainreturnsu8, the eight bits a parent process can observe, or nothing at all. Its only optional parameter issys: Sysby value. A computed exit code narrows explicitly:narrow[u8](n).{0}inside anf-literal.f"{0}"would interpolate the number 0, so it is refused. For a literal catalog placeholder, use a plain string"{0}"or escape the braces inside thef-literal:f"{{0}}".Inventing syntax. The most expensive mistake. If a construct is not on
SPEC-CARD.mdorSTD-CARD.md, it is not in the language, however natural it looks.
Discarding a Result
One rule catches test bodies more often than any other: a Result cannot be thrown away.
pub fn drop_it(c: Conn) !{io} {
c.close();
}The compiler answers:
error[card §3]: a `Result` cannot be discarded by an expression statement
= help: handle it — `match`, `?`, or `.unwrap()` — or discard it deliberately with `let _ = ...;`The four repairs are not equivalent. match says what each outcome does, ? propagates the error, .unwrap() traps on failure (a claim that it cannot fail), and let _ = ...; discards on purpose, visibly. Here is the match:
use std::log;
pub fn hang_up(c: Conn) !{io, log} {
match c.close() {
Ok(_) => {},
Err((_, os)) => log::warn(f"close failed: {os}"),
}
}The rule covers an expression statement and the tail of a block whose value nobody receives: a while, for, loop, region, domain, recover or test body. Option is exempt, because a None is an answer while an Err is an ignored failure. let _ = does not excuse an unconsumed resource; that is a separate rule and it still applies. In a test, prefer the match with Err(_) => { assert false; }: a test that ignores a refusal passes for the wrong reason.
If the compiler complains
Every diagnostic cites a section of the card, as in error[card §5]: .... Open that section before trying variants at random; the format is explained in the quick start. Most diagnostics also carry a help: line with the repair. If you think you have found a bug or a hole in the language, reduce it to a minimal file and check docs/confusion-log.md in the repository, which is where such findings are recorded. Treat an explanation you came up with from one case as a guess until the minimal file confirms it.