Preface
Within the narrative and theoretical scope of the book, this appendix serves primarily to demonstrate how a memory-safe, C-successor language ought to define its semantic boundaries, rather than to provide a drop-in replacement for a rigorous ISO-level standard.
In this context, CobaltC can best be described as a systems language designed to address the safety problems of C while adopting a more robust structural model.
If nothing else, it illustrates the exact pain points and formal guarantees required to make a safe systems programming language actually function.
As argued throughout The Wrong Memory essays, attempting to tack a "safe subset" onto an inherently unsafe language usually fails because the core language semantics still allow invariant violations. By embedding ownership and borrowing into the core type system, CobaltC makes safety an intrinsic property of the language rather than an optional discipline imposed upon it.
CobaltC unashamedly draws on the ownership and memory-safety techniques demonstrated by Rust. Although Rust isn't the origin of most of these ideas, its significant contribution was demonstrating how they could be combined into a practical, high-performance systems language. CobaltC adopts that safety architecture without attempting to reproduce Rust's syntax or overall language philosophy. Instead, it applies the broader body of ideas to the problem identified in Appendix II: making semantic invariants such as ownership, lifetime, aliasing, mutability, and bounds properties that the compiler can represent and enforce, rather than obligations the programmer must maintain by convention.
The CobaltC Proposition
The distinction is perhaps easiest to see in a deliberately small example. Allocate some storage, keep a second name for it, destroy the storage, and use the second name.
#include <stdlib.h>
int main(void)
{
int* data = malloc(3 * sizeof *data);
data[0] = 7;
int* alias = data; /* a second name for the same storage */
free(data); /* the storage is gone... */
return alias[0]; /* ...but this still compiles, and reads
the wrong memory */
}
C accepts this program without complaint — GCC and Clang compile it
silently even with every common warning enabled. At best, a modern
compiler may warn about a case this obvious; move the free
behind one function call and the warning disappears, while the bug does
not. The program compiles, runs, and reads the wrong memory.
The fact that alias still refers to freed storage exists
only in the programmer's head.
"You have the power. Don't screw it up."
import std;
fn main()
{
Vec<i32> data = Vec::new();
Vec::push(&mut data, 7);
ref<i32, shared> alias = &data[0]; // a second name for the same storage
drop(data); // ✗ rejected before the program runs:
// diag.destroy-while-aliased
printf("%v\n", *alias);
}
The same program, the same shape — allocate, alias, destroy, use — and
it does not run. The fact that alias still points into
data lives in the language, so destroying data
underneath it is refused before the program starts, with a named
diagnostic: heuristic warnings on one side, a semantic guarantee on the
other.
"You have the power. Let’s prove you’re exercising it correctly."
The guarantee is not bought by removing capability. Below, two exclusive (mutable) references into the same array are held at once — something safety-minded languages commonly refuse — legal here because the compiler has proven they touch disjoint elements. CobaltC forbids the crime, not the capability.
import std;
fn main()
{
array<i32, 2> values = [0, 0];
// Two mutable (exclusive) references into the same array, allowed
// because their targets are statically proven to be disjoint
// (pointing both at values[0] is rejected before the program runs:
// diag.aliasing-conflict).
ref<i32, exclusive> a = &mut values[0];
ref<i32, exclusive> b = &mut values[1];
*a = 7;
*b = 8;
}
CobaltC retains the same fine-grained control, while allowing the compiler
to establish the relevant safety properties: a and b
refer to distinct elements of values, their mutable accesses
are disjoint, and the underlying storage remains owned by values.
This guide describes the CobaltC specification, edition 2026.101401.
Introduction
Why CobaltC exists, what it promises, and which features were chosen to keep those promises.
import std;
fn main()
{
printf("hello, world\n");
}hello, world
What CobaltC is
CobaltC is a small systems programming language with a C-family surface syntax. It is designed around one idea: the facts that correct low-level code depends on should live in the language, not in the programmer's memory. When you write C, you have to remember that a pointer is still valid, that an integer cannot overflow here, that a buffer has been initialised, that nobody else is writing to that struct right now, and that this handle is closed exactly once. CobaltC's goal is that every such fact is either represented and checked by the language, or explicitly marked as trusted by you.
CobaltC is not a safe language obtained by removing low-level capability. You still get deterministic control over resources, raw storage, foreign functions, threads, and predictable data layout. What changes is that each of those capabilities comes with a precise account of what is checked, when it is checked, and what you are asserting when the language cannot check it.
The two implementations: coby and cobc
CobaltC ships with two implementations, both written in Rust, and they share one front end — the same parser, name resolution and static checks — so they accept and reject exactly the same programs, with the same diagnostics.
coby is the reference interpreter. It runs a program straight
from its source on the abstract machine the specification defines, performing every check the
specification requires on every operation. It is the specification's meaning made executable:
when this guide says a program prints something or stops with a diagnostic, that is what coby
does. There is no build step, so it is the quickest way to run a program or try an idea.
cobc is the native compiler. After the shared front end has
checked the program, it translates it to plain C and compiles that with the system's C compiler
(GCC or Clang) against cbrt, its run-time library. A compiled program keeps every run-time check
the specification requires except those the compiler proves can never fail — the rest
becomes ordinary C — so it runs many times faster than the interpreter while behaving identically.
cobc --keep-c leaves the generated C beside the executable if you want to read what your program
became; Reading the C that cobc generates, below, says what is in
it and what you can do with it.
The two are kept honest against each other: the test suites require cobc's
executables to match coby's output and diagnostics byte for byte on every program, under both
GCC and Clang — including every complete example in this guide. Use coby to run a program at
once; use cobc to build an executable, or to see how fast the program can be.
On Linux the two work side by side, and everything in this guide runs under
either. On Windows, coby runs programs today, and the formatter cobfmt (below) runs there too;
cobc does not yet follow them there.
coby) and compiler (cobc) are available
here; the repository's README says how to
build them.The objectives
The language design was derived from a short list of objectives, in priority order. Where two objectives conflict, the earlier one wins.
- Preserve semantic invariants. Arithmetic stays in range, references point at live objects, a resource has exactly one owner, an object is initialised before it is read, two threads never race on one location, and external data is treated as a claim until validated.
- Safety of valid safe programs. A well-formed program that writes no
unsafeblock of its own cannot violate any of those invariants on any conforming implementation. The standard library's own bodies do useunsafe(aVecwrites its elements through a raw pointer); those blocks are the implementation's to keep true, each listed in the specification with the fact that justifies it, so they cost the program nothing (§20, Who asserts what). If the language cannot establish a fact, the operation is one of: statically rejected, dynamically checked, explicitly fallible (returns aResultorOption), explicitly trusted (insideunsafe), or simply not provided. - Consistency and composability. A handful of mechanisms that combine cleanly, instead of many overlapping features. Every value-placing construct (declarations, arguments, fields, returns) follows one rule; every access (through a name, a field, an index, a reference) follows one rule.
- Formal precision and deterministic diagnostics. The specification is written as rules, so two implementations must reject exactly the same programs and report the same named diagnostic. Every error you see has a stable identifier such as
diag.aliasing-conflict. - A small language. Every feature starts as absent and must earn its place. The result is a language you can hold in your head: no traits, no inheritance, no exceptions, no implicit conversions, no operator overloading, no macros.
- Low-level capability. Raw pointers, manual allocation, FFI, threads, and defined representation are all available, gated by
unsafewhere the language cannot check them.
Which features were chosen, and why
Each mechanism in CobaltC exists because a specific invariant needed a home. The table below is the map from objective to feature; the rest of this guide explains each feature in turn.
| Invariant to protect | Mechanism CobaltC provides | Where to read |
|---|---|---|
| Arithmetic stays in range | Integer + - * and / % are checked: overflow is a fault, never silent wrap. Wrapping, saturating and checked_ families are explicit opt-ins. There are no implicit numeric conversions; widen, narrow, reinterpret and friends are named calls. | §06 |
| A resource is owned once and released once | Resource types (anything that owns something, marked resource or containing a resource) have a single holder. Assigning or passing one moves it. The old name becomes stale. Destruction is automatic at scope end, in reverse order, running the type's drop destructor. | §07 |
| No conflicting simultaneous access | Two reference modes: ref<T, shared> (read-only, any number) and ref<T, exclusive> (read and write, alone). Conflicts are checked at every use, so a stale or conflicting reference is always caught. There is no mut on variables: mutability is a property of the access path, not the binding. | §08, §09 |
| A reference never outlives what it points at | References cannot be returned or stored beyond their referent's scope; the obvious cases are rejected at compile time, the rest at run time. Functions with one reference parameter may return a reference without annotations. | §10 |
| No read of uninitialised storage | Definite assignment: a variable must be written on every path before it is read. Aggregates are initialised whole. | §11 |
| Types mean what they say | Nominal typing, no subtyping, no coercions. Generics are monomorphic: each use with concrete types becomes its own copy of the function, so there is no run-time type information; a type parameter's operations come from one of five built-in bounds. Type inference is local: auto infers from the initialiser, and generic arguments come from arguments or the expected type. | §12 |
| Evaluation order is defined | Strict left-to-right evaluation everywhere. Blocks, if and match are expressions. | §13, §14 |
| Function interfaces carry the whole contract | A by-value resource parameter takes ownership; a shared reference can only read; an exclusive reference can mutate but not destroy. Closures declare their captures and the language checks the list. | §15 |
| Aggregates are whole and exhaustive | Structs, fixed arrays and enums with payloads. Array indexing is bounds-checked. match must be exhaustive. | §16 |
| Names are unambiguous | Modules with export/import, private by default. Name resolution is a fixed, documented order. | §17 |
| Failure is never silent | Two channels only: a checked fault is fatal and unwinds destructors; recoverable errors are Result values with the ? operator. There is no catch. | §18 |
| Threads do not race | spawn/join with owning handles; a mutex whose interior is reachable only through a lock guard. The same aliasing rules apply across threads. A fault in a spawned thread ends that thread alone and reaches its supervisor as a value; any wait can be cancelled; deadlocks are reported. | §19 |
| External data is a claim, not a fact | unsafe blocks record what you assert. rawptr<T>, reclaim, allocate and unsafe extern fn are the only doors to the outside, and FFI signatures are restricted to plain types. | §20 |
| A text literal is a value; owned text is a resource | A string literal "…" is a str: a copyable value like an integer, valid UTF-8 by construction, never owned or freed. printf writes one. Owned, growable text is String, a resource: moved, destroyed at scope end, built from a str (a literal written where a String is declared becomes one there) or validated from bytes. | §21 |
| The library proves the language is sufficient | Vec<T>, String and Rc<T> are written in CobaltC itself, using nothing the language does not give you. | §21 |
What CobaltC deliberately leaves out
These are considered decisions, not gaps. Each one either duplicated an existing mechanism or would have hidden a semantic fact the language wants to keep visible.
- Implicit conversions and subtyping.
i32andi64never mix silently. - User-defined traits or bounds. A type parameter's operations come from five built-in bounds —
eq,ordered,number,integer,clone(§12) — and nothing else: there is no way to declare a new bound. Your own struct or enum satisfiesclonealone, by its fields or its ownT::clone; to compare or combine values of your own types in generic code, pass the operation in as a function value. - Exceptions. A fault terminates the program after running destructors; expected failure is a
Result. - Garbage collection. Ownership and scope-end destruction are deterministic;
Rc<T>is a library type. - Character literals, and indexing or slicing a
str. Astris a whole value; its bytes are reached withstr_byte, and a single byte is writtenb'a': au8, or the wider unsigned type its context asks for (§06). AStringis sliced into views,&s[i..j], and taken apart byString::split,findandchars(§21). Text that arrives from outside is still bytes untilString::from_utf8validates it. - Method-call syntax. A call names the function's owner and writes the borrow it takes:
Vec::push(&mut v, x), neverv.push(x). The mode of every access path is visible where the path is formed, and.means one thing, field access. This is final (CHG-0132): no receiver sugar, no uniform call syntax, no auto-borrowing dot, no chaining. - A
mutqualifier on bindings,switchandgoto,import … asrenaming, partial initialisation, lifetime annotations, atomics.
How to read this guide
The next section is a complete program you can read top to bottom to absorb the syntax. After that there is one section per chapter of the normative specification, numbered the same way (§06 to §22) so you can cross-reference. If you are new, a good order is: the tour, then §22 (syntax reference), then §06 onward. Each section ends with a short list of rules of thumb.
Every code block that is a complete program has been run through both implementations,
the reference interpreter coby and the native compiler cobc, which agree on every one. A green
runs badge means it executes to completion; a red badge names the
diagnostic the language reports for that program. Diagnostics are labelled static (the
program is rejected before it runs) or dynamic (a checked fault at run time); where a check
is static only when the operands are literal, both labels may appear in the text. Comments inside
examples use ✓ for accepted lines and ✗ for rejected ones.
The language and its standard library are still moving: an edition may change
what an earlier one accepted, and the specification's register says so in each such edition's row.
When a part is offered as stable it is first a candidate for thirty days, then
stable, and after that a program written against it keeps compiling and keeps doing what it
did, apart from a repair that closes a soundness hole or a change announced a release ahead
(spec/STABILITY.md).
Running a program
Save a program as a .cb file. coby program.cb interprets it; cobc --run
program.cb compiles it to native code and runs it, and cobc -o program program.cb keeps the
executable. Both take the program's entry file (it may name other files as modules, §17), both exit
with status 0 when the program runs to completion, and both print a rejected program's diagnostic
the same way and exit with status 1. coby --check program.cb (or cobc --check) runs only
the checks made before a program starts, and prints the same diagnostic for a program they
reject; nothing runs, so a program that reads input, writes files or serves the network can be
checked without doing so. Given a directory, it checks every .cb file in it.
There is no project file and no build step besides these commands. A program that
spans several files names them itself: module parser "./parser.cb"; loads a file as a module, its
path relative to the file that says so (§17), and extern "./fast.c"; or extern "z"; links C code
of the program's own or an installed library (§20). cobc main.cb then builds the whole program
from that one file, and coby main.cb runs it, with nothing else to keep in step (D-0231).
Testing a program
Tests live beside the code they test, in unittest blocks: a block at the top level of a
file or a module, with an optional name. A test sees the module's private items, and is checked every
time the program is, so a test that no longer compiles stops the build at once. Nothing runs it until
you ask for a test run.
import std;
fn parse_port(StringView s) : Option<u16>
{
match (StringView::parse<u32>(s))
{
Ok(n) : if (n > 65535) { None } else { Some(narrow<u16>(n)) },
Err(_) : None,
}
}
unittest "parse_port: bounds"
{
assert(parse_port(StringView::of("0")) == Some(0));
assert(parse_port(StringView::of("65535")) == Some(65535));
assert(parse_port(StringView::of("65536")) == None);
}
unittest
{
assert(parse_port(StringView::of("x")) == None);
}
fn main()
{
printf("%v\n", parse_port(StringView::of("8080")) == Some(8080));
}true
coby port.cb runs the program as always. coby --unittest port.cb runs its tests instead
of main, each in a thread of its own: a failed assert, or any other fault, is reported, and the
next test still runs. cobc --unittest --run port.cb does the same with the compiled program, and
reports it identically; cobc --unittest -o port_tests port.cb keeps the test program.
test port.cb:12 parse_port: bounds ... ok
test port.cb:19 ... ok
2 tests: 2 passed, 0 failed
The exit status is 1 when a test failed, so a test run can gate a build.
--unittest-filter TEXT runs only the tests whose name contains TEXT. A file that holds only
a library and its tests needs no main. A /// comment on a test right after a function makes it
that function's example in the generated documentation (D-0230).
All code in this guide uses Allman brace style (every opening brace on its own line) and four-space indentation. That is a documentation convention: CobaltC ignores whitespace between tokens, so any brace placement is equally valid.
cobfmt, the formatter, writes that style for you: cobfmt program.cb rewrites the
file in place, cobfmt --backup src/ does every .cb file under src/ and keeps each changed
file's previous contents as NAME.cb.format.bak, and cobfmt --check src/ only lists the files it
would change (for a test or a commit hook). It changes layout and nothing else: indentation, braces,
spacing, blank lines, the : of a match's arms and of a bitstruct's fields aligned in one column, and trailing comments kept
in theirs. Before writing, it checks that the result is the same program token for token, so
formatting can never change what a program does. It keeps your line breaks — a short
if (n == 0) { return; } stays on one line, though a function's body always opens on its own line — and a line longer than 120 characters
that holds a list of arguments, parameters or array elements is broken one item per line. --width N
sets another width, and --no-wrap leaves every line as long as it is. cobfmt --help lists the rest. Like coby, cobfmt runs on Windows as well as
Linux, and it keeps a file's Windows line endings (\r\n) as they are.
Reading the C that cobc generates
cobc does not produce machine code itself. Once the shared front end has checked
your program, cobc translates it into one C file and hands that file to GCC or Clang, which
builds the executable against libcbrt.a, the run-time library. Normally the C file is deleted
afterwards; cobc --keep-c -o prog prog.cb keeps it as prog.c, beside prog (without -o,
a.out gets a.c). Under --run without -o it lives in a temporary directory and is deleted
with it. If the C compiler ever rejects the file, cobc keeps it whatever you asked and prints
its path.
The file is the complete translation of your program, not an excerpt: nothing but
libcbrt.a is needed to build it. Even a seven-line program comes to a few hundred lines,
because the file carries everything it uses, in this order:
- The run-time interface,
cbrt.h, copied in at the top: the C declarations of everycb_…function the program calls. /* ---- types ---- */: each struct, enum and generic instantiation as a C struct, each followed by a_Static_assertthat its size and alignment are the ones the specification fixes.Vec<i32>becomesstruct s_std__Vec_Li32_R./* ---- string literals ---- */: your text literals as byte arrays./* ---- functions ---- */: one C function for each function the program actually uses,std's included, and one copy per instantiation of a generic one (f_std__Vec__push_Li32_RisVec::pushfori32). Yourmainisf_main; every other function isstatic, so the C compiler may inline it where it is called.- Call thunks and type descriptors: what
fnvalues and the run-time checks need to know about each type. int main, last: it hands the program's arguments to the runtime and startsf_main.
Inside a function, the C follows your statements one by one, and every run-time
check the specification requires is an explicit line, so you can see exactly which checks your
code still pays for. This is printf("%v\n", v[0]); with v a Vec<i32> (without the
#line directives described below):
cb_at(0u, 7u); // now at line 7 of file 0, for diagnostics
struct s_std__Vec_Li32_R *v9 = &(l_v_1);
if (t8 >= v9->len) cb_fault("diag.index-out-of-bounds", CB_NOLOC, 0);
int32_t *e10 = v9->ptr + (int64_t)t8; // the element's address
int32_t t11 = (*e10);Elsewhere you will meet cb_borrow, cb_read and cb_write (the access and
aliasing checks of §08–§09), __builtin_add_overflow and its relatives (checked arithmetic, §06),
and cb_frame_push, cb_stmt_push and their _pop partners (the bookkeeping that runs
destructors at scope end). A check the compiler has proved can never fail is simply absent.
Each line of C made from one of your statements is preceded by a directive such as
#line 7 "prog.cb", which tells the C compiler which line of your file the next line of C came
from. The C compiler's own messages, a debugger and a sanitizer then name prog.cb:7 rather than a
line of prog.c; code made for std keeps the C file's own numbering. cobc --no-line-directives
leaves the directives out, for reading the C on its own terms.
What you can do with it
- Read it to see how a construct is compiled, which checks a loop costs, or why one way of writing it is faster than another. Performance (chapter P) collects what such reading and measuring found.
- Build it yourself, with your own flags.
cobc's own build ofprog.cis the command below, andcobc -vprints the exact one it runs, with the real path oflibcbrt.a(and anyexternC file's compilation), ready to paste and change;-O2is the default level andlibcbrt.asits beside thecobcexecutable. Add-g -O0for a build to step through in a debugger,-fsanitize=address,undefinedto have the sanitizers watch it,-march=nativeor-flto, or leave out-wto see the C compiler's warnings, whichcobcsilences. - Debug it.
cobc -O0 -g prog.cbcompiles with debug information and keeps it in the executable;gdb progthen stops at and steps through the lines ofprog.cb, and a sanitizer build (--ccnaming a wrapper that adds-fsanitize=address) reports a memory error inunsafecode at its.cbline.perf annotateshows the C beside them. - Report a compiler bug. If GCC or Clang rejects the generated C, that kept file and the C compiler's message are the whole report.
gcc -std=gnu11 -O2 -ffp-contract=off -fno-optimize-sibling-calls -w prog.c \
-L /path/to/cobc/dir -lcbrt -lpthread -ldl -lm \
-Wl,--gc-sections -Wl,--strip-debug -o progKeep -std=gnu11 and the two -f flags when you change the rest; each is part of
the program's meaning. -ffp-contract=off stops the C compiler fusing a * b + c into one
fused multiply-add with a single rounding, since the specification rounds each operation on its
own (§06). -fno-optimize-sibling-calls keeps every call a real call, so a recursion too deep for
the stack stops with the same diagnostic it gets from coby, instead of being turned into a loop
that runs to completion. A program that links its own C code (extern "./mine.c";, §20) also
needs that code compiled and added before -lcbrt; cobc -v shows both commands. The last two
options only make the executable smaller: they leave out the parts of the runtime the program does
not use, and the runtime's debug information (a hello world is about 0.8 MB with them, 5.9 MB
without); drop them if you link a -g build to debug. cobc -s also leaves out the symbol table
(--strip-all in place of --strip-debug, about 0.6 MB), which only profilers and debuggers read.
cobc, and it is only correct with the
libcbrt.a built alongside the same cobc. cobc refuses a mismatched runtime, but a build by hand
skips that check.Why compile to C
Compiling through C is a long tradition rather than a shortcut: C++ began as Cfront, which translated it into C, and Nim, Vala, Cython and several Scheme compilers work this way today. Few compilers write machine code entirely on their own; most hand the last step to a shared back end such as LLVM or GCC's. C as the target brings:
- Portability for free. Wherever there is a C compiler, the language can run.
- Decades of optimisation for free. GCC's and Clang's
-O2do register allocation, inlining, vectorisation and instruction scheduling better than a young compiler could. - Readable output. You can see what your program became, which is what
--keep-cis for; LLVM IR or assembly would be far harder to read. - Easy C interoperation. Calling C functions and linking C libraries is natural, which is what
unsafe extern fn(§20) needs.
What it costs
- C's own rules come along. The generated C must avoid C's undefined behaviour, and it has to keep up as C compilers grow stricter: code an older compiler accepted with a warning, a newer one may reject outright.
- Less control. Some things C cannot say directly (exact stack layout, particular calling conventions, precise debug information), so they are worked around with flags such as
-ffp-contract=offabove. - Slower builds. Every build includes a full C compilation.
- Debuggers show C lines, not CobaltC lines.
cobcemits no#linedirectives, sogdbsteps through the C; thecb_at(file, line)calls lead back to your source.
For CobaltC the trade is a good one. Its central promise is that coby and cobc
behave identically, and letting GCC and Clang do the machine-code work leaves cobc free to get
the semantics and the checks exactly right.
A tour in one program
A complete, runnable program that introduces the syntax and the main capabilities, and the mistakes the compiler stops, before the reference sections begin.
Here is a complete program. Read it once for the shape of the syntax; every construct it uses has its own section later. It runs under both the interpreter and the compiler and prints the output shown below the code.
// ------------------------------------------------------------------
// A first CobaltC program: a temperature log.
//
// One page that touches most of what CobaltC is for: structs, an enum
// with payloads and `match`, a resource with a destructor, Vec, String
// and HashMap, references the compiler checks, slices, Option, Result
// and `?`, checked arithmetic, generics, a closure, and two threads
// sharing a counter through a mutex.
// ------------------------------------------------------------------
import std;
// A module. Its `export` items are reached as `limits::COLD`; the rest
// stay inside. A `const` is a value fixed when the program is compiled.
module limits
{
export const i32 COLD = 150; // tenths of a degree
export const i32 HOT = 300;
}
// A plain struct: copied on assignment, comparable with ==.
struct Reading
{
i32 tenths; // temperature in tenths of a degree
u8 sensor;
}
// An enum. Variants may carry a payload.
enum Verdict
{
Cold(i32),
Fine,
Hot(i32)
}
// `ref<Reading, shared>` is a read-only reference. `if` is an expression,
// so the function's value is whichever branch runs.
fn judge(ref<Reading, shared> r) : Verdict
{
if (r.tenths < limits::COLD)
{
Cold(limits::COLD - r.tenths)
}
else if (r.tenths > limits::HOT)
{
Hot(r.tenths - limits::HOT)
}
else
{
Fine
}
}
// A resource: something that owns other things. It is moved, never
// copied, and destroyed exactly once, automatically, at scope end.
resource struct Log
{
Vec<Reading> entries;
HashMap<String, u32> per_sensor; // readings counted by sensor name
}
// The destructor. It is never called directly; the language runs it.
fn Log::drop(ref<Log, exclusive> self)
{
printf("L%d\n", Vec::len(&self.entries)); // printf's format is checked when compiled
}
fn record(ref<Log, exclusive> log, i32 tenths, u8 sensor)
{
Vec::push(&mut log.entries, Reading { .tenths = tenths, .sensor = sensor });
*HashMap::entry(&mut log.per_sensor, sprintf("sensor-%d", sensor), 0) += 1;
}
// A slice is a borrowed run of elements. Arithmetic never wraps
// silently: `checked_add` gives None on overflow, and `?` on an Option
// returns that None from this function at once.
fn average(slice<Reading, shared> rs) : Option<i32>
{
if (slice_len(rs) == 0)
{
return None;
}
i32 total = 0;
foreach (r in rs)
{
total = checked_add(total, r.tenths)?;
}
Some(total / narrow<i32>(slice_len(rs))) // `narrow` checks that the value fits
}
// A fallible function returns a Result. Err carries the offending byte.
fn digit(u8 b) : Result<i32, u8>
{
if (b > b'9' || b < b'0')
{
return Err(b);
}
Ok(widen<i32>(b - b'0')) // widening is always safe, still spelled out
}
// `?` unwraps an Ok, or returns the Err from this function at once.
fn parse_two(u8 hi, u8 lo) : Result<i32, u8>
{
i32 h = digit(hi)?;
i32 l = digit(lo)?;
Ok(h * 10 + l)
}
// A generic function: T is any integer type, fixed at each call.
fn larger<T: integer>(T a, T b) : T
{
if (a > b) { a } else { b }
}
// Runs on its own thread: counts the hot readings in its half of the
// log into a counter the two threads share. Only the lock reaches it.
fn count_hot(ref<mutex<u32>, shared> hot, slice<Reading, shared> part) : usize
{
foreach (r in part)
{
if (r.tenths > limits::HOT)
{
*lock(hot) += 1;
}
}
slice_len(part)
}
fn main()
{
Log log = Log { .entries = Vec::new(), .per_sensor = HashMap::new() };
record(&mut log, 120, 1);
record(&mut log, 215, 1);
record(&mut log, 340, 2);
record(&mut log, 310, 2);
// Loop over the entries through a shared reference to each one.
// `match` takes the Verdict apart; it must handle every variant.
foreach (r in &log.entries) // r : ref<Reading, shared>
{
match (judge(r))
{
Cold(by) : printf("C%d ", by), // "C" then the shortfall
Fine : printf("F "),
Hot(by) : printf("H%d ", by), // "H" then the excess
}
}
printf("\n");
// A lookup gives an Option; a pattern condition takes it apart.
if (Some(n) = HashMap::get(&log.per_sensor, &String::from_str("sensor-2")))
{
printf("sensor-2: %d\n", *n);
}
// A slice of the Vec, and an average that cannot overflow unseen.
if (Some(avg) = average(&log.entries[0 .. 2]))
{
printf("avg %d\n", avg);
}
// Sorting, and a closure: `to_f` captures nothing, `offset` is read.
Vec<i32> temps = Vec::new();
foreach (r in &log.entries)
{
Vec::push(&mut temps, r.tenths);
}
Vec::sort(&mut temps);
i32 offset = 32;
auto to_f = [offset](i32 tenths)
{
tenths * 9 / 50 + offset
};
printf("coldest %dF, warmest %dF\n", to_f(temps[0]), to_f(temps[3]));
// Result handling: `b"42"` is a byte-array literal (52 and 50).
auto digits = b"42";
if (Ok(v) = parse_two(digits[0], digits[1]))
{
printf("%d %d\n", v, larger(v, 17)); // T := i32
}
// Two threads share the counter through a mutex; each gets its own
// half of the log as a slice. `join` waits and gives the result.
auto hot = Mutex::new(0: u32);
auto h1 = spawn(count_hot, &hot, &log.entries[0 .. 2]);
auto h2 = spawn(count_hot, &hot, &log.entries[2 .. 4]);
usize seen = join(h1) + join(h2);
printf("%d of %d readings hot\n", *lock(&hot), seen);
} // `log` is destroyed here: its destructor prints "L4", then its Vec and HashMap are freed.C30 F H40 H10 sensor-2: 2 avg 167 coldest 53F, warmest 93F 42 42 2 of 4 readings hot L4
What the compiler stops
The program above is correct, so it shows what CobaltC lets you write. Just as much of the language is what it will not let through. Each of these mistakes, a one-line change to the same kind of code, is rejected before the program runs, by both the interpreter and the compiler, with a diagnostic that names the rule; what cannot be decided in advance is checked while the program runs, and stops it at the line where it happens (§08–§10).
import std;
struct Reading
{
i32 tenths;
u8 sensor;
}
fn main()
{
Vec<Reading> entries = Vec::new();
Vec::push(&mut entries, Reading { .tenths = 120, .sensor = 1 });
ref<Reading, shared> first = Vec::index_shared(&entries, 0);
Vec::push(&mut entries, Reading { .tenths = 215, .sensor = 1 }); // ✗ the push may move the elements
printf("%d\n", first.tenths); // while `first` still points into them
}import std;
resource struct Log
{
Vec<i32> entries;
}
fn archive(Log l) // takes the log: it is destroyed when this call ends
{
}
fn main()
{
Log log = Log { .entries = Vec::new() };
archive(log); // `log` moves into the call
Vec::push(&mut log.entries, 7); // ✗ `log` no longer owns anything
}import std;
fn digit(u8 b) : Result<i32, u8>
{
if (b > b'9' || b < b'0')
{
return Err(b);
}
Ok(widen<i32>(b - b'0'))
}
fn main()
{
digit(b'x'); // ✗ an error dropped on the floor would go unseen
// Handle it (`?`, `match`, `if (Err(e) = …)`), or say you mean to drop
// it: `_ = digit(b'x');` discards the value on purpose.
}import std;
fn main()
{
i32 tenths = 2000000000;
i32 twice = tenths * 2; // ✗ the product does not fit in an i32: no silent wrap
printf("%d\n", twice);
}enum Verdict
{
Cold(i32),
Fine,
Hot(i32)
}
fn label(Verdict v) : i32
{
match (v) // ✗ `Hot` is not handled: every variant must be
{
Cold(by) : -by,
Fine : 0,
}
}
fn main()
{
label(Fine);
}Reading a diagnostic
Both tools report a mistake the same way, character for character. This is what either prints for the first example above, a reference kept across a push:
error[diag.aliasing-conflict]: `first` still borrows `entries`, so it cannot be written or borrowed `mut` here; end that borrow first
--> tour.cb:14:15
|
14 | Vec::push(&mut entries, Reading { .tenths = 215, .sensor = 1 });
| ^
= repair: end the conflicting reference (let its holder go out of scope) before this access, or use `shared` on both sides
- The first line says what went wrong.
errormeans the program was rejected before it ran; a check that fails while the program runs is afaultinstead (fault[diag.index-out-of-bounds]: index out of bounds), and the program stops there. In the brackets is the diagnostic's identifier: it is the same in every implementation and every version, and the specification's diagnostic registry has an entry for each. -->gives the file, the line and the column, in the form editors and terminals turn into a link.- The source line, with carets under the token the rule applies to.
= repair:is the registry's advice for this diagnostic.
coby --explain tour.cb (or cobc --explain) adds the registry's whole entry: the rule of
the specification that rejected the program, the invariant it protects, what it required and what
it observed, and the examples that show it. A compiled program shows its faults that way when it
runs with COBALTC_EXPLAIN=1 in its environment.
What you just saw
- Declarations are type-first, like C:
i32 x = 5;,Vec<Reading> entries;.autoinfers the type from the initialiser. Function return types follow a colon:fn judge(…) : Verdict. - Blocks,
ifandmatchare expressions. The last expression in a block, with no semicolon, is the block's value.judgereturns whatever branch of theifruns. - Conditions need parentheses (
if (c),while (c),match (e));matcharms arePattern : expression,, and amatchmust cover every variant (a missing one is rejected, above). A pattern can also be a condition:if (Some(n) = HashMap::get(…))runs its block only when the lookup found something, withnbound. &xis a shared (read-only) reference and&mut xan exclusive one. The compiler checks every use: a reference kept into theVecacross apush, a value used after it moved, are rejected before the program runs (above).&log.entries[0 .. 2]is a slice, a borrowed run of elements.Logis aresource: it owns aVecand aHashMap. Its destructorfn Log::dropruns at the end ofmain, then its fields are freed. Passing or assigning a resource moves it.String,HashMapandsprintf.sprintf("sensor-%d", s)builds aString;*HashMap::entry(&mut m, key, 0) += 1counts. The standard library lives in the modulestd, imported byimport std;.- Errors are values. A
Resultmust be used (dropping one is rejected),?returns anError aNoneearly, andchecked_addturns an overflow intoNone. Plain arithmetic that overflows is never silent: it is rejected when the compiler can see it, and stops the program when it happens. - No implicit conversions.
widen<i32>(b - b'0')andnarrow<i32>(n)say what happens to the representation;narrowchecks that the value fits. - Library calls are qualified:
Vec::push(&mut v, x),Vec::sort(&mut v),HashMap::get(&m, &k). There is no method-call syntax. - Generics, closures and threads.
larger<T: integer>works for any integer type;to_fis a closure whose capture list names what it uses;spawnstarts a thread andjoinwaits for its result. The two threads share their counter only through amutex:lockis the only way in.
Further showcases can be found in the repository's showcase directory; its README describes them all.
Core language and std
Which parts of CobaltC are the language itself, always available, and which come from the standard library and need import std;.
CobaltC comes in two layers. The core language is always there: its
types, its constructs, and a set of intrinsics, functions defined by a rule of the
specification rather than by a body. The standard library, std, is a module,
written mostly in CobaltC itself. Nothing of std is in scope until a module writes
import std;, and each module imports it for itself (§17). This page says which
layer each part belongs to.
// No `import std;`: only the core language.
fn main() : u8
{
array<i32, 4> a = [3, 4, 5, 6];
i32 s = 0;
foreach (x in a)
{
s += x;
}
u32 m = max_value<u32>();
s += narrow<i32>(m % 10); // 4294967295 % 10 is 5
narrow<u8>(s) // the exit status: 23
}fn main()
{
printf("hello\n"); // ✗ diag.unbound-name (static): printf is std's
}Core: always available, no import
Types built into the language
- Integers
i8–i128,u8–u128,isize,usize; floatsf32,f64 bool,str(string literals),void/()array<T, N>,slice<T, shared>andslice<T, exclusive>ref<T, shared>andref<T, exclusive>,rawptr<T>fn(…) : Rvalues and closureshandle<T>(a thread),mutex<T>,guard<T>
Language constructs
struct, bitstruct, enum (with numeric codes), resource, destructors (Type::drop),
generics with the built-in bounds eq, ordered, number, integer and clone, match with
nested, literal and text patterns, if and while (either with a pattern as the condition), for,
foreach (over collections and ranges), break, continue, ?, const, modules and import, unsafe,
unsafe extern fn and extern "code";.
Intrinsics
| Group | Names |
|---|---|
| Resources | drop |
| Conversions | widen, narrow, checked_narrow, narrow_wrapping, reinterpret, to_float, to_int |
| Bits | count_ones, leading_zeros, trailing_zeros, rotate_left, rotate_right |
| Arithmetic | wrapping_add/sub/mul, saturating_add/sub/mul, checked_add/sub/mul/div/rem, checked_neg |
| Numeric limits | min_value, max_value |
| Layout | sizeof, alignof |
str and slices | str_len, str_byte, str_ptr, slice_len |
| Faults and compile-time checks | fault, static_assert |
| Threads | spawn, join, cancel, join_status, Mutex::new, lock |
Raw memory (most need unsafe) | rawptr_of, reinterpret_ptr, dangling, allocate, deallocate, reclaim, release, copy_raw, read_volatile, write_volatile |
allocate returns a Result, which a program can use without the import even though it
cannot name the Result type without it.
Intrinsics private to std
key_hash, key_eq, key_less, key_less_at, swap_places, append_native,
text_write, parse_check, parse_value, str_slice (beneath StringView::of), and the names
that begin with $: the machinery std's own code is built on. A program cannot call them, nor
std's private externs stderr_write, which eprintf writes through, and stdout_flush, beneath
flush_stdout.
std: needs import std;
Mostly written in CobaltC itself; a few parts are provided natively by each
implementation. std is divided into submodules by subject, and import std; brings in all of
them, so a program never needs to know which holds what; the column says, for a program that wants
one subject alone (import std::text;). §21 covers it in full.
| Area | Items | Submodule |
|---|---|---|
| Core types | Option<T>, Result<T, E>, AllocError; unwrap, unwrap_or, expect, ok_or, is_some/is_none/is_ok/is_err; map_err | std::core |
| Collections | Vec<T> (push, pop, index, insert, remove, swap_remove, retain, dedup, clone, from_slice, filled, from_fn, append, contains, position, reverse, truncate, swap, clear, …), HashMap<K, V>, HashSet<K>, Queue<T>, PriorityQueue<T>, slice_parts | std::collections |
| Sorting | Vec::sort, sort_by, binary_search, binary_search_by | std::collections |
| Ownership helpers | Box<T>, Rc<T>, Weak<T> | std::memory |
| Values behind references | swap, replace, overwrite | std::core |
| Numeric helpers | min, max, abs, pow, clamp, gcd, is_nan, is_finite (over the built-in bounds) | std::math |
| Floating-point functions | sqrt, floor, ceil, round, trunc, ln, exp, log2, log10, sin, cos, tan, asin, acos, atan, atan2, powf | std::math |
| Random numbers | Rng, a seeded pseudo-random generator (Rng::from_os for a fresh seed, Rng::shuffle); os_random_bytes, os_random_u64 for secrets; uuid_v4 | std::random |
| Networking | TcpListener (bind, accept), TcpStream (connect, read_line, read, write, printf, shutdown_write, try_clone, timeouts, keepalive), UdpSocket (send_to, recv_from, connect, send, recv), IpAddr, SocketAddr, resolve, NetError | std::net |
| Other programs | Command (arg, current_dir, env, output, status, spawn), Child (read_output_line, read_error_line, write_input, wait, terminate, kill), process_id, watch_interrupts, interrupt_requested, exit | std::process |
| Bytes | Vec::push_le, push_be, read_le, read_be | std::collections |
| Checksum | crc32 (CRC-32/IEEE; catches accidents, not tampering) | std::crypto |
| Text | String, StringView (split, lines, split_whitespace, chars, code_point, find, trim, starts/ends_with, strip_prefix/suffix, parse), String::parse<T>, String::append, push_code_point, sprintf, String::appendf, the ASCII tests and case changes, Utf8Error, ParseError | std::text |
| Output | printf, eprintf, flush_stdout; the extern stdout_write | std::io |
| Checks | assert | std::core |
| Input | read_line, read_all, read_all_bytes; the extern stdin_read | std::io |
| Files | File (open, create, create_new, append, open_rw, read, read_line, write, seek, position, flush, len, close, …), read_file, write_file, read_bytes, write_bytes, FileError | std::fs |
| Directories and paths | make_dir, remove_file, remove_dir, rename, list_dir, path_kind, make_dir_all, remove_dir_all, copy_file, file_info, the path_ functions (join, parent, file_name, stem, extension, is_absolute, normalize, canonical) | std::fs |
| Arguments and environment | arg_count, arg and the extern arg_bytes; Args (flag, option, options, finish, help); env_var, current_dir, set_current_dir, temp_dir, home_dir, current_exe, hostname | std::env |
| Clocks | monotonic_ns, unix_seconds, sleep_ms; dates and times are DateTime | std::time |
| Concurrency | Channel<T> (new, send, recv, try_recv, recv_timeout_ms, close), cpu_count, try_join, is_finished, ThreadFailure (text, diagnostic, is_cancelled) | std::sync |
| Cryptography | sha256, sha384, sha512, sha1 (legacy), blake2b, hmac_sha256, hmac, digest_eq, hkdf_sha256, chacha20_poly1305_seal/open, aes_gcm_seal/open, x25519, ecdh, ed25519_sign/verify, ecdsa_sign/verify, rsa_sign_pss/verify_pss, hash_password/verify_password; to_hex, from_hex, base64_encode, base64_decode in std::text | std::crypto; to_hex, from_hex, base64_encode, base64_decode: std::encoding |
| HTTP | http_get, http_post, HttpClient, Url, form_decode/form_encode; HttpServer (bind, bind_tls), HttpConnection::request/respond | std::http |
| JSON | Json (parse, text, pretty, get, at, as_*, set, push), JsonMember | std::encoding::json |
| Compression | deflate/inflate, zlib_deflate/zlib_inflate, gzip/gunzip, adler32, CompressError | std::compress |
| Databases | PgConnection::connect, query, execute, run_script; PgValue, PgResult, PgRow::get, get_i64, get_f64, get_bool, get_bytes | std::database::postgres |
| Big integers | BigUint (add, sub, mul, div_rem, mod_pow, shifts, text, parse, bytes) | std::math |
| Certificates and TLS | Certificate, TrustStore, verify_chain; TlsStream (connect, client, server, read, read_line, read_exact, write, peer_certificate, close) | std::x509, std::tls |
| Dates and times | DateTime (from_unix, now, new, to_unix, to_iso, from_iso, to_http_date, from_http_date, weekday, less, to_local), Weekday, unix_ms, iso_from_unix_ms, unix_ms_from_iso, local_offset_seconds — UTC, and the machine's local time | std::time |
What may surprise you
printfandassertarestd, not core: a program without the import cannot print.Vec,StringandOptionarestd, butarray,str,slice,handleandmutexare core.swap, the one to use, isstd;swap_places, which it is built on, is private tostd.minandmaxarestd;min_valueandmax_valueare core.sqrt,roundand the other floating-point functions arestd(std::math), not core: they are ordinary library functions, so they need the import, and a function of your own namedroundis an ordinary name.
§06Arithmetic and numeric types
spec/06-arithmetic.mdChecked integer arithmetic, explicit conversions, how literals get their types, and the sizes and limits of types.
Numbers are where silent corruption usually starts, so CobaltC's arithmetic is
checked by default: an operation whose mathematical result does not fit its type does not
wrap around, it faults. Every other policy (wrapping, saturating, or returning None) is a named
function you call on purpose. Types never convert implicitly; every change of representation is a
named intrinsic with a documented behaviour.
Numeric types
| Type | Meaning | Notes |
|---|---|---|
i8 i16 i32 i64 i128 | signed integers of the given width | two's complement |
u8 u16 u32 u64 u128 | unsigned integers of the given width | |
isize usize | signed / unsigned integers of address width | the types of indices, lengths, sizes and pointer offsets; the width (16, 32, 64 or 128 bits) is fixed and documented by each implementation |
f32 f64 | IEEE 754 binary32 / binary64 | ±Inf and NaN are ordinary values; float arithmetic never faults |
bool | true or false | produced by comparisons and && || ! |
There is no implicit promotion: i32 + i64 is a type error, and a u8 cannot be passed
where an i32 is expected. Unary minus applies only to signed and float types.
Literals
Integer literals are digit strings: decimal, or hexadecimal, octal or binary after 0x,
0o or 0b (0xFF, 0o755, 0b1010), with no sign. A _ between two digits groups them and
is ignored: 1_000_000, 0xFFFF_0000, 3.141_592 (anywhere else, a _ is a syntax error). A literal's
type comes from, in order: an explicit suffix 123: u8; the expected type at that position
(a declared variable type, a parameter type, a field type, or the other operand of the same
operator); otherwise the default, i32 for integers and f64 for floats. An expression made only
of literals and operators takes the expected type as a whole, so u64 x = 1024 * 1024 * 1024 * 16;
is computed in u64, while auto y = 65536 * 65536; has no expected type, is an i32, and
overflows. A literal that does not
fit its type is a static error. A minus written directly before an integer literal is checked
with it, as one negative value, so -2147483648 is the smallest i32. A type's limits also
have names, min_value<i32>() and max_value<i32>() (below). Float literals are digits . digits, with an
optional exponent (1.5e-3, 2.5E3, or 1e5 with no fraction), optionally suffixed : f32;
one too large for its type (1e400) is a static error, not infinity. A byte character b'a'
is one ASCII character or an escape (b'\n', b'\'', b'\x7f'), for writing bytes readably
in code that parses. It is a number like any other literal and takes an unsigned type from its
context (u32 c = …; c == b'|' compares two u32s), and is a u8 when nothing fixes its type;
a signed context is a type error. A string literal "…" is always a str and a byte literal
b"…" always an array<u8, N> (§21, §22); neither takes an expected type.
import std;
fn main()
{
auto a = 5; // i32: no suffix, no expected type
u8 b = 200; // u8 from the declared type; 200 fits
auto c = 7: u64; // u64 from the suffix
auto d = b + 1; // the 1 takes u8 from b; d is u8
auto e = 2.5; // f64
f32 f = 0.5: f32;
f64 tiny = 1.5e-3; // an exponent: 0.0015
u8 comma = b','; // a byte character: 44
u32 mask = 0xFF00; // hexadecimal; also 0o17 (octal) and 0b1010 (binary)
u64 ns = 1_000_000_000; // _ groups digits: one second in nanoseconds
u64 sixteen_gib = 16 * 1024 * 1024 * 1024; // literals only: computed in u64
i64 g = -1; // the expected type reaches through unary minus
i32 lo = -2147483648; // min(i32): 2147483648 alone would not fit
assert(d == 201);
assert(g < 0);
assert(lo + 2147483647 == -1);
assert(tiny == 0.0015 && comma == 44 && mask == 65280);
assert(sixteen_gib == 17_179_869_184 && ns == 1000000000);
static_assert(sizeof<u64>() == 8); // sizeof and alignof are intrinsics returning usize
static_assert(alignof<u16>() == 2);
}fn main()
{
256: u8; // ✗ diag.literal-out-of-range (static): 256 does not fit u8
}Checked arithmetic
+, - and * on integers fault with diag.arith-overflow when the result leaves the
type's range. / and % truncate toward zero (the remainder takes the sign of the dividend);
dividing by zero is diag.div-by-zero, and the one overflowing division, the minimum value of a
signed type divided by -1, is diag.div-overflow. Negating the minimum signed value is an
overflow too. When both operands are literals the fault is detected statically and the program is
rejected; otherwise it is a run-time fault that terminates the program (see §18). A local
initialized with a literal counts as that literal until something writes it, borrows it or
captures it in a closure, so u8 x = 200; u8 y = x + 100; is rejected too. After such a write,
or after an if that writes it on only one branch, the check happens at run time.
fn add(i32 a, i32 b) : i32
{
a + b // checked: faults if the sum leaves i32
}
fn main()
{
add(2147483647, 1); // ✗ diag.arith-overflow (dynamic): 2^31 does not fit i32
}fn main()
{
2147483647 + 1; // ✗ diag.arith-overflow (static): both operands are literals
}fn div(i32 a, i32 b) : i32
{
a / b
}
fn main()
{
div(-2147483647 - 1, -1); // ✗ diag.div-overflow (dynamic): -MIN does not fit
}Choosing a different overflow policy
When wrapping or clamping is what you actually want, say so. The wrapping_ and
saturating_ families never fault; the checked_ family returns an Option so the caller can
decide. These are ordinary generic calls with the operand type inferred from the arguments.
| Call | Result | On overflow |
|---|---|---|
wrapping_add(a, b), wrapping_sub, wrapping_mul | same type | wraps modulo 2N |
saturating_add(a, b), saturating_sub, saturating_mul | same type | clamps to the type's minimum or maximum |
checked_add(a, b), checked_sub, checked_mul, checked_div, checked_rem | Option<T> | None (also for division by zero) |
checked_neg(a) | Option<T> | None for the most negative value, and for any non-zero unsigned value |
import std;
fn main()
{
u8 top = 255;
assert(wrapping_add(top, 1: u8) == 0); // 255 + 1 wraps to 0
assert(saturating_add(top, 9: u8) == 255); // clamps at the maximum
match (checked_add(top, 1: u8))
{
Some(_) : // must not happen
{
assert(false, "unreachable");
},
None : {}, // overflow reported as None
}
match (checked_add(1: u8, 1: u8))
{
Some(v) :
{
assert(v == 2);
},
None :
{
assert(false, "unreachable");
},
}
}Shifts, bitwise operators and comparison
<<and>>take au32shift amount that must be less than the type's bit width; anything else isdiag.shift-amount-out-of-range. Left shift discards high bits by definition (it is not an overflow); right shift is arithmetic on signed types.&,|,^and~work on any integer type;!is boolean not.== != < <= > >=compare two operands of the same type and yieldbool. Floats follow IEEE rules: every comparison involvingNaNis false except!=.
import std;
fn main()
{
u32 one = 1;
assert(one << 4 == 16);
static_assert(-8 >> 1 == -4); // arithmetic shift keeps the sign
static_assert((12 & 10) == 8);
static_assert((12 | 3) == 15);
static_assert((6 ^ 3) == 5);
static_assert(~0 == -1);
f64 zero = 0.0;
auto nan = zero / zero; // floats do not fault
assert(!(nan == nan));
assert(nan != nan);
}Conversions are explicit
There is exactly one way to change a number's type: call the intrinsic that names the conversion. Each takes the target type as a type argument.
Five intrinsics work on an integer's bits directly: count_ones(x), leading_zeros(x) and
trailing_zeros(x) (each a u32; zero gives the type's width), and rotate_left(x, k) and
rotate_right(x, k) (by k modulo the width, keeping x's type). They act on the value's cell
image, so a negative i8 has eight ones, and none of them faults (D-0123).
| Intrinsic | From → to | Behaviour |
|---|---|---|
widen<U>(x) | integer to a wider integer whose range contains every value; f32 to f64 | always exact, never faults (u8 → i32, i32 → i64, f32 → f64) |
narrow<U>(x) | any integer to any integer | checked: diag.narrowing-overflow if x does not fit |
checked_narrow<T>(x) | any integer to any integer | Option<T>: Some(x) when x fits T, else None — for a value from outside, where a fault would be wrong (D-0122) |
narrow_wrapping<U>(x) | any integer to any integer | keeps the low bits, never faults |
reinterpret<U>(x) | between the signed and unsigned types of one width (and usize ⇄ isize); between a float and an integer of its width (f32 ⇄ u32/i32, f64 ⇄ u64/i64) | the same bit pattern read as the other type |
to_float<U>(x) | any integer or float to f32/f64 | nearest representable value (ties to even); beyond the target's range an infinity, as float arithmetic gives; NaN stays NaN |
to_int<U>(x) | float to integer | truncates toward zero; checked: infinities, NaN and out-of-range values fault |
import std;
fn main()
{
u8 small = 200;
i32 wide = widen<i32>(small); // always safe
assert(wide == 200);
assert(narrow<u8>(wide) == 200); // fits, so it succeeds
static_assert(narrow_wrapping<u8>(300) == 44); // 300 mod 256
static_assert(reinterpret<i32>(4294967295: u32) == -1);
f64 f = to_float<f64>(wide);
assert(to_int<i32>(f * 1.5) == 300);
usize n = 12;
assert(narrow<i32>(n) == 12); // usize → i32 is a narrowing, so it is checked
}Between the two float types: widen<f64>(x) takes an f32 to an f64 exactly, and
to_float<f32>(x) takes an f64 to the nearest f32, rounding as float arithmetic does. There
is no widen<f32> of an f64, since most f64 values have no exact f32. reinterpret gives
a float's IEEE-754 bits as an integer of the same width, and turns such an integer back into a
float, with no unsafe.
import std;
fn main()
{
f32 sample = 0.1;
f64 d = widen<f64>(sample); // exact: 0.100000001490116...
assert(to_float<f32>(d) == sample); // and back, rounded, to the same f32
static_assert(to_float<f32>(1.0 / 3.0) == 0.33333334: f32);
u64 bits = reinterpret<u64>(-2.0);
assert(bits >> 63 == 1); // the sign bit
assert(reinterpret<f64>(bits) == -2.0);
static_assert(reinterpret<u32>(1.0: f32) == 0x3f80_0000: u32);
static_assert(max_value<f32>() == 3.4028235e38: f32); // the largest finite f32
}fn to_byte(i32 x) : u8
{
narrow<u8>(x) // checked at run time
}
fn main()
{
to_byte(300); // ✗ diag.narrowing-overflow (dynamic)
}widen is only accepted where every value of the source fits the target, so it can never
lose one: widen<u64> of an i32 is a static error, because a negative i32 has no u64
value. narrow works between any two integer types, and never faults where the value fits.
Use it for usize and isize: whether usize → u64 is a widening depends on the address
width, so narrow<u64>(n) is the spelling that is accepted by every implementation.
fn to_u64(i32 x) : u64
{
widen<u64>(x) // ✗ diag.type-mismatch (static): -1 has no u64 value; use narrow
}
fn main()
{
to_u64(1);
}Sizes and limits of types
Four intrinsics describe a type rather than a value. Each takes the type as its type argument, written explicitly, and no other arguments.
| Intrinsic | Result | Meaning | For |
|---|---|---|---|
sizeof<T>() | usize | the bytes a T occupies, padding included | any type |
alignof<T>() | usize | the alignment a T's address must have, in bytes | any type |
min_value<T>() | T | the smallest value of T (for a float, the most negative finite one) | integer and float types |
max_value<T>() | T | the largest value of T (for a float, the largest finite one) | integer and float types |
Every fixed-width type has the same size and limits on every implementation:
sizeof<i32>() is 4 and max_value<i32>() is 2147483647. isize and usize have the address
width, which each implementation documents, so for them the intrinsics are the portable way to
write a size or limit, where a literal is not. A struct's size and alignment follow from its
fields (§16): each field is aligned, and the struct's alignment is its largest field's.
import std;
struct Pair
{
u8 tag;
u32 value;
}
fn main()
{
static_assert(sizeof<i32>() == 4);
static_assert(sizeof<u64>() == 8);
static_assert(alignof<u64>() == 8); // a number is aligned to its own size
static_assert(sizeof<bool>() == 1);
static_assert(sizeof<array<u16, 10>>() == 20); // an array has no padding
static_assert(alignof<Pair>() == 4); // its largest field's alignment
static_assert(sizeof<Pair>() == 8); // tag, 3 bytes of padding, value
static_assert(sizeof<usize>() == sizeof<isize>()); // the address width, whatever it is
static_assert(min_value<i8>() == -128);
static_assert(max_value<u8>() == 255);
static_assert(max_value<i32>() == 2147483647);
static_assert(min_value<u64>() == 0);
static_assert(max_value<usize>() == ~(0: usize)); // every bit set, at any address width
static_assert(wrapping_add(max_value<i32>(), 1) == min_value<i32>());
}Limits are most often the starting value of a search, a bound to clamp against, or a value that means "none". Here are all three:
import std;
// The spread of a list: start the minimum at the largest i64 and the
// maximum at the smallest, so the first element replaces both.
fn spread(ref<Vec<i64>, shared> v) : i64
{
i64 lo = max_value<i64>();
i64 hi = min_value<i64>();
foreach (p in v)
{
i64 x = *p;
if (x < lo)
{
lo = x;
}
if (x > hi)
{
hi = x;
}
}
hi - lo
}
// Clamp into i32's range, then narrow: the narrow can no longer fault.
fn clamp_to_i32(i64 x) : i32
{
if (x > widen<i64>(max_value<i32>()))
{
max_value<i32>()
}
else if (x < widen<i64>(min_value<i32>()))
{
min_value<i32>()
}
else
{
narrow<i32>(x)
}
}
// The index of the first b, or usize's maximum for "not found".
fn find(ref<Vec<u8>, shared> v, u8 b) : usize
{
foreach (i, x in v)
{
if (*x == b)
{
return i;
}
}
max_value<usize>()
}
fn main()
{
Vec<i64> v = Vec::new();
Vec::push(&mut v, 7);
Vec::push(&mut v, -3);
Vec::push(&mut v, 12);
assert(spread(&v) == 15);
assert(clamp_to_i32(5000000000) == 2147483647);
assert(clamp_to_i32(-5000000000) == -2147483648);
assert(clamp_to_i32(42) == 42);
Vec<u8> bytes = Vec::new();
Vec::push(&mut bytes, 10);
Vec::push(&mut bytes, 20);
assert(find(&bytes, 20) == 1);
assert(find(&bytes, 99) == max_value<usize>());
}A generic function may use the limits of its own type parameter. It cannot do arithmetic
on a T without a bound (§12), but it can produce a T's limit and pass it on. Each use is
checked when the function is instantiated, so a T that is not an integer type is rejected before
the program runs.
import std;
// The value inside, or T's maximum if there is none.
fn or_max<T>(Option<T> r) : T
{
match (r)
{
Some(x) : x,
None : max_value<T>(),
}
}
fn main()
{
u64 big = 5000000000;
assert(or_max(checked_mul(big, big)) == max_value<u64>()); // the product overflows u64
assert(or_max(checked_add(2: i32, 3)) == 5);
}fn main()
{
max_value<bool>(); // ✗ diag.type-mismatch (static): only number types have these
}fn main()
{
max_value<i32>() + 1; // ✗ diag.arith-overflow (dynamic): a limit is a value, not a literal
}- Default arithmetic is checked. If you want wrapping, write
wrapping_add; if you want to handle overflow, writechecked_addand match on theOption. - Never expect a conversion to happen for you. Reach for
widenfirst; usenarrow(checked) ornarrow_wrapping(deliberate truncation) when going the other way. - Use
usizefor lengths and indices,isizefor pointer offsets, and suffix a literal (5: u8) when the context does not already fix its type. - Floats never fault. Remember that
NaNis unequal to itself. - Write a type's limits as
min_value<T>()andmax_value<T>(). Forisizeandusizethey are the only portable spelling.
§07Resources, ownership and destruction
spec/07-resource-authority.mdResource types have exactly one owner, move instead of copying, and are destroyed automatically and exactly once.
Some values are just data: an i32, a bool, a struct of numbers. Copying them is
harmless. Other values own something that must be released exactly once: a heap buffer, a
file handle, a lock, a thread. CobaltC calls the second kind resources, and the
rules in this section exist so that a resource always has exactly one owner, is never copied, and
is destroyed exactly once, at a predictable moment, whether or not you remember to do it.
Plain types and resource types
A type is a resource if it is declared with the resource keyword, or if any of its
fields, elements or payloads is a resource. Vec<T>, String, Rc<T>, a thread handle, a
mutex and a lock guard are all resources. Numbers, bool, references, raw pointers and function
values are plain. A struct of plain fields is plain; a struct containing a Vec is a resource by
derivation, with no keyword needed.
struct Point // plain: two numbers
{
i32 x;
i32 y;
}
struct Path // a resource by derivation: it contains a Vec
{
Vec<Point> points;
}
resource struct Handle // a resource by declaration: it manages something
{ // beyond its fields (an OS handle, say)
i32 fd;
}One owner: moves, not copies
A resource lives in exactly one place. When you initialise a variable from it, pass it by
value to a function, store it in a field, or return it, the resource moves: the new place
becomes the owner and the old name becomes stale. Using a stale name is an error
(diag.stale-binding), caught at compile time when the move is visible in the same function and at
run time otherwise. A name moved on only some paths (in one branch of an if, or in a loop body that
runs again) counts as moved: give it a new value on every path before using it again. Nothing is ever
copied silently, so there can never be two owners.
resource struct Token
{
i32 id;
}
fn consume(Token t) // a by-value resource parameter takes ownership
{
} // t is destroyed here, at the callee's scope exit
fn main()
{
Token t = Token { .id = 1 };
consume(t); // ✓ t moves into consume
consume(t); // ✗ diag.stale-binding: t no longer owns anything
}import std;
fn main()
{
Vec<i32> v = Vec::new();
auto w = v; // moves the Vec into w; v is now stale
Vec::push(&mut w, 1); // ✓ use the new owner
}Because a resource is never copied, it cannot appear where a copy would be made: you
cannot compare two Vecs with ==, and you cannot read one out of a field of a live struct. Take
a reference instead (§09), or move the whole container.
import std;
fn main()
{
Vec<i32> a = Vec::new();
Vec<i32> b = Vec::new();
auto same = a == b; // ✗ diag.read-of-resource: comparison would copy both
}Automatic destruction
Every resource is destroyed automatically: a named resource when the block that declared
it ends, and a temporary (an unnamed call result or literal) at the end of the statement that
created it. Locals are destroyed in reverse order of creation. A struct's resource fields are
destroyed after the struct's own destructor, innermost first, in reverse declaration order. A
return, break, continue or run-time fault unwinds the same way. There is no way to leak a
resource, so there is no leak diagnostic.
You can also destroy a resource early with drop(x). Afterwards x is stale until you give
it a new value: once x's value has gone, moved away or dropped, x = e gives it a new one, which
is destroyed at the end of x's own block like its first value. A value whose end only frees memory
(a String, a Vec, a HashMap, an Option or struct made of such: a *quiet* type, D-0194) is simply
replaced: s = String::from_str("new"); destroys the old String there. Assigning over any other
resource that is still there is an error (diag.overwrite-of-live-resource), because its end does
something you should see: a destructor of yours runs, a file closes, a thread is joined. Write
overwrite(&mut x, e), which destroys the old value and stores the new one, or drop it first. The same
holds for a field (g.file = f;) and for a write through a reference (*r = v;, r.rows = v;): where
the compiler can see the old value is still there, it rejects the assignment before the program runs;
elsewhere, the run does. What counts is whether the value there owns anything: a None of an
Option<File> owns nothing, so o = Some(f); over it is fine, while a Some must be dropped
first. A type declared resource, or with a destructor, always owns. Declaring the name again in
the same block does not: that is diag.duplicate-local (§11).
import std;
fn make() : Vec<i32>
{
Vec<i32> v = Vec::new();
Vec::push(&mut v, 1);
v // moved out to the caller, not destroyed here
}
fn main()
{
{
auto result = make(); // result owns the Vec now
Vec::push(&mut result, 2);
} // destroyed here, at the caller's block exit
make(); // an unstored temporary: destroyed at this `;`
Vec<i32> v = Vec::new();
drop(v); // ✓ destroyed early; v is now stale
v = Vec::new(); // ✓ v's value has gone, so v takes a new one
Vec::push(&mut v, 3);
}Destructors
A type gets a destructor by declaring an associated function named drop that takes an
exclusive reference to the value and returns nothing. The language calls it exactly once, when the
resource is destroyed; you may not call it yourself. Inside it you can mutate the value but not
move out of it. Field resources are destroyed after the body runs. The type must be a resource:
declare it resource struct. A plain struct is copied and never destroyed, so a destructor on one
would never run, and is refused (diag.destructor-on-plain-type).
import std;
resource struct Tagged
{
str tag;
}
fn Tagged::drop(ref<Tagged, exclusive> self)
{
printf("%s", self.tag); // runs once, when this Tagged is destroyed
}
resource struct Holder
{
Tagged a;
Tagged b;
}
fn main()
{
Tagged x = Tagged { .tag = "1" };
Tagged y = Tagged { .tag = "2" };
Holder h = Holder { .a = Tagged { .tag = "3" }, .b = Tagged { .tag = "4" } };
printf("\n");
} // h first (its fields in reverse order: 4 then 3), then y, then x4321
4321.Things the rules forbid
import std;
resource struct Lock
{
u32 id;
}
fn Lock::drop(ref<Lock, exclusive> self)
{
printf("released %u\n", self.id);
}
fn main()
{
Vec<i32> v = Vec::new();
v = Vec::new(); // fine: a Vec of numbers is quiet, the old one is destroyed
Lock l = Lock { .id = 1 };
l = Lock { .id = 2 }; // ✗ diag.overwrite-of-live-resource: lock 1's release would be hidden
}The fix is overwrite(&mut l, Lock { .id = 2 });, which destroys the old Lock there (and
prints its release where you wrote it), or drop(l); first. Two more rejections you will meet:
drop(v) twice is diag.stale-binding (the first drop ended v); and moving a resource out of a
field of a live struct (auto inner = holder.field;) is diag.move-out-of-field, because the struct
would be left half-owned. Move the whole struct, or take a reference to the field.
- If a type owns something, it is a resource: mark it
resourceand give it adrop. Types that merely contain resources become resources automatically. - Passing a resource by value hands it over. To let a callee look without taking, pass
&x; to let it modify, pass&mut x. - Do not fight scope-end destruction. Put a resource in the smallest block that needs it and let the block end release it.
drop(x)is for early release. After it,xis gone until you give it a new value (x = e) or declare the name again.
§08Aliasing: shared and exclusive access
spec/08-alias-validity.mdAny number of readers, or one writer, never both. The rule is checked at every access, so a conflict cannot go unnoticed.
Two pieces of code touching one object at the same time is the root of most memory bugs: an iterator invalidated by a push, a struct half-updated by two threads, a buffer freed while something still points into it. CobaltC's answer is a discipline on access paths: every way of reaching an object (its name, a reference, a field projection) has a mode, and the modes that may coexist are fixed.
Two modes
| Mode | Written | May | Coexists with |
|---|---|---|---|
shared | &x, type ref<T, shared> | read | any number of other shared paths |
exclusive | &mut x, type ref<T, exclusive> | read and write; and, for the owner, move or destroy | nothing else that overlaps |
A variable's own name is always an exclusive path: there is no mut keyword on bindings,
because whether you may write is decided by what references currently exist, not by how the variable
was declared. Restricting access is what a borrow does.
The one rule
At every read and every write, the language checks: is there another live path to
the same storage whose mode conflicts with this access? Shared and shared never conflict.
Anything involving an exclusive path conflicts, unless the other path is an ancestor of
this one (the thing it was borrowed from). Forming a conflicting borrow is reported at once as
diag.aliasing-conflict; so is using the owner while a conflicting borrow lives.
fn main()
{
i32 x = 1;
auto r1 = &x; // ✓ shared
auto r2 = &x; // ✓ any number of shared readers; the owner may still read x
x = 2; // ✗ diag.aliasing-conflict: writing while r1, r2 are live
i32 sum = *r1 + *r2;
}fn main()
{
i32 x = 1;
auto r1 = &mut x; // ✓ exclusive
auto r2 = &x; // ✗ diag.aliasing-conflict: r1 is exclusive and still live
*r1 = 2;
}Borrows in one argument list are all formed before the call holds any of them, so passing two conflicting references to the same storage is accepted at the call. The conflict is reported at the first access inside the function that meets both, the point where it actually matters.
fn set_then_read(ref<i32, exclusive> a, ref<i32, shared> b) : i32
{
*a = 1; // ✗ diag.aliasing-conflict (dynamic): b reaches the same i32
*b
}
fn main()
{
i32 x = 0;
set_then_read(&mut x, &x); // accepted here; each borrow formed while the other was not yet held
}Most conflicts are reported before the program runs, marked (static): whenever the
function body itself shows that a conflicting reference is certainly live. That covers a borrow held
in a variable, a reborrow through a reference or a guard (&*r, &r.f), the binder of a
match (&e) arm, and the arguments a thread was spawned with, until join. What one body cannot
see, like the call above, is reported when the conflicting access runs, marked (dynamic).
struct Counter
{
i32 hits;
i32 misses;
}
fn bump(ref<Counter, exclusive> c)
{
auto n = &mut c.hits; // a reborrow of c.hits, through c
c.misses = 0; // ✓ a different field
c.hits = 0; // ✗ diag.aliasing-conflict (static): n still borrows c.hits
*n += 1;
}
fn main()
{
Counter k = Counter { .hits = 0, .misses = 0 };
bump(&mut k);
}When does a borrow end?
A reference lives as long as something holds it. A reference passed straight to a call
dies when the statement ends, which is why consecutive calls that each borrow the same variable are
fine. A reference, slice or StringView kept in a local variable lives until the last statement
of its block that uses it, and ends there. So does any other local that holds references but
owns nothing to destroy: the Option<ref<V, shared>> that HashMap::get returns, or a struct of
references. Using it inside a loop, a branch or a nested block
counts as a use by that whole statement, so a reference used in a loop lives through the loop.
A reference variable lives to the end of its block, as before, when a closure captures it, when
another reference was borrowed through it, or when a variable that can hold references (a
Vec<StringView>, say) shares a statement with it and is used later, since that variable may
keep a reference derived from it.
import std;
fn main()
{
String cur = "abc";
ref<Vec<u8>, shared> b = String::as_bytes(&cur);
assert(Vec::len(b) == 3); // the last use of b: it ends after this statement
String::clear(&mut cur); // ✓ nothing borrows cur any more
assert(String::len(&cur) == 0);
HashMap<String, u32> m = HashMap::new();
HashMap::insert(&mut m, String::from_str("a"), 1);
Option<ref<u32, shared>> hit = HashMap::get(&m, &String::from_str("a"));
assert(Option::is_some(&hit)); // the last use of hit
HashMap::insert(&mut m, String::from_str("b"), 2); // ✓ hit no longer borrows m
}import std;
fn main()
{
Vec<i32> v = Vec::new();
Vec::push(&mut v, 1);
ref<i32, shared> first = &v[0];
foreach (k in 0..2: i32)
{
printf("%v\n", *first); // used in the loop: first lives through the whole loop
Vec::push(&mut v, k); // ✗ diag.aliasing-conflict: first still borrows v
}
}A block ends every borrow it holds, so wrapping a borrow in a block still works, and reads well when a borrow should visibly end.
import std;
fn main()
{
Vec<i32> nums = Vec::new();
Vec::push(&mut nums, 10); // the borrow dies at this `;`
Vec::push(&mut nums, 20); // ✓ a fresh borrow; nothing conflicts
i32 x = 1;
{
auto r = &mut x; // r lives until the end of this block
*r = 5;
}
x += 1; // ✓ r is gone
assert(x == 6);
}Reborrowing and ancestors
A path derived from another (a reborrow &*r, or a field projection r.a) has that path
as its ancestor. Ancestors never conflict with their descendants, but the ancestor cannot be used in
a conflicting way while a descendant lives. Once the descendants are gone, the ancestor is usable
again.
import std;
fn main()
{
i32 x = 1;
{
auto r1 = &mut x;
{
auto r2 = &*r1; // ✓ a shared reborrow, child of r1
assert(*r2 == 1); // writing through r1 here would be diag.aliasing-conflict
}
*r1 = 3; // ✓ r2 is gone, the parent is usable again
}
assert(x == 3); // ✓ r1 is gone, the owner is usable again
}Disjoint fields
Conflicts are decided by overlap of storage, not by which variable the paths come from. Two exclusive borrows of different fields of the same struct do not overlap and may coexist.
import std;
struct Pair
{
i32 a;
i32 b;
}
fn main()
{
Pair p = Pair { .a = 1, .b = 2 };
auto ra = &mut p.a;
auto rb = &mut p.b; // ✓ disjoint from p.a
p.b = 30; // ✗ diag.aliasing-conflict: rb is a live exclusive path to p.b
*ra = 10;
*rb = 20;
}The write to p.b above is rejected because rb, a live exclusive path to exactly that
storage, still exists until the block ends. The same applies to reading p.b through the owner: an
exclusive child excludes every non-ancestor access, reads included, until it is gone. Confine the
borrows to a block and the owner is free again:
import std;
struct Pair
{
i32 a;
i32 b;
}
fn main()
{
Pair p = Pair { .a = 1, .b = 2 };
{
auto ra = &mut p.a;
auto rb = &mut p.b;
*ra = 10;
*rb = 20; // reading p.a or p.b here would conflict with the live exclusive borrows
}
assert(p.a == 10); // ✓ both borrows ended with the block
assert(p.b == 20);
}Shared means read-only, all the way down
An exclusive path can only be derived from an exclusive path. Given a ref<T, shared>
you cannot write through it and cannot reborrow it exclusively: the mode is part of the reference's
type, so a function's signature tells the caller exactly what it may do. The other direction is
always allowed: an exclusive reference can be passed to a function that asks for a shared one, and
the function gets a shared reborrow of it (as if you had written &*r). With v : ref<Vec<T>,
exclusive>, Vec::len(v) just works; likewise a slice<T, exclusive> for a slice<T, shared>.
struct P
{
i32 a;
}
fn bump(ref<P, shared> p)
{
p.a = 1; // ✗ diag.write-through-shared
}
fn main()
{
P v = P { .a = 0 };
bump(&v);
}All the way down includes the references a value holds. Read through a ref<S, shared>, an
exclusive reference stored in S comes out as a shared one: you can read through it, keep it, pass
it or return it as a ref<T, shared>, but not write through it. A copy of a value that holds an
exclusive reference cannot be taken out through a shared reference either, and clone cannot copy
one (diag.borrow-exceeds-source). A fn field is checked when it is called: a closure that writes a
variable it captured cannot be called through a shared reference to the struct that holds it
(diag.borrow-exceeds-source, at run time, since a fn type does not show what the closure
captured). To change something through a value others share, put it in a mutex (§19).
import std;
struct Counter
{
ref<i32, exclusive> total;
}
fn peek(ref<Counter, shared> c) : i32
{
*c.total // ✓ reading is fine
}
fn bump(ref<Counter, shared> c)
{
*c.total = *c.total + 1; // ✗ diag.write-through-shared
}
fn main()
{
i32 n = 0;
Counter c = Counter { .total = &mut n };
printf("%v\n", peek(&c));
bump(&c);
}Moving or destroying needs solitude
Moving a resource, or destroying it (with drop or at scope end), requires that no other
path to it exists at all. Otherwise a reference would be left pointing at nothing.
import std;
fn main()
{
Vec<i32> v = Vec::new();
auto r = &v;
drop(v); // ✗ diag.destroy-while-aliased: r is still live
usize n = Vec::len(r); // (because r is used here)
}import std;
fn take(Vec<i32> v)
{
}
fn main()
{
Vec<i32> v = Vec::new();
auto r = &v;
take(v); // ✗ diag.move-while-aliased: r still reaches v
usize n = Vec::len(r); // (because r is used here)
}- Borrow shared (
&x) by default; borrow exclusively (&mut x) only for the duration of the change. - Keep references short-lived. Passing
&mut vdirectly as an argument creates a borrow that ends at the semicolon. - If the checker complains about a conflict, end the earlier borrow: put it in its own block, or reorder the code so the write happens after the last use of the reference.
- Field-level borrows are precise: borrowing
p.aexclusively leavesp.bfree.
§09References
spec/09-identity-origin-extent.mdReferences are copyable values that name a place; dereferencing gives you that place back with every check intact.
A reference is the value form of an access path: a way to reach an object without owning it. References are plain values (copyable, no destroy authority) and carry their mode in their type. Raw pointers are a different thing entirely and live in §20.
Forming and using references
| Syntax | Meaning |
|---|---|
&e | a ref<T, shared> to the place e |
&mut e | a ref<T, exclusive> to the place e |
*r | the place r refers to: read it in an expression, assign to it, borrow it again, or project a field or element |
r.f, r[i] | field access and indexing look through a reference automatically |
r1 == r2 | identity: true only if both refer to the same storage of the same object |
The operand of & must be a place: a variable, a field, an element, or *r.
You cannot borrow a temporary such as a call result or a literal (&make()); bind it to a variable
first. This is rejected rather than given a lifetime because the temporary would end at the
semicolon and take the reference with it.
import std;
struct Pair
{
i32 a;
i32 b;
}
fn main()
{
i32 x = 5;
{
auto r = &mut x;
*r += 1; // read and write through the same reference
}
assert(x == 6);
Pair p = Pair { .a = 1, .b = 2 };
auto rp = &p;
assert(rp.a == 1); // auto-deref: rp.a means (*rp).a
auto ra = &p.a;
auto rb = &p.b;
assert(!(ra == rb)); // different fields, so different identities
auto ra2 = &p.a;
assert(ra == ra2); // same field of the same object
ref<i32, shared> copy = ra; // references are plain values: copying is fine
assert(*copy == 1);
}fn make() : i32
{
5
}
fn main()
{
auto r = &make(); // ✗ diag.borrow-of-non-place: a call result is not a place
}The one place a temporary may be borrowed is as an argument: f(&make()) or
printf("%v", &sprintf(…)). The value lives until the end of the statement, and the call is over
before then. This works when the call's own result is not a reference, since such a result could
carry the borrow out. An operator's result, the value ? gives, and a number or bool literal
may be borrowed there too: HashMap::get(&m, &(k + 1)), Vec::len(&names()?),
HashMap::get(&m, &3). A literal takes the type the parameter refers to, so &3 passed where a
ref<u64, shared> is expected is a reference to a u64.
import std;
fn total(ref<Vec<i32>, shared> v) : i32
{
i32 t = 0;
foreach (x in v)
{
t += *x;
}
t
}
fn make(i32 n) : Vec<i32>
{
Vec<i32> v = Vec::new();
for (i32 i = 1; i <= n; i += 1)
{
Vec::push(&mut v, i);
}
v
}
fn main()
{
assert(total(&make(4)) == 10); // the Vec lives until the end of this statement
printf("%v\n", &sprintf("%v-%v", 1, 2));
HashMap<u64, str> names = HashMap::new();
HashMap::insert(&mut names, 3, "three");
u64 k = 2;
assert(HashMap::contains(&names, &(k + 1))); // an operator's result
printf("%s\n", *Option::unwrap(HashMap::get(&names, &3))); // a literal, as a u64
}Part of a temporary may be borrowed there too: a field (String::len(&person().name)),
an element, or a slice of an array literal or a call's result (sum(&[1, 2, 3][0..2])). A String
that a call returns may be viewed the same way, so a computed path needs no binding:
read_file(&sprintf("%s/%s", dir, name)[0..$]). It lives as
long as the temporary does, to the end of the statement. A field can also be moved out of a
temporary, String n = person().name;: the rest of the temporary ends at once, as if the struct had
been destructured. That needs a struct without a destructor of its own.
import std;
struct Person
{
String name;
String city;
}
fn person() : Person
{
Person { .name = "Ada", .city = "London" }
}
fn sum(slice<i32, shared> xs) : i32
{
i32 t = 0;
foreach (x in xs)
{
t += *x;
}
t
}
fn main()
{
assert(String::len(&person().name) == 3); // a field of a temporary, borrowed
assert(sum(&[1, 2, 3][0..2]) == 3); // a slice of an array literal
assert(StringView::len(&person().city[0..3]) == 3); // a view of a temporary's String
String n = person().name; // moved out; `city` ends here
assert(String::eq_str(&n, "Ada"));
}References inside structs
A reference may be stored in a field. It then lives as long as the struct that holds it, which in turn may not outlive the referent (§10).
import std;
struct View
{
ref<i32, shared> r;
}
fn main()
{
i32 x = 1;
auto v = View { .r = &x }; // the borrow is held by v's object
assert(*v.r == 1); // ✓ valid as long as v lives
}References as parameters
Passing &x or &mut x to a function creates a borrow that the parameter holds for the
duration of the call and releases when the call returns. Inside the function the parameter is an
ordinary reference. A parameter of type ref<T, exclusive> can be passed on to another call
directly, without a new &mut, because it already is an exclusive reference.
import std;
fn bump(ref<i32, exclusive> counter) : i32
{
*counter += 1;
*counter
}
fn bump_twice(ref<i32, exclusive> c) : i32
{
bump(c); // pass the reference on as a value
bump(c)
}
fn main()
{
i32 n = 0;
assert(bump(&mut n) == 1); // the borrow lives for the call only
assert(bump_twice(&mut n) == 3);
assert(n == 3);
}- Use
&xto lend,&mut xto lend for writing. The callee's signature tells you which it needs. *ris a place, so*r = v,&*r, and(*r).fall work; for fields,r.fis the same as(*r).f.- Bind a call result before keeping a borrow of it; borrowing one just for a call,
f(&make()), is fine. ==on references asks whether they point at the same thing, not whether the values are equal; compare*a == *bfor values.
§10Lifetimes and temporal validity
spec/10-temporal-validity.mdA reference is checked statically where the escape is visible and dynamically everywhere else; either way it never outlives its target.
A reference must never outlive the object it refers to. CobaltC enforces this with two
layers: a lexical check that rejects the obvious escapes before the program runs, and a use-time
check that catches everything else as diag.stale-binding at the first use of a dead reference. No
lifetime annotations exist; the language decides validity from where things are declared.
A reference cannot leave its referent's scope
If a reference to a local is returned from the function, or stored into a variable
declared in an enclosing block, it is rejected statically as diag.reference-escapes-scope.
fn dangling() : ref<i32, shared>
{
i32 x = 1;
&x // ✗ diag.reference-escapes-scope: x dies with this call
}
fn main()
{
auto r = dangling();
*r;
}The corrected form returns the value itself: fn ok() : i32 { i32 x = 1; x }.
When the escape is not lexically visible (the reference is stored through another reference, into an aggregate the checker cannot see the owner of, or through a call), the static rule does not fire and the run-time check takes over. The program is still rejected, just later.
struct Holder
{
ref<i32, shared> r;
}
fn stash(ref<Holder, exclusive> h)
{
i32 local = 5;
h.r = &local; // a store through a reference: not lexically visible as an escape
} // local ends here; whatever h points at now holds a dead reference
fn main()
{
i32 outer = 0;
auto held = Holder { .r = &outer };
stash(&mut held);
*held.r; // ✗ diag.stale-binding (dynamic): first use of the dead reference
}Returning references from functions
A function may return a reference when it has exactly one reference-typed
parameter: the result is understood to point into whatever that argument pointed at, and callers are
checked accordingly. This is the shape of every library accessor (Vec::index_shared,
Rc::get). A function with zero or several reference parameters may not return a reference
(diag.lifetime-elision-ambiguous); compose single-reference helpers instead, or return an owned
value.
import std;
struct Pair
{
i32 a;
i32 b;
}
fn first(ref<Pair, shared> p) : ref<i32, shared>
{
&p.a // ✓ one reference parameter: the result borrows from it
}
fn main()
{
Pair pr = Pair { .a = 1, .b = 2 };
auto y = first(&pr); // y is a shared borrow of pr.a, held by y
assert(*y == 1);
}fn pick(ref<i32, shared> p1, ref<i32, shared> p2) : ref<i32, shared>
{
p1 // ✗ diag.lifetime-elision-ambiguous: which argument does the
} // result depend on? The language will not guess.Because y above is known to borrow from pr, a later pr.a = 3; while y is still in
use is an aliasing conflict (§08), exactly as if you had written auto y = &pr.a;.
Growth invalidates element references
A Vec may move its elements to a new buffer when it grows. A reference to an element
obtained before a push that reallocates is stale afterwards. Where the checker can see both the
borrow and the push in one function, it rejects the program; where it cannot (the push happens
through a reference parameter, say), the stale reference faults at its next use. Either way it does
not read freed memory.
import std;
fn main()
{
Vec<i32> v = Vec::new();
Vec::push(&mut v, 10);
auto first = Vec::index_shared(&v, 0); // a shared borrow into v, held by first
Vec::push(&mut v, 20); // ✗ diag.aliasing-conflict: an exclusive borrow of v
*first; // while first (derived from v) is still live
}- Return values, not references, unless the function has exactly one reference parameter and the result points into it.
- Do not store a reference in a variable that outlives the block where the referent was declared.
- Re-fetch element references after any call that may grow or shrink a
Vec. - If you see
diag.stale-binding, ask what ended the referent: a scope exit, a move, adrop, or a reallocation.
§11Initialisation
spec/11-initialization.mdEvery variable is written before it is read, on every path, and every aggregate is built whole; constants name values known before the program runs.
Reading storage that has never been written is undefined behaviour in C. In CobaltC it is impossible in a well-formed program: a variable declared without an initialiser must be assigned on every path before it is read, and the check is static. Aggregates are always initialised whole.
Declarations
| Form | Meaning |
|---|---|
T x = e; | declare x of type T, initialised from e (which is checked against T) |
auto x = e; | declare x with the type of e |
T x; | declare x uninitialised; the type is required because there is nothing to infer from |
Name { f1, f2 } = e; | destructure a struct: bind each field to its name and consume the struct |
Initialising from a plain value copies it; from a resource, moves it; from a temporary such as a struct literal or a call result, adopts it.
One name, one declaration per block
A block declares a name once. A second i32 total further down the same block is
diag.duplicate-local, which says where the first one is: to change the variable, assign to it
(total = 10;); for a value of another type, give it another name. A function's parameters count
as declared in its body, and so do the names a foreach, a for header, a match arm or an
if-pattern binds for the block they govern, so fn f(i32 n) { i32 n = n * 2; } is rejected
too: write n = n * 2;.
A block inside it may reuse the name: the inner variable shadows the outer one
until the inner block ends, and the outer one is untouched. Separate blocks, successive loops and
separate match arms each have their own names, and a local may take the name of a function or a
constant (a program's own names never clash with names added to std later). Languages differ
here: Rust lets any declaration shadow, Java and C# forbid shadowing a local even from an inner
block; CobaltC follows C and C++. Every local can already be assigned, so a second declaration in
the same block would only hide a mistake.
import std;
fn main()
{
i32 total = 0;
total += 4; // ✓ change it by assigning
{
i32 total = 100; // ✓ an inner block's own total
assert(total == 100);
}
assert(total == 4); // the outer one is untouched
i32 total = 10; // ✗ diag.duplicate-local: declared above, in this block
}Constants
const τ NAME = e; names a value, at the top level of any module or inside a function (below). NAME can be used
wherever its value could, with the visibility, export, import and qualified paths of any item.
e must be a constant expression: literals, other constants, operators, struct, array and
variant literals of constant expressions and their fields and elements (SCREEN.w, PRIMES[2]),
a function's name, and the pure intrinsics (min_value, sizeof, widen, …). A constant cannot be a resource, cannot depend on itself, and cannot be
assigned to or borrowed; an overflow in computing one is reported at the constant, before the
program runs. Integer, float, bool and str constants cost nothing at run time: each use becomes
the literal.
import std;
const u64 KIB = 1024;
const u64 MIB = KIB * KIB;
const u32 READ = 0x1;
const u32 WRITE = 0x2;
const u32 READ_WRITE = READ | WRITE;
const i32 LOWEST = -2147483648;
struct Size
{
u32 w;
u32 h;
}
const Size SCREEN = Size { .w = 640, .h = 480 };
fn main()
{
static_assert(MIB == 1048576);
static_assert(READ_WRITE == 3);
static_assert(LOWEST < 0);
static_assert(SCREEN.w * SCREEN.h == 307200); // a field of a constant is a constant
}A constant is typed exactly as a declaration τ x = e; would be: in
const u64 MIB = 1024 * 1024; the literals are u64s, and an initializer that overflows its type,
such as const i32 BIG = 65536 * 65536;, is rejected at the constant. const auto NAME = e; takes
its type from e, as auto x = e; does: const auto MIB = KIB * KIB; is a u64 when KIB is, and
a bare literal is an i32 (or an f64). When e alone does not settle a type — None, say —
write the type.
A constant can also be declared inside a function, as a statement: const u8 LIMIT = 200;. It is
the same kind of constant, but its name is scoped like a local: it can be used from its
declaration to the end of its block, and an inner block can declare another constant or a variable
of the same name, which hides it there (in its own block, a second declaration of the name is
diag.duplicate-local, above). A local variable, even one set from a literal, is
not a constant — any later statement could change it — so static_assert cannot use it; name the
value with const when you want the check made before the program runs. A local constant's
initializer can use other constants, but not a local variable or the function's type
parameters.
import std;
fn checksum(slice<u8, shared> data) : u32
{
const u32 PRIME = 16777619; // FNV-1a: the 32-bit prime and offset basis
const u32 SEED = 2166136261;
static_assert(PRIME % 2 == 1);
u32 h = SEED;
foreach (b in data)
{
h = wrapping_mul(h ^ widen<u32>(*b), PRIME);
}
h
}
fn main()
{
u8 b = 200;
auto d = b + 1; // a variable: checked when the program runs
assert(d == 201);
const u8 B = 200;
const auto D = B + 1; // constants: checked before it runs (auto: D is a u8)
static_assert(D == 201, "D is %v", D);
array<u8, 3> abc = [97, 98, 99];
assert(checksum(&abc[0..$]) == 440920331);
}const means known when the program is compiled
A const can never change: assigning to it, or borrowing it with &mut, is rejected when
the program is compiled. A constant is not a variable at all, but stands for its value,
so assigning to it is like assigning to a literal. Copying its value out is fine: i32 y = LIMIT;
gives a variable y, which can change, while LIMIT stays as it is.
import std;
const i32 LIMIT = 10;
fn main()
{
i32 y = LIMIT; // a copy of the value: y is an ordinary variable
y += 1;
LIMIT = 11; // ✗ diag.type-mismatch: a constant cannot be assigned
}What makes it a constant is when its value is known: before the program runs, like
const in Rust, Go and C# or constexpr in C++. That differs from C, C++, Java or JavaScript, where
const (or final) often marks a variable that is set once, at run time, and never reassigned.
const usize N = arg_count(); is diag.const-not-constant, because the value is only known once
the program runs.
CobaltC has no read-only local variables either: every local can be assigned. It does not need
them for safety, because every change to a variable shows where it happens. Only two things can
change a local: an assignment to its name in its own function, or an exclusive reference to it,
&mut x, which must be written out where it is made. A function given &x or a copy of x cannot
change it; while a shared reference to it is in use, nothing can (diag.aliasing-conflict, §8); and
threads share changing state only through a mutex. So to see whether x can change, look in its
function for x = and &mut x. To lend a value without letting it change, lend it with &.
import std;
fn peek(ref<i32, shared> r) : i32
{
*r // may read; writing *r is diag.write-through-shared
}
fn bump(ref<i32, exclusive> r)
{
*r += 1;
}
fn main()
{
i32 limit = 10; // a variable: it can be assigned, as every local can
assert(peek(&limit) == 10); // &limit: the callee can only read it
bump(&mut limit); // the one way another function changes it, visible here
assert(limit == 11);
ref<i32, shared> view = &limit;
assert(*view == 11); // while view is in use, nothing may write limit
limit = 12; // ✓ view's last use is above, so it no longer holds limit
assert(limit == 12);
}Definite assignment
import std;
fn main()
{
i32 x; // uninitialised
bool c = true;
if (c)
{
x = 1;
}
else
{
x = 2;
}
assert(x == 1); // ✓ assigned on both paths
}fn main()
{
i32 x;
bool c = false;
if (c)
{
x = 1;
}
i32 y = x + 1; // ✗ diag.use-of-uninitialized: no write on the else path
}The analysis is purely about the function's own control flow: it does not know that c
happens to be true. If it cannot prove a write on some path, the program is rejected (this is one
of the few checks where "unknown" means rejected rather than "check at run time").
Aggregates are initialised whole
You cannot declare a struct uninitialised and then fill its fields one by one. A struct literal supplies every field at once; after that, individual fields can be assigned freely.
import std;
struct Pair
{
i32 a;
i32 b;
}
fn main()
{
Pair p = Pair { .a = 1, .b = 2 }; // every field, exactly once, in any order
p.a = 10; // ✓ a field write on an initialised object
assert(p.a == 10);
assert(p.b == 2);
}Destructuring a struct
import std;
struct Entry
{
String item;
i64 change;
}
fn main()
{
Entry e = Entry { .item = "pens", .change = -3 };
Entry { item, change } = e; // binds `item` and `change`; e is consumed
assert(String::len(&item) == 4 && change == -3);
}This is the only pattern form outside match and the pattern conditions of if and while (§14). It names fields of the struct,
each once, in any order, and consumes the struct: a resource field moves out to its new binding, a
plain one is copied. It is how a struct gives up a resource it holds (the library's
String::into_bytes uses it to hand back the Vec<u8>), since a resource field cannot otherwise
be moved out of a struct that lives on. field : name binds a field to another name, so two values
of one type can be taken apart side by side, and a final .. stands for the fields not named:
they are not bound, and those holding resources are destroyed when the block ends. Naming a field
twice, or leaving one out without .., is a type error, and a struct with its own destructor
cannot be destructured, since the destructor would then run on a struct whose fields had left it
(diag.move-out-of-field).
import std;
struct Entry
{
String item;
i64 change;
String note;
}
fn main()
{
Entry a = Entry { .item = "pens", .change = -3, .note = String::new() };
Entry b = Entry { .item = "ink", .change = 5, .note = String::new() };
Entry { item : first, change : da, .. } = a; // renamed; `note` is left and ends with the block
Entry { item : second, change : db, .. } = b;
assert(String::len(&first) + String::len(&second) == 7 && da + db == 2);
}- Prefer
T x = e;orauto x = e;. Declare uninitialised only when the value genuinely comes from a branch, and then assign in every branch. - Build structs with a literal that names every field. Assign fields afterwards if needed.
autoneeds an initialiser it can learn a type from;auto v = Vec::new();is rejected becauseTis unknown (§12).
§12The type system
spec/12-type-system.mdNominal types, no coercions, monomorphic generics (one copy per use with concrete types) with five built-in bounds, and local inference that only flows downward.
CobaltC's type system is deliberately plain: every type is exactly what it is declared to be, nothing converts to anything else, and generics are templates instantiated with concrete types. What it gives you in return is that the type of every expression is decidable from the text, and that a function signature is a complete statement of what the function may do with its arguments.
The built-in types
| Type | Values | Resource? |
|---|---|---|
integers, f32, f64, bool | numbers and truth values (§06) | no |
str | text: a valid UTF-8 byte sequence, written "…"; a whole value like an integer, not a reference and not owned (§21) | no |
void (type) / () (value) | the unit type; the type of assignments, of if without else, of loops and of functions with no return type. Written void in type position and () as a value. | no |
ref<T, shared>, ref<T, exclusive> | references (§09) | no |
rawptr<T> | raw addresses (§20) | no |
array<T, N> | N values of T in a row, N a constant expression (§16) | if T is |
slice<T, shared>, slice<T, exclusive> | a borrowed run of elements of an array or Vec (§16) | no |
fn(T1, T2) : R | function values | no |
handle<T>, mutex<T>, guard<T> | threads and locks (§19) | yes |
struct, enum | user-declared aggregates (§16) | if marked resource or any part is |
bitstruct | named runs of bits packed into one unsigned integer, u8 to u128 (§16) | no |
never | the type of return, break and continue: an expression that never yields a value, accepted wherever any type is expected | n/a |
Nominal, exact, no coercions
Two types are the same only if they have the same name and the same type arguments.
ref<i32, shared> and ref<i32, exclusive> are different types. There is no subtyping and no
implicit conversion anywhere: not between integer widths, not from T to ref<T, m>, not from
Option<T> to T. This applies to arguments too. A literal is not an exception but a different
thing: it has no type of its own until its position gives it one, so u64 x = 5;, StringView v =
"x"; and String s = "x"; type the literal (§21) rather than convert a value, and the same
positions still reject an i32, a str or any other typed value.
fn takes_i64(i64 x) : i64
{
x
}
fn main()
{
i32 y = 5;
takes_i64(y); // ✗ diag.type-mismatch: i32 is not i64; write takes_i64(widen<i64>(y))
}A bare integer literal is the one flexible thing: takes_i64(5) is fine, because the
literal takes its type from the parameter (§06).
Equality
== and != are defined for numbers, bool, (), str (bytewise), references (identity), raw pointers
(address), and for structs, arrays and enums whose parts are all comparable (element by element;
enums must have the same variant and equal payloads). They are not defined for function values or
for anything that is a resource, with one exception: two Strings compare by their text.
< <= > >= order numbers, bool (false before true), and text: two strs, two
Strings or two StringViews, by their bytes, a prefix first (for UTF-8 that is code point
order). A String operand is not copied or moved: it is borrowed for the comparison, as &s would
be, so it stays usable afterwards.
import std;
fn main()
{
String a = "app";
String b = "apple";
assert(a != b && a < b); // text order: a prefix first
assert("b" > "abc");
assert(false < true);
String c = a; // a was only borrowed by the comparisons; it can still move
assert(c == String::from_str("app"));
}Generics
Functions, structs and enums can take type parameters. Generics are monomorphic:
each use with a different set of concrete types is a separate copy of the declaration, compiled
(or, in the interpreter, run) as if you had written it out with those types: identity(5) and
identity(2.5) below call two functions, one for i32 and one for f64. So there is no run-time type
information, no boxing and no indirect call at call sites; the cost is one copy per combination
of types used. Inside a generic body a value of a type parameter T can be stored, passed, returned,
referenced and moved. To do more with it — compare it, add it — give T a bound (below).
import std;
struct Box<T>
{
T value;
}
enum Maybe<T>
{
Just(T),
Nothing
}
fn identity<T>(T x) : T
{
x // all a bare T can do: be moved around
}
fn boxed<T>(T x) : Box<T>
{
Box { .value = x } // the field type is T, so this is fine
}
fn main()
{
assert(identity(5) == 5); // T := i32, from the argument
assert(identity<i64>(5) == 5); // T given explicitly: name<Type>(...)
Box<i32> b = Box { .value = 7 }; // Box's T from the declared type
assert(b.value == 7);
Box<Vec<u8>> c = boxed(Vec::new()); // both T's from the declared type of c
assert(Vec::len(&c.value) == 0);
}Bounds
A function's type parameter may carry one of five bounds, written after a colon:
fn max<T: ordered>(T a, T b) : T. Each of the first four names a fixed set of built-in types and
the operators every one of them has, and each includes the ones before it; the fifth, clone, says
a value of T can be copied (below):
| Bound | Satisfied by | Allows on a T |
|---|---|---|
eq | integers, floats, bool, str, String | == != |
ordered | integers, floats, bool, str, String | also < <= > >= |
number | integers, floats | also + - * / %, unary -, += and the like; integer literals |
integer | integers | also & | ^ ~, << >> |
clone | everything above; String, Rc<T>, Box<T: clone>, Vec<T: clone>, the hash tables; a type with its own T::clone; a struct or enum whose fields are all clone (D-0116) | clone(&x): an independent copy |
clone alone — by
its fields, or by a fn T::clone(ref<T, shared> self) : T you declare, recognised by its name as
T::drop is — and never the four operator bounds: to compare or combine values of your own types in
generic code, pass the operation in as a function value, as Vec::sort_by takes less.import std;
fn largest<T: ordered>(slice<T, shared> xs) : T
{
T best = xs[0];
foreach (x in xs)
{
if (*x > best) { best = *x; }
}
best
}
fn total<T: number>(slice<T, shared> xs) : T
{
T sum = 0; // a literal takes T's type, a float one too
foreach (x in xs)
{
sum += *x;
}
sum
}
fn main()
{
array<i32, 3> a = [4, -2, 9];
array<f64, 2> f = [1.5, 2.25];
assert(largest(&a[0..$]) == 9);
assert(total(&a[0..$]) == 11);
assert(total(&f[0..$]) == 3.75);
}The body is checked once against the bound: a + b in a function whose T is only
ordered is diag.unbounded-type-parameter. Each call is checked for a type that satisfies it:
largest of bools, or of a String, is diag.type-mismatch. Each use is also checked at its
concrete type, so -x at T = u32 is still rejected there. Structs and enums take no bounds;
put them on the functions that use the operations.
clone(&x) copies a value of any clone type: a plain one by reading it, String and the
containers by their own clone, and a struct or enum of clone fields by a copy the compiler
derives, field by field. A resource type — one with a destructor — declares its own T::clone or
has none, because only it knows how to copy what it manages. Generic code says what it needs with
the bound:
import std;
struct Person
{
String name; // Person is clone: every field is
u32 age;
}
enum Shape
{
Dot,
Labelled(String), // Shape is clone too
}
resource struct Ticket
{
u32 id;
}
fn Ticket::drop(ref<Ticket, exclusive> self)
{
}
fn Ticket::clone(ref<Ticket, shared> self) : Ticket // a resource says how it is copied
{
Ticket { .id = self.id + 1000 }
}
fn pair_up<T: clone>(ref<T, shared> x) : Vec<T> // needs T: clone to call clone(x)
{
Vec<T> v = Vec::new();
Vec::push(&mut v, clone(x));
Vec::push(&mut v, clone(x));
v
}
fn main()
{
Person a = Person { .name = "Ada", .age = 36 };
Person b = clone(&a); // derived: name copied, age copied
String::push_ascii(&mut b.name, b'!');
assert(String::eq_str(&a.name, "Ada") && String::eq_str(&b.name, "Ada!"));
Shape s = Labelled(String::from_str("x"));
Shape s2 = clone(&s);
Vec<Person> people = pair_up(&a);
Vec<String> names = Vec::from_fn(2, [](usize i) : String { sprintf("n%d", i) });
Vec<String> copy = Vec::clone(&names); // Vec::clone needs T: clone; String is
Ticket t = Ticket { .id = 1 };
Vec<Ticket> tickets = Vec::filled(2, clone(&t)); // Ticket::clone, three times
assert(Vec::len(&people) == 2 && Vec::len(©) == 2 && tickets[1].id == 2001);
}Inference is local and downward. A type parameter is found from, in order: an
explicit type argument (identity<i64>(5)), the type of an argument in the same call, or the
expected type where the call appears (a declared variable type, a parameter type, a field
type). An unsuffixed literal argument comes last: it takes the type the other arguments or the
expected type give its parameter, so with i64 x, pick(true, 0, x) makes T an i64 and the 0
with it; only when nothing else fixes T does a literal's own default (i32, f64) decide.
Nothing is ever inferred from how a value is used later, so a call that fixes nothing is
rejected:
import std;
fn main()
{
auto v = Vec::new(); // ✗ diag.cannot-infer-type-parameter: nothing says what T is
Vec::push(&mut v, 1); // (a later use does not count)
}The same holds when the call is an argument to another generic call. Mutex::new<T> is
one: with auto nothing reaches it, so nothing reaches the Vec::new() inside it either. Give the
outer binding its type and both are fixed.
import std;
fn main()
{
mutex<Vec<u8>> m = Mutex::new(Vec::new()); // T of Mutex::new, then T of Vec::new, from the declaration
mutex<i64> big = Mutex::new(5000000000); // and the literal is an i64 for the same reason
assert(Vec::len(&*lock(&m)) == 0);
}import std;
fn main()
{
auto m = Mutex::new(Vec::new()); // ✗ diag.cannot-infer-type-parameter: nothing fixes Vec's T
}import std;
struct Box<T>
{
T value;
}
fn identity<T>(T x) : T
{
x
}
fn pick<T>(bool first, T a, T b) : T
{
if (first) { a } else { b }
}
fn main()
{
Vec<i32> a = Vec::new(); // T from the declaration
Box<Vec<u8>> c = Box { .value = Vec::new() }; // both T's from the declared type
Vec::push(&mut c.value, 200); // the literal becomes u8 from the field type
assert(Vec::len(&c.value) == 1);
assert(Vec::len(&a) == 0);
i64 w = identity<i64>(3); // explicit: name<Type>(...)
assert(w == 3);
i64 big = 5000000000;
Vec<i64> both = Vec::new();
Vec::push(&mut both, pick(true, 0, big)); // T from big; the 0 is an i64
assert(both[0] == 0);
}auto and the expected type
auto x = e; gives x the type of e. Since inference is downward only, auto works
whenever e has a type of its own: a literal (i32/f64 by default), an arithmetic expression, a
call whose type parameters are already fixed, a struct literal with a concrete type. Where e
needs help (Vec::new()), write the type on the left instead: auto m = Mutex::new(HashMap::new());
cannot know the map's types, but mutex<HashMap<String, i64>> m = Mutex::new(HashMap::new()); can.
The branches of an if or match share one type, and a branch that is only a literal takes it from
the others: in auto n = if (big) { count } else { 0 }; with count : i64, the 0 is an i64.
import std;
fn main()
{
i64 count = 5000000000;
auto n = if (count > 10) { count } else { 0 }; // 0 : i64, from the other branch
Option<i64> o = None;
auto m = match (o) { None : -1, Some(v) : v }; // -1 : i64, from the Some arm
assert(n + m == 4999999999);
mutex<HashMap<String, i64>> totals = Mutex::new(HashMap::new()); // the declaration fixes K and V
assert(HashMap::len(&*lock(&totals)) == 0);
}- Write the type you mean; the language will not adjust it for you. Convert integers with
widen/narrow, and take references explicitly. - Use
autofreely for values whose type is obvious from the right-hand side, and a written type for empty containers and generic constructors. - Keep generic code to plumbing (containers, wrappers, moving values around). Anything that computes on
Tneeds a concrete type. - A
voidfunction returns(); you may write: voidexplicitly or leave the return type off.
§13Expressions
spec/13-expression-semantics.mdPlaces versus values, strict left-to-right evaluation, and the operators.
CobaltC has a small expression language with a fixed evaluation order and a clear line
between places (things you can assign to or borrow) and values. Almost everything
is an expression, including blocks, if and match, which is what lets you write
i32 x = if (c) { 1 } else { 2 };.
Places and values
A place is a variable name, a field access e.f, an index e[i], or a
dereference *r. A place can be read (yielding its value), written (place = e), borrowed
(&place, &mut place), or projected further. Everything else, such as a literal, an arithmetic
result, a call result or a struct literal, is a value or a temporary. Reading a place that holds a
resource is not allowed (that would copy the resource, §07); such a place may only be moved,
borrowed, or destructured.
import std;
struct Pair
{
i32 a;
array<i32, 3> items;
}
fn main()
{
Pair p = Pair { .a = 1, .items = [10, 20, 30] };
p.a = 99; // a field is a place
p.items[1] = 200; // so is an element
{
auto r = &mut p.items[2];
*r = 300; // and so is *r
}
assert(p.a == 99);
assert(p.items[1] == 200);
assert(p.items[2] == 300);
}Evaluation order
Every expression is evaluated strictly left to right: the left operand before the right,
the callee before its arguments, arguments in written order, struct-literal fields in written order.
Assignment evaluates the target place first, then the value. && and || short-circuit: the right
operand runs only when needed.
import std;
fn bump(ref<i32, exclusive> counter) : i32
{
*counter += 1;
*counter
}
fn main()
{
i32 n = 0;
i32 left = bump(&mut n);
i32 right = bump(&mut n);
assert(left == 1); // the first call ran first
assert(right == 2);
bool touched = false;
if (true || bump(&mut n) == 0) // the right side is never evaluated
{
touched = true;
}
assert(touched);
assert(n == 2);
}Expressions that produce values
import std;
fn main()
{
bool c = true;
i32 x = if (c) // if is an expression; both arms must have one type
{
1
}
else
{
2
};
i32 y = // a block's value is its trailing expression
{
i32 t = x * 3;
t + 1
};
Option<i32> o = Some(x);
i32 z = match (o) // so is match
{
Some(v) : v * 10,
None : 0,
};
assert(x == 1);
assert(y == 4);
assert(z == 10);
i32 w = 0;
w = 5; // assignment is an expression of type void
assert(w == 5);
}if, while or match used as a statement needs no semicolon after its
closing brace. When one of them is the right-hand side of a declaration or assignment, the
statement as a whole still ends with ;, as in i32 x = if (c) { 1 } else { 2 };.Operator precedence
From tightest to loosest binding. Comparison is non-associative: a < b < c is a
syntax error, and a < b == c needs parentheses.
| Level | Operators | Notes |
|---|---|---|
| postfix | e.f e[i] e(args) e? | field, index, call, propagate |
| unary | -e !e ~e &e &mut e *e | negate, boolean not, bitwise not, borrow, dereference |
| multiplicative | * / % | left-associative |
| additive | + - | left-associative; p + n on a rawptr is pointer offset (§20) |
| shift | << >> | the right operand is a u32 |
| bitwise and | & | |
| bitwise xor | ^ | |
| bitwise or | | | |
| comparison | == != < <= > >= | non-associative |
| logical and | && | short-circuit |
| logical or | || | short-circuit |
| assignment | = += -= *= /= %= &= |= ^= <<= >>= | right-associative; the left side must be a place |
x op= e is x = x op e with x evaluated once: the same types, the same overflow
check (x += 1 can overflow), the same write rules. Evaluating the place once matters only when it
contains a call, as in *Vec::index_exclusive(&mut v, i) += 1, which calls index_exclusive once.
There is no ++ or --; write += 1.
hi << 8 | lo is
(hi << 8) | lo. Unlike C, the bitwise operators bind tighter than comparisons, so
x & 1 == 0 parses as (x & 1) == 0. When in doubt, add parentheses.- Use
if,matchand blocks as values instead of declaring a variable and assigning in each branch. - Side effects in one operand are visible to the next: order is defined, so you may rely on it, but readers will thank you for not making them.
- Parenthesise mixed comparisons and bitwise expressions.
§14Control flow and scopes
spec/14-control-flow.mdBlocks, if, while, for, foreach, break and continue; scope exit as the moment of destruction; and the fixed line between compile-time and run-time checks.
Control flow is C's if, while and for, without switch and goto, plus
expression-valued if and match and a foreach over collections. What is new is how carefully the language ties scope to destruction, and
the precise line between what the compiler proves and what the run time checks.
Blocks and scopes
A block { … } opens a scope. Variables declared in it die when it ends, in reverse
order; resources are destroyed at that point (§07); references held by those variables end too. A
block is an expression: its value is its trailing expression, or () if there is none. Every
statement is a scope of its own for temporaries: a value created during a statement and not stored
anywhere is destroyed at the semicolon.
if and else
if (cond) { … } else { … } with mandatory parentheses and mandatory braces. else if
chains as expected. An if without else has type void; an if used as a value needs both arms,
of the same type.
The condition may also be a pattern test, if (Some(x) = e) { … } else { … }: it means
match (e) { Some(x) : { … }, _ : { … } }, so x exists in the first block only, e is consumed
exactly as that match would consume it, and the whole if can be a value. Any match pattern
works — nested (Ok(Some(l))), a literal (if (0 = n)), a qualified variant — as long as it can
fail: a bare name on the left of = is the assignment it always is, which is not a bool and is
rejected, so if (x = e) never silently tests anything. A match stays the form when both cases
do work, and the only form that checks every case is handled.
import std;
fn main()
{
HashMap<String, i32> ages = HashMap::new();
HashMap::insert(&mut ages, String::from_str("Ada"), 36);
if (Some(a) = HashMap::get_str(&ages, "Ada"))
{
assert(*a == 36); // a: ref<i32, shared>, in scope here only
}
Option<i32> o = Some(4);
i32 doubled = if (Some(x) = o) { x * 2 } else { 0 };
assert(doubled == 8);
Result<Option<i32>, str> r = Ok(Some(7));
if (Ok(Some(n)) = r) { assert(n == 7); } else { assert(false); }
if (Err(_) = r) { assert(false); }
}while, for, break, continue
while (cond) { … } loops while cond holds; for (init; cond; step) { … } is C's
counted loop: init runs once (a declaration there belongs to the loop), cond is tested before
each iteration, and step runs after each one. Any of the three may be left out; a missing cond
is true, so for (;;) loops until a break. The body is a fresh scope each iteration, so
anything declared inside it is destroyed before the next test. break exits the innermost loop;
continue goes to its next iteration, running a for's step first. Both are typed never and
must appear inside a loop. A body is always a block, so C's trap for (…); (an empty loop, then a
block that runs once) is a syntax error here, and the message says to remove the ;.
A while condition may be a pattern test too: while (Some(x) = e) { … } is
while (true) { match (e) { Some(x) : { … }, _ : break } } — e is evaluated again each time
round, the body runs with x bound while the pattern matches, and the first value that does not
match ends the loop. That last point matters for a Result: while (Ok(Some(line)) = read_line())
stops at the end of input and on a read error alike, which is the loop's written decision; a loop
that must tell them apart keeps the match.
import std;
fn main()
{
Vec<String> stack = Vec::from_fn(3, [](usize i) : String { sprintf("job %d", i) });
usize done = 0;
while (Some(job) = Vec::pop(&mut stack)) // each job moved out, destroyed at the body's end
{
if (String::ends_with(&job, "1")) { continue; }
done += 1;
}
assert(done == 2 && Vec::len(&stack) == 0);
u32 n = 10;
while (Some(m) = checked_sub(n, 3)) // stops when the subtraction would go below zero
{
n = m;
}
assert(n == 1);
}import std;
fn main()
{
i32 sum = 0;
i32 i = 0;
while (i < 10)
{
i += 1;
if (i == 3)
{
continue; // skip 3
}
if (i == 7)
{
break; // stop before adding 7
}
sum += i;
}
assert(sum == 1 + 2 + 4 + 5 + 6);
Vec<i32> v = Vec::new();
Vec::push(&mut v, 4);
Vec::push(&mut v, 9);
i32 total = 0;
for (usize k = 0; k < Vec::len(&v); k += 1) // an index loop; foreach (below) is shorter
{
total += v[k];
}
assert(total == 13);
u32 odd = 0;
for (u32 j = 0; j < 10; j += 1)
{
if (j % 2 == 0)
{
continue; // still runs j += 1
}
odd += j;
}
assert(odd == 25);
}fn main()
{
break; // ✗ diag.break-outside-loop
}Loops that only end by returning. A while (true) that no break leaves can
end only by return, a fault or exit, so nothing after it ever runs: its type is never, like
return's (D-0175). A function may therefore end with such a loop and needs no value after it. A
break inside a nested loop leaves only that loop and does not count; a break that leaves the
while (true) itself makes it an ordinary loop again, after which the function must still give its
value. Only the literal true has this meaning: the compiler does not look into a condition that
merely happens to stay true. There is no separate loop keyword; while (true) is the way to say
"forever".
import std;
// The first line that starts with `#`, read until one does; no value is
// needed after the loop, because nothing after it can run.
fn first_comment(ref<Vec<String>, shared> lines) : usize
{
usize i = 0;
while (true)
{
if (String::starts_with(&lines[i], "#"))
{
return i;
}
i += 1;
}
}
fn main()
{
Vec<String> lines = Vec::new();
Vec::push(&mut lines, String::from_str("x = 1"));
Vec::push(&mut lines, String::from_str("# note"));
printf("line %v\n", first_comment(&lines));
}line 1
foreach
foreach (x in c) { … } visits every element of a Vec, an array, a slice, a HashSet or a
HashMap, in order: by index for a Vec or an array, in insertion order for a set or a map. How
the loop holds the collection is written at the loop, the same three ways a function takes a
value:
| Written | Each element is | Afterwards |
|---|---|---|
foreach (x in c) | the element itself, moved out of c | c is gone: the loop consumed it |
foreach (x in &c) | a shared reference, ref<T, shared>: read it | c is unchanged and usable |
foreach (x in &mut c) | an exclusive reference: *x = … changes the element | c is usable, with its changes |
Use &c most of the time. A second name gives the position: foreach (i, x in &v) has
i : usize. A HashMap is always looped over with two names, the key and the value:
foreach (k, v in &m), or three to have the position too, foreach (i, k, v in &m). printf prints what a reference refers to, so printf("%d", x) works
with x a ref<i32, shared>; elsewhere write *x for the number.
import std;
fn shout(String s) // takes ownership
{
printf("%s!\n", &s);
}
fn main()
{
Vec<String> names = Vec::new();
Vec::push(&mut names, String::from_str("ada"));
Vec::push(&mut names, String::from_str("lin"));
foreach (i, name in &names) // read: names is still usable afterwards
{
printf("%d: %s\n", i, name);
}
Vec<i32> numbers = Vec::new();
Vec::push(&mut numbers, 3);
Vec::push(&mut numbers, 5);
foreach (n in &mut numbers) // change in place
{
*n *= 2;
}
foreach (n in &numbers)
{
printf("%d ", n); // printf reads through the reference
}
printf("\n");
HashMap<str, u32> stock = HashMap::new();
HashMap::insert(&mut stock, "pens", 12);
HashMap::insert(&mut stock, "ink", 3);
foreach (item, count in &stock)
{
printf("%-5s %2d\n", item, count);
}
foreach (name in names) // consume: each String moves into shout
{
shout(name);
}
// names is gone here; using it would be diag.stale-binding
}0: ada 1: lin 6 10 pens 12 ink 3 ada! lin!
The loop's borrow of the collection lasts the whole loop, so a body that tries to change
the collection itself (push to it, clear it) is rejected before the program runs, the same
aliasing rule as anywhere else. There is no iterator invalidation to debug. A consuming loop
left early, by break, return or a fault, destroys the elements it had not reached, exactly
once.
import std;
fn main()
{
Vec<i32> v = Vec::new();
Vec::push(&mut v, 1);
foreach (x in &v)
{
Vec::push(&mut v, *x); // ✗ diag.aliasing-conflict (static): v is borrowed by the loop
}
}An array of plain values, such as array<i32, 5>, is copied by the by-value form:
foreach (i, e in arr) walks a copy, so arr is neither borrowed nor used up, a write to
arr[j] in the body is allowed (and not seen by e), and arr is still there afterwards. To change
the elements themselves, loop over &mut arr and write through the reference: *e = …. While
that loop runs, arr is borrowed by it, so reach the elements through e, not through arr[j].
import std;
fn main()
{
array<i32, 5> arr = [0, 0, 0, 0, 0];
foreach (i, e in &mut arr)
{
*e = narrow<i32>(i) * 10; // write through the element's reference
}
arr[4] += 1; // direct indexing works too, outside the loop
foreach (i, e in &arr)
{
printf("[%v] = %v\n", i, *e);
}
}[0] = 0 [1] = 10 [2] = 20 [3] = 30 [4] = 41
Counting with a range
foreach (x in lo..hi) counts from lo up to, not including, hi, the same way
&a[lo..hi] takes elements lo up to hi. So 0..n runs exactly n times, and 1..10 stops at
9. A second name gives the position, counted from 0: foreach (i, x in lo..hi). Both bounds are
evaluated once, lo first. If hi is not above lo, the body never runs.
x has the bounds' integer type. A plain number takes the other bound's type, so with
u8 top, 0..top counts in u8. Two plain numbers are usize, so the counter can index an
array or a Vec directly; for a negative range, give one bound a type: -3..0: i32. Bounds of two different types are
rejected rather than converted. A range can end at its type's largest value without overflowing,
because the counter never passes hi.
import std;
fn main()
{
foreach (v in 1..10)
{
printf("%v ", v);
}
printf("\n");
auto names = ["ada", "lin", "kay"];
foreach (i in 0..3) // i : usize, so it can index
{
printf("%v: %v\n", i, names[i]);
}
u8 top = 255;
foreach (i, b in 253..top) // b : u8; i is the position
{
printf("step %v is %v\n", i, b);
}
}1 2 3 4 5 6 7 8 9 0: ada 1: lin 2: kay step 0 is 253 step 1 is 254
A range to count over is written only in a foreach (a slice's lo..hi, §16, is the same
notation for elements). It is not a value you can store, and it has no
inclusive form or step. To count down or in steps, use a for loop.
Early exit and unwinding
return, break, continue, and a run-time fault all leave scopes the same way: every
statement scope and block between here and the target is exited in order, destroying what each one
owns. A return e first moves e out so it survives the unwinding. You never have to write
cleanup code on an early exit.
What is checked when
Many rules in this guide say a program is "rejected" in one example and "faults at run
time" in another. The boundary is fixed by the language, not left to the compiler's cleverness, so
that every implementation accepts and rejects the same programs. A compiler may prove more than this
checker does and leave out the run-time checks it has proved unnecessary (that is what cobc does; see
the performance chapter), but it may not accept a program the language rejects or reject one it accepts:
coby --check is the reference, and what it accepts is the language. Within one function body, the
checker tracks a few facts about each variable: whether it has been moved from, whether it has been
initialised, which references were visibly borrowed from it, and whether a reference to it has been
handed somewhere the checker cannot follow (a call, a field, a thread). At each checked operation
the outcome is one of three:
| Outcome | When | Effect |
|---|---|---|
| proven | the tracked facts guarantee the condition | no run-time check |
| refuted | the tracked facts guarantee it fails | the program is rejected (a static diagnostic) |
| unknown | the facts do not decide (the value came from a call, a loop merged two states, a reference passed through a parameter) | the run-time check runs, and faults if it fails (a dynamic diagnostic) |
The analysis looks only at the current function and only at literals for value ranges.
So 2147483647 + 1 is refuted statically, add(2147483647, 1) is checked when add runs, and
borrowing x twice exclusively in one function is refuted while doing so through two reference
parameters is checked at the use. Three exceptions are stricter: definite assignment (§11), a use
of a variable that may have been moved (§7), and reference escape (§10) reject on "unknown" rather
than deferring.
- Let scopes do your cleanup. Open a block to bound a borrow or a lock; close it to release.
- A
whilebody is re-entered fresh each iteration; declare per-iteration variables inside it. - Prefer trailing expressions to
returnat the end of a function; usereturnfor early exits. - If a diagnostic says static, the checker saw the problem in the text; if dynamic, it saw it when the values arrived. Both are the same rule.
§15Functions and closures
spec/15-function-semantics.mdSignatures that state the whole contract, associated functions, function values, and closures with checked capture lists.
Functions in CobaltC carry their whole contract in the signature. A parameter's type says whether the function takes ownership, may only look, or may modify. Closures are ordinary structs that capture what they use, and the capture list is checked, not trusted.
Declaring and calling
import std;
// Parameters are type-first; the return type follows a colon.
fn area(i32 w, i32 h) : i32
{
w * h // the trailing expression is the result
}
// No return type means void; `return;` leaves early.
fn clamp_in_place(ref<i32, exclusive> x, i32 hi)
{
if (*x <= hi)
{
return;
}
*x = hi;
}
// An associated function: a plain function whose name is qualified by a type.
struct Rect
{
i32 w;
i32 h;
}
fn Rect::square(i32 side) : Rect
{
Rect { .w = side, .h = side }
}
fn Rect::area(ref<Rect, shared> r) : i32
{
area(r.w, r.h)
}
fn fact(i32 n) : i32
{
if (n <= 1) // recursion needs nothing special
{
1
}
else
{
n * fact(n - 1)
}
}
fn main()
{
i32 v = 50;
clamp_in_place(&mut v, 10);
assert(v == 10);
auto sq = Rect::square(4);
assert(Rect::area(&sq) == 16); // called by qualified name; there is no method syntax
assert(fact(5) == 120);
}Recursion needs nothing special, and its depth is limited only by the stack. Every
implementation allows at least 10 000 nested calls, and both tools allow far more: they run a
program and each of its threads on a large stack. A call beyond the limit is not a crash but a
fault at the call, diag.stack-exhausted, so a recursion that runs away ends with a diagnostic
like any other failure.
A parameter list and an argument list may end with a comma, as array and struct literals
may: handy when a long call is split one argument per line. A comma is never the whole list:
f(,) is a syntax error.
import std;
fn span(
i32 from,
i32 to,
) : i32
{
to - from
}
fn main()
{
assert(span(
3,
10,
) == 7);
}What a parameter type promises
| Parameter type | The caller writes | The callee may | After the call |
|---|---|---|---|
plain T (numbers, plain structs) | f(x) | use its own copy | the caller's x is unchanged |
resource T (Vec<i32>, handle<T>, …) | f(v) | own it: mutate, move on, or let it be destroyed at its scope end | the caller's v is stale |
ref<T, shared> | f(&x) | read through it | the borrow is released; x is unchanged |
ref<T, exclusive> | f(&mut x) | read and write through it, but not destroy or move x | the borrow is released; x may have changed |
Function values
A function type is written fn(T1, T2) : R (or fn(T1) for a void result). A
parameter of that type accepts a function with that signature, or a closure with the same
parameters and result. Function values cannot be compared.
A closure's type is fn(parameters) : result, the result being what its body gives. Where
a fn type is expected, as when a closure is declared fn(u32) : u32 f = … or passed to such a
parameter, the body is checked against that result, and its literals take that type. A closure
declared with auto takes its body's own type: [](u32 x) { 1 } gives an i32, as auto y = 1
does. A closure can be stored like any value: in a variable, a field, a Vec<fn(…) : …> or a
Box, and called from there.
A function value owns the closure it holds and destroys it when it ends. Copying the value
(g = f, passing it by value) copies the closure with its captures, so each copy keeps its own
state. The one exception is a closure that owns a resource, such as a move closure that captured a
String: copying it would duplicate the resource, so it moves instead, and the value it
came from is left empty. Calling an empty function value is diag.stale-binding; the function type
does not say what its closure owns, so this is found when the program runs. Calling through a
reference, a field or an element ((*f)(x), (t.run)(), (fs[i])()) calls the value where it is
and moves nothing.
Closures
Functions do not nest: a fn declared inside another function is a syntax error, unless it
is a function of a type declared in the same block (fn T::name, §16). A function needed only
inside another is written there as a closure, which can also use the variables around it.
A closure is written [captures](params) { body }, in the style of a C++ lambda. The
bracket must list exactly the outer variables the body uses, no more and no fewer; it is
documentation that the compiler checks (diag.capture-list-mismatch). How each variable is captured
is decided by the body, not the list: a variable the body only reads is captured by shared
reference; one the body writes, borrows exclusively, or moves is captured by exclusive reference.
Prefix move to capture by value instead, moving resources into the closure. The result type is
taken from the body, or from the fn(…) : R the closure is passed as; write it after the
parameters, as for a function, when neither says enough: [](i64 a) : i64 { 0 }.
import std;
fn main()
{
auto zero = [](i64 a) : i64 { 0 }; // without `: i64` the 0 would be an i32
auto small = [](u8 a) : bool { a < 10 };
assert(zero(5000000000) == 0 && small(3));
}import std;
fn apply(fn(i32) : i32 f, i32 x) : i32
{
f(x)
}
fn main()
{
i32 k = 10;
auto add_k = [k](i32 x) // k is read only: captured as ref<i32, shared>
{
x + k
};
assert(add_k(5) == 15);
assert(apply(add_k, 4) == 14); // a closure where a fn(i32) : i32 is expected
Vec<i32> v = Vec::new();
Vec::push(&mut v, 7);
auto reader = move [v]() // v moves into the closure; the closure owns it now
{
v[0]
};
assert(reader() == 7);
assert(reader() == 7); // calling twice is fine: nothing is consumed
} // reader is destroyed here, and its Vec with itfn main()
{
i32 x = 1;
i32 unused = 2;
auto f = [x, unused]() // ✗ diag.capture-list-mismatch: unused is not used by the body
{
x
};
f();
}A closure that captures by reference holds those borrows for as long as the closure
object lives, so the captured variables are subject to the usual aliasing rules until it goes out
of scope: a closure that writes total blocks every other use of total while it exists. Put such
closures in a block, or use move. Calling a closure borrows it shared for the call: its writes go
through the captures, which are checked as any reference is, so a closure that writes a capture
cannot be called through a shared reference to it.
fn main()
{
i32 total = 0;
{
auto add = [total](i32 x) // the body writes total: captured as ref<i32, exclusive>
{
total += x;
};
add(5);
add(7);
} // add ends here and releases its borrow of total
i32 seen = total; // ✓ 12: no borrow remains
}Generic functions
fn name<T>(...) declares type parameters; see §12 for what a body may do with T and
how T is inferred at each call.
main
A program is a set of declarations with exactly one main at the top level, taking no
parameters: fn main(), or fn main() : u8. Execution is the evaluation of main(); the program's
observable behaviour is its termination outcome (success or a named fault) plus the sequence of
extern calls it makes.
A main that returns a u8 sets the program's exit status; fn main()
exits with 0. Returning from main runs every destructor still pending and waits for every
thread, as leaving any block does, so the status is reported only after all of that. A fault
still ends the program with its diagnostic instead. The arguments the program was started with
come from arg_count() and arg(i) (§21); argument 0 is the first one after the program, not the
program's name.
coby greet.cb Adarunsimport std;
fn main() : u8
{
if (arg_count() != 1)
{
printf("usage: greet NAME\n");
return 2; // exit status 2
}
match (arg(0))
{
Ok(name) :
{
printf("hello, %v\n", &name);
0
},
Err(_) :
{
printf("the name is not UTF-8\n");
1
},
}
}hello, Ada
Run with no argument, the same program prints its usage line and exits with status 2.
A main with parameters, or returning anything other than u8, is rejected with
diag.no-main.
- Choose the parameter kind that says the least: a shared reference if the function only reads, an exclusive reference if it modifies, by value only if it needs to keep the thing.
- Return values rather than out-parameters; a struct or an enum is cheap to return.
- List every captured variable in the closure brackets and let the compiler tell you if you got it wrong.
- Use
moveclosures for anything that outlives the current block or runs on another thread.
§16Structs, arrays, enums and match
spec/16-aggregates.mdThe four aggregate kinds, how they nest, and exhaustive matching over enums with payloads.
CobaltC has four aggregate kinds: structs (named fields), bitstructs (named runs of bits
packed into one unsigned integer), arrays (a fixed number of elements) and enums (one of several
variants, each with an optional payload). They compose freely, and all but bitstructs generically. match is how you take an enum apart, and it must handle every variant.
Structs
import std;
struct Point
{
i32 x;
i32 y;
}
struct Segment
{
Point from; // structs nest by value
Point to;
}
fn main()
{
auto a = Point { .x = 1, .y = 2 }; // every field, once, in any order
Point b = Point { .y = 2, .x = 1 };
assert(a == b); // plain structs compare field by field
auto s = Segment { .from = a, .to = Point { .x = 4, .y = 6 } };
s.to.y = 7; // nested fields are places
assert(s.to.y == 7);
Point c = a; // plain structs copy
c.x = 100;
assert(a.x == 1);
}Field initialisers use C99's designated form .name = value. Fields are private to the
module that declares the struct unless marked export (§17). A struct with no fields is allowed.
Mark a struct resource if it manages something beyond its fields (§07).
bitstruct: named bits of one integer
A bitstruct is a struct whose fields are runs of bits of one unsigned integer, its backing
type: bitstruct Instr : u32 { op : 6; rd : 5; rs : 5; imm : 16; }. The fields are laid out from
the low bit up in declaration order and must fill the backing type exactly (name the unused bits,
pad : 3;). Each field has the smallest unsigned type that holds its width — u8 up to 8 bits,
u16 up to 16, and so on — and is read and written like any field, i.op, i.imm = 500. A write
that does not fit its field faults with diag.narrowing-overflow, or is rejected outright when the
value is a literal; nothing is ever truncated. Instr::bits(i) is the whole integer and
Instr::from_bits(w) the fields of one, both total. A bitstruct is a plain value: copied by a read,
clone, equal by its bits, a HashMap key, and its bytes in raw memory are the integer's. A field
has no address of its own, so it cannot be borrowed — borrow the whole value — and a bitstruct is
not taken apart by destructuring. Byte order is not its concern: Vec::push_le and push_be write
it out (D-0119), and an unsafe extern fn takes Instr::bits(i).
import std;
bitstruct Double : u64
{
mantissa : 52; // bits 0 .. 52
exponent : 11; // bits 52 .. 63
sign : 1; // bit 63
}
bitstruct Instr : u32
{
op : 6;
rd : 5;
rs : 5;
imm : 16;
}
fn main()
{
Double d = Double::from_bits(reinterpret<u64>(-2.5));
assert(d.sign == 1 && d.exponent == 1024); // exponent: u16, sign: u8
d.exponent = 1023; // fits in 11 bits: checked
assert(reinterpret<f64>(Double::bits(d)) == -1.25);
Instr i = Instr { .op = 3, .rd = 1, .rs = 2, .imm = 500 };
u32 word = Instr::bits(i);
assert(word == 3 | 1 << 6 | 2 << 11 | 500 << 16);
Instr j = Instr::from_bits(word);
assert(j == i && sizeof<Instr>() == 4);
j.imm = 65535; // the largest 16-bit value
assert(j.imm == 65535 && j.op == 3);
}import std;
bitstruct Flags : u8 { carry : 1; zero : 1; pad : 6; }
fn main()
{
Flags f = Flags { .carry = 2, .zero = 0, .pad = 0 }; // ✗ 2 does not fit 1 bit: diag.narrowing-overflow
}Arrays
array<T, N> holds exactly N elements; N is a constant expression — a literal, a const name (m::N too), or + - * / % & | ^ << >> ~ ( ) over them, folded before the program runs (D-0126); outside parentheses > ends it, so Vec<array<u8, N * 2>> reads as expected. The literal form is [a, b,
c]. Indexing takes a usize and is bounds-checked: a literal index out of range is rejected
statically, a computed one faults at run time with diag.index-out-of-bounds. Arrays of plain
elements are plain (copied on assignment); an array of resources is a resource.
import std;
fn sum(ref<array<i32, 4>, shared> a) : i32
{
i32 t = 0;
foreach (i in 0..4)
{
t += a[i]; // indexing looks through the reference
}
t
}
fn main()
{
array<i32, 4> a = [10, 20, 30, 40];
a[2] = 33;
assert(sum(&a) == 103);
auto grid = [[1, 2], [3, 4]]; // array<array<i32, 2>, 2>
assert(grid[1][0] == 3);
array<u32, 256> counts = [0; 256]; // one value in every element
counts[65] += 1;
auto flags = [false; 8]; // array<bool, 8>
array<str, 3> names = [
"ann",
"bo",
"cy", // a trailing comma is allowed
];
assert(counts[65] == 1 && !flags[7] && names[2] == "cy");
}[v; N] makes an array of N copies of v, which is evaluated once. N is written as in
the type array<T, N>, a constant expression: a number, a constant (const usize W = 24; then
array<u8, W> and [0; W], in this module or, qualified, a visible one of another,
array<u8, screen::W>), or arithmetic on them ([0; W * 2]). Because the value is copied, its type must be plain: a
String or other resource, a reference, or a function value cannot be repeated
(diag.read-of-resource or diag.type-mismatch); build such arrays element by element, or
use a Vec.
fn get(array<i32, 3> a, usize i) : i32
{
a[i] // checked against 3 when the index arrives
}
fn main()
{
array<i32, 3> a = [1, 2, 3];
get(a, 3); // ✗ diag.index-out-of-bounds (dynamic)
}Arrays of arrays
CobaltC has no separate multidimensional array type. An array's element can itself be an
array, so a grid, a cube, or anything with more dimensions is the one constructor used again:
array<array<i64, 3>, 2> is an array of 2 elements, each an array<i64, 3>. The same holds for the
literal forms, [[1, 2, 3], [4, 5, 6]] and [[0; 3]; 2], and for auto, which infers the nesting
from the literal.
Two directions are worth keeping straight:
- The type reads from the inside out. In
array<array<array<i64, 3>, 2>, 4>the innermostarray<i64, 3>is a row of 3 numbers, the nextarray<…, 2>holds 2 such rows (a block), and the outermostarray<…, 4>holds 4 blocks. The length written last is the outermost one. - Indexing and loops go from the outside in.
cube[i][j][k]picks blocki(below 4), then rowjof it (below 2), then elementkof that row (below 3). Nested loops follow the same order: the outer loop walks the blocks, the innermost the elements of one row, so the elements are visited in the order they lie in memory.
Each index is checked against its own length. Because every level is an ordinary array,
cube[1] is a whole block and cube[1][0] a whole row, values you can copy, pass and loop over.
Writing those inner types out gets long; where the type is clear from the right-hand side, auto
reads better. To print every value, nest one foreach per level: foreach (i, block in &cube)
borrows each block, and the loop inside it over block borrows each row of that block, so nothing
is copied.
import std;
fn main()
{
// Read the type from the inside out: rows of 3 i64s, 2 rows to a
// block, 4 blocks.
array<array<array<i64, 3>, 2>, 4> cube = [[[0; 3]; 2]; 4];
// Index from the outside in: cube[block][row][column].
foreach (i in 0..4)
{
foreach (j in 0..2)
{
foreach (k in 0..3)
{
cube[i][j][k] = narrow<i64>(100 * i + 10 * j + k);
}
}
}
printf("cube[3][1][2] = %v\n", cube[3][1][2]);
// Print every value: each loop borrows one level of the one outside it.
foreach (i, block in &cube)
{
foreach (j, row in block)
{
printf("cube[%v][%v]:", i, j);
foreach (x in row)
{
printf(" %v", x);
}
printf("\n");
}
}
// `auto` spares spelling the inner types; each is a copy (the elements are plain).
auto block = cube[1]; // array<array<i64, 3>, 2>
auto row = block[0]; // array<i64, 3>
auto grid = [[1, 2, 3], [4, 5, 6]]; // array<array<i32, 3>, 2>
printf("row %v %v %v, grid[1][2] = %v\n", row[0], row[1], row[2], grid[1][2]);
}cube[3][1][2] = 312 cube[0][0]: 0 1 2 cube[0][1]: 10 11 12 cube[1][0]: 100 101 102 cube[1][1]: 110 111 112 cube[2][0]: 200 201 202 cube[2][1]: 210 211 212 cube[3][0]: 300 301 302 cube[3][1]: 310 311 312 row 100 101 102, grid[1][2] = 6
For a growable sequence use Vec<T> (§21). A Vec is indexed the same way, v[i]: it
reads through Vec::index_shared, and writes (an assignment, &mut v[i]) through
Vec::index_exclusive, so every rule about borrowing the Vec applies.
Slices
A slice is a borrowed run of consecutive elements of an array, a Vec, or another slice:
&a[i..j] is elements i up to (not including) j, and &mut a[i..j] the same for writing. Inside
the brackets, $ stands for the length of what is being indexed, so &a[0..$] is all of it and
&a[$ - 3 .. $] the last three, and v[$ - 1] is a Vec's last element. $ is a usize like any
index, so v[$ - 1] of an empty v is an overflow fault, never a wrapped-around index.
The type is slice<T, shared> or slice<T, exclusive>. One parameter of that type takes any
array, any Vec, or part of either, so a function need not be written once per container or per
array length. slice_len(s) is its length; s[i] is bounds-checked; foreach works over it. A
slice is a borrow like a reference: while it lives, the elements it views cannot be changed except
through it, its source cannot be pushed to or popped from, and a slice of a local cannot be returned.
import std;
// Sorts any run of i32 in place: an array, a Vec, or part of one.
fn sort(slice<i32, exclusive> s)
{
for (usize i = 1; i < slice_len(s); i += 1)
{
usize j = i;
while (j > 0 && s[j - 1] > s[j])
{
i32 t = s[j - 1];
s[j - 1] = s[j];
s[j] = t;
j -= 1;
}
}
}
fn sum(slice<i32, shared> s) : i32
{
i32 total = 0;
foreach (x in s)
{
total += *x;
}
total
}
fn main()
{
auto a = [5, 3, 9, 1, 7, 2];
sort(&mut a[0..3]); // only the first three: [3, 5, 9, 1, 7, 2]
assert(a[0] == 3 && a[3] == 1);
assert(sum(&a[$ - 3 .. $]) == 10); // the last three
Vec<i32> v = Vec::new();
foreach (x in [40, 10, 30, 20])
{
Vec::push(&mut v, x);
}
sort(&mut v[0..$]); // the same function, on a Vec
assert(v[0] == 10 && v[$ - 1] == 40);
v[0] += 1; // indexing a Vec, written
assert(sum(&v[0..$]) == 101);
}import std;
fn main()
{
Vec<i32> v = Vec::new();
Vec::push(&mut v, 1);
auto s = &v[0..1];
Vec::push(&mut v, 2); // ✗ diag.aliasing-conflict: s still borrows v
i32 first = s[0]; // (because s is used here)
}A slice borrows only its own elements. Two slices of one array or Vec conflict only if their
ranges share an element, so a buffer can be split into parts that are each written through their
own exclusive slice, at the same time: the halves of a divide-and-conquer algorithm, or one part per
thread. What reaches the whole source (&mut v, Vec::push) still conflicts with every live slice
of it.
import std;
fn sum_into(slice<u64, exclusive> s, u64 start) : u64
{
u64 total = 0;
foreach (i in 0..slice_len(s))
{
s[i] = start + widen<u64>(i);
total += s[i];
}
total
}
fn main()
{
Vec<u64> v = Vec::new();
for (usize i = 0; i < 100; i += 1)
{
Vec::push(&mut v, 0);
}
usize mid = slice_len(&v[0 .. $]) / 2;
{
slice<u64, exclusive> left = &mut v[0 .. mid]; // elements 0 .. 50
slice<u64, exclusive> right = &mut v[mid .. $]; // elements 50 .. 100: disjoint
auto a = spawn(sum_into, left, 0: u64); // each half filled by its own thread
auto b = spawn(sum_into, right, 1000: u64);
assert(join(a) + join(b) == 1225 + 51225);
}
assert(v[49] == 49 && v[50] == 1000);
}Types declared inside a function
A struct, enum or bitstruct used by one function can be declared inside it, as a
statement, and so can its functions, fn T::name(…) (a destructor too). It is an ordinary type whose name is
scoped like a local: usable from its declaration to the end of its block. Two functions can each
have their own Entry. A local type and its functions see module items and the local types and
constants around them, but not the function's variables or type parameters; a plain local function
is written as a closure (§15).
import std;
fn shortest(ref<Vec<String>, shared> words) : usize
{
struct Entry // visible only in this function
{
usize len;
usize at;
}
fn Entry::better(ref<Entry, shared> a, ref<Entry, shared> b) : bool
{
a.len < b.len || (a.len == b.len && a.at < b.at)
}
Entry best = Entry { .len = max_value<usize>(), .at = 0 };
foreach (i, w in words)
{
Entry e = Entry { .len = String::len(w), .at = i };
if (Entry::better(&e, &best))
{
best = e;
}
}
best.at
}
fn main()
{
Vec<String> w = Vec::new();
Vec::push(&mut w, String::from_str("banana"));
Vec::push(&mut w, String::from_str("fig"));
Vec::push(&mut w, String::from_str("pea"));
assert(shortest(&w) == 1); // "fig": as short as "pea", and first
}A bitstruct declared in a function is the same as one at module level: it gets its
Name::bits and Name::from_bits, and its fields read and write in the same way. A packed header
that one decoder reads can live inside that decoder.
import std;
fn payload_len(u16 raw) : u16
{
bitstruct Header : u16 // bits 0..4, 4..14, 14..16
{
kind : 4;
len : 10;
flags : 2;
}
Header h = Header::from_bits(raw);
h.len
}
fn main()
{
assert(payload_len(0x0123) == 18); // 0x0123 >> 4 is 0x12
assert(payload_len(0xFFFF) == 1023);
}Enums and match
An enum lists its variants; each may carry one payload type. Variant names are used
bare (Cold(3), Fine) or qualified (Verdict::Fine) when two visible enums share a name. A
variant with no payload behaves as if it carried ().
match (e) { … } compares the discriminant and runs the arm for that variant. Each arm is
Variant(binder) : expression, for a payload, Variant : expression, for none, or
_ : expression, as a catch-all; every arm ends with a comma. The arms must cover every variant
or include _. Arms are expressions, so a match produces a value; use a block as the arm body
for statements. Arms are tried in order and the first that fits is taken.
import std;
enum Shape
{
Circle(i32), // radius
Square(i32), // side
Dot
}
fn area(Shape s) : i32
{
match (s)
{
Circle(r) : 3 * r * r, // the payload is bound to r for this arm
Square(w) : w * w,
Dot : 0,
}
}
fn is_round(Shape s) : bool
{
match (s)
{
Circle(_) : true, // _ as a binder ignores the payload
_ : false, // a wildcard arm covers the rest
}
}
fn main()
{
assert(area(Circle(2)) == 12);
assert(area(Shape::Dot) == 0);
assert(is_round(Square(1)) == false);
array<Shape, 2> shapes = [Circle(1), Square(3)];
assert(area(shapes[1]) == 9);
}enum Sign
{
Pos,
Neg,
Zero
}
fn describe(Sign s) : i32
{
match (s)
{
Pos : 1,
Neg : -1, // ✗ diag.non-exhaustive-match: Zero is not covered
}
}
fn main()
{
describe(Zero);
}Nested patterns
Inside the parentheses, a pattern can say what the payload itself must be, as deep as
needed: Ok(Some(line)) matches an Ok whose payload is a Some, and binds the Some's
payload. A name there is a binder, unless it is a variant's name (Some(None)), and _ ignores
that level. Since a variant has one payload, an arm binds one name, the innermost payload it
reaches, unless that payload is a struct taken apart by a struct pattern (below). Every value must reach some arm (the error names one that does not, such as
Ok(None)), and every arm must be reachable: an arm whose values all go to earlier arms is
rejected, since it could never run.
import std;
fn classify(Result<Option<i32>, ParseError> r) : i32
{
match (r)
{
Ok(Some(n)) : n, // two levels tested, the inner payload bound
Ok(None) : 0,
Err(Invalid(at)) : -100 - narrow<i32>(at),
Err(_) : -1,
}
}
fn main()
{
assert(classify(Ok(Some(7))) == 7);
assert(classify(Ok(None)) == 0);
assert(classify(Err(Invalid(3))) == -103);
assert(classify(Err(Empty)) == -1);
}import std;
fn main()
{
Option<Option<i32>> o = Some(None);
i32 v = match (o)
{
Some(Some(x)) : x,
Some(None) : 1,
Some(_) : 2, // ✗ diag.unreachable-arm: every Some went to the arms above
None : 0,
};
}Struct patterns
Where a binder could stand, a struct pattern can take the payload apart at once:
Some(Question { name, end }) binds name and end, as Question { name, end } = q; would after
binding q. The rules are destructuring's (§11): every field is named once, or .. stands for the
rest; end : e binds a field to another name; and the fields are taken by value, so a String
field moved out consumes the value matched, as any binder of a resource payload does. A struct
pattern always fits, so it counts as a binder for exhaustiveness. It works in match arms and in
if and while patterns. Through a reference (match (&o)) it is rejected: bind the payload and
read its fields through it.
import std;
struct Question
{
String name;
u32 end;
}
fn ask(u32 k) : Option<Question>
{
if (k == 0)
{
return None;
}
Some(Question { .name = "who", .end = k })
}
fn main()
{
if (Some(Question { name, end }) = ask(4))
{
assert(name == "who" && end == 4);
}
u32 n = match (ask(7))
{
Some(Question { end : e, .. }) : e, // renamed; `name` left and ended
None : 0,
};
assert(n == 7);
}Literal patterns
A pattern can also be a literal: an integer (3, -1, 0x10), a byte (b'x'), true or
false. It can stand alone, when the value being matched is an integer or a bool, or at the end
of a nested pattern: Ok(0), Some(b'\n'). This is CobaltC's multi-way branch in place of switch,
and it is checked: a literal must be of the type it is compared with, a repeated case is an arm that
can never run, and an integer match needs a _ arm, since literals never list every value
(true and false do cover a bool). Text is matched the same way: a match on a str, a
String or a StringView takes text literals ("get" : …), compared byte for byte where the text
is, so a String you match on is still yours afterwards; Some("quit") works inside a payload too,
and if ("quit" = cmd) as a condition. Floats are not patterns. With that, patterns are _, a
literal, a variant, and a variant with a pattern for its payload; there is nothing else to learn.
import std;
fn step(u8 op, i64 acc) : i64
{
match (op)
{
0x01 : acc + 1, // a byte from an instruction stream
0x02 : acc * 2,
0x10 : 0,
_ : acc, // any other opcode: no effect
}
}
fn is_end(Result<usize, FileError> r) : bool
{
match (r)
{
Ok(0) : true, // a read of 0 bytes: the end of the file
Ok(_) : false,
Err(_) : true,
}
}
fn main()
{
array<u8, 5> program = [0x01, 0x02, 0x02, 0x7f, 0x01];
i64 acc = 0;
foreach (op in program)
{
acc = step(op, acc);
}
assert(acc == 5);
assert(is_end(Ok(0)) && !is_end(Ok(16)));
}import std;
fn opcode(str word) : i32
{
match (word)
{
"load" : 1,
"store" : 2,
"jump" : 3,
_ : -1, // any other word: not an instruction
}
}
fn is_quit(ref<String, shared> line) : bool
{
match (*line) // a String is read where it is, not moved
{
"quit" : true,
"exit" : true,
_ : false,
}
}
fn main()
{
assert(opcode("jump") == 3 && opcode("halt") == -1);
String cmd = "exit";
assert(is_quit(&cmd) && String::len(&cmd) == 4);
Option<String> first = Some(String::from_str("load"));
i32 k = match (first) { Some("load") : 1, Some(_) : 2, None : 0, };
assert(k == 1);
}Ranges and several values
An arm names one pattern: there are no range patterns (10..15 : …) and no lists of
alternatives (1, 4, 6 : … or 1 | 4 | 6 : …); each is a syntax error. Two forms cover these cases.
To test a range, match on the test itself: the condition is a bool, and true and false cover
it, so no _ arm is needed. Write the bounds as a range would mean them, the upper one left out
(v < 15 for 10..15).
import std;
fn check(i32 v)
{
match (v >= 10 && v < 15) // 10 to 14, as 10..15 would mean
{
true : printf("%v\n", v),
false : printf("%v is not in the correct range\n", v),
}
}
fn main()
{
check(12);
check(15);
check(9);
}12 15 is not in the correct range 9 is not in the correct range
For several values that share an action, give each value its own arm, and put an action
longer than a line in a function the arms call: the match still checks every literal's type and
reports a repeated one. When the values are the whole question, match on the test instead, as for a
range. For a longer list, keep the values in an array and ask contains, or in a HashSet when the
list is built at run time. Text works the same way: one arm per word.
import std;
fn weekend(ref<array<str, 7>, shared> names, u32 day)
{
printf("%v is a weekend day\n", names[widen<usize>(day)]);
}
fn describe(u32 day)
{
array<str, 7> names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
match (day)
{
0 : weekend(&names, day), // one arm per value; the shared action in a function
6 : weekend(&names, day),
_ : printf("%v is a weekday\n", names[widen<usize>(day)]),
}
}
fn is_square(u32 v) : bool
{
match (v == 1 || v == 4 || v == 9) // several values as one test: true and false cover it
{
true : true,
false : false,
}
}
fn main()
{
describe(0);
describe(3);
assert(is_square(4) && !is_square(5));
array<u32, 6> small = [2, 3, 5, 7, 11, 13]; // a longer list
Vec<u32> primes = Vec::from_slice(&small[0 .. $]);
assert(Vec::contains(&primes, &7) && !Vec::contains(&primes, &9));
str cmd = "exit";
bool quit = match (cmd)
{
"quit" : true,
"exit" : true,
_ : false,
};
assert(quit);
}Sun is a weekend day Wed is a weekday
import std;
fn main()
{
u8 op = 2;
i32 cycles = match (op)
{
1 : 3,
2 : 5,
1 : 4, // ✗ diag.unreachable-arm: 1 is taken by the first arm
_ : 1,
};
}Binders and named constants
A name on its own is a pattern too. If it names a constant of integer or bool type, it is
that constant's value, a literal by name (PUSH : …); if it names a variant, it is that variant;
otherwise it is a new binding for the whole value, which matches anything, like _, and is
usually the last arm (other : …). A binding of a resource moves it, as a payload binding does.
Mistaking a constant for a binding cannot go unnoticed: a match on an integer then lacks _.
import std;
const u8 PUSH = 0x01;
const u8 POP = 0x02;
fn describe(u8 op) : i32
{
match (op)
{
PUSH : 1,
POP : 2,
other : 100 + widen<i32>(other), // every other opcode, bound to `other`
}
}
fn main()
{
assert(describe(1) == 1);
assert(describe(2) == 2);
assert(describe(9) == 109);
}Matching through a reference
match (e) binds each payload by value: a plain one is copied, and a resource one (a
String, a Vec, a Box) is moved out, which consumes the enum. To look at an enum without
taking it apart, match a reference to it: match (&e) binds each payload as a shared reference,
match (&mut e) as an exclusive one, through which the payload can be changed. A scrutinee that
already is a reference, such as a function's ref<Expr, shared> parameter, binds in its own mode.
The enum is not consumed, and the usual borrow rules apply to what is bound: nothing can replace
the enum while a payload reference is still in use. This is how a tree built from Boxes (§21)
is walked.
import std;
enum Expr
{
Num(i64),
Neg(Box<Expr>),
}
fn eval(ref<Expr, shared> e) : i64
{
match (e) // e is a reference: n and x are references too
{
Num(n) : *n,
Neg(x) : -eval(Box::get(x)),
}
}
fn main()
{
Expr e = Neg(Box::new(Neg(Box::new(Num(7)))));
assert(eval(&e) == 7);
Option<String> name = Some(String::from_str("ada"));
match (&mut name) // change the payload in place
{
Some(s) : String::push_ascii(s, b'!'),
None : {},
}
usize len = match (&name) // look without consuming
{
Some(s) : String::len(s),
None : 0,
};
assert(len == 4);
String kept = Option::unwrap_or(name, String::new()); // name still owns its String
assert(String::len(&kept) == 4);
}A nested pattern looks through a reference payload in the same way. HashMap::get gives
an Option<ref<V, shared>>; when V is an enum, Some(Num(v)) matches its variant through the
reference and binds v as a reference of that mode, so a lookup is taken apart in one match. A
literal compares through a reference too: Some(0) against an Option<ref<u32, shared>>. The
binder borrows what the reference borrows, so the map cannot change while v is in use.
import std;
enum Cell
{
Num(i64),
Text(String),
Empty,
}
fn width(Option<ref<Cell, shared>> c) : i64
{
match (c)
{
Some(Num(v)) : *v, // v : ref<i64, shared>
Some(Text(t)) : narrow<i64>(widen<u64>(String::len(t))),
Some(Empty) : 0,
None : -1,
}
}
fn main()
{
HashMap<u32, Cell> sheet = HashMap::new();
HashMap::insert(&mut sheet, 1, Num(12));
HashMap::insert(&mut sheet, 2, Text(String::from_str("total")));
assert(width(HashMap::get(&sheet, &1)) == 12 && width(HashMap::get(&sheet, &2)) == 5);
assert(width(HashMap::get(&sheet, &9)) == -1);
match (HashMap::get_mut(&mut sheet, &1))
{
Some(Num(v)) : { *v += 1; }, // v : ref<i64, exclusive>
_ : {},
}
assert(width(HashMap::get(&sheet, &1)) == 13);
}An arm that takes the payload consumes the enum
If the enum carries a resource payload, an arm that binds that payload by value
moves it into the binder and consumes the enum: if that arm ran, the scrutinee is stale
afterwards. This is what makes match (Vec::pop(&mut v)) { Some(x) : …, None : … } hand you the
popped element. An arm that takes nothing out — _, a payload-less variant, or Some(_) — leaves
the scrutinee alone, so match (o) { None : …, _ : use(o) } is fine. To look inside and keep the
enum whatever the arm, match on a reference to it (above).
import std;
enum Slot
{
Full(Vec<i32>),
Empty
}
fn main()
{
Vec<i32> payload = Vec::new();
Vec::push(&mut payload, 4);
auto s = Slot::Full(payload); // payload moves into the enum
usize n = match (s) // the Full arm consumes s
{
Full(v) : Vec::len(&v), // v owns the Vec for the arm, and destroys it at the arm's end
Empty : 0,
};
assert(n == 1);
Option<String> o = Some(String::from_str("kept"));
match (o)
{
None : {},
_ : // takes nothing out: o is still here
{
String t = Option::unwrap_or(o, String::new());
assert(String::len(&t) == 4);
},
}
}Numeric codes
An enum without payloads can carry a number per variant, for a file format, a protocol or
a C interface: enum Op : u8 { Add = 1, Sub, Mul = 10 }. The type after the colon is the code
type, any integer type. A variant without = n takes the previous code plus one (the first, 0).
Two variants may not share a code. The enum then has two associated functions:
Op::code(e) gives a variant's code, and Op::from_code(n) gives Some(variant) for a code
in use and None for any other.
import std;
enum Op : u8
{
Add = 1,
Sub, // 2
Mul = 10,
}
fn main()
{
assert(Op::code(Sub) == 2 && Op::code(Mul) == 10);
match (Op::from_code(10))
{
Some(Mul) : {},
_ : { assert(false); },
}
assert(Op::from_code(3) == None);
}Generic aggregates
struct Box<T> { T value; } and enum Maybe<T> { Just(T), Nothing } declare families of
types. std's Option<T> and Result<T, E> are ordinary enums of exactly this kind (§21).
Type arguments may be left off a literal when the declared type of the variable, parameter or field
fixes them (§12).
Layout
Struct fields are laid out in declaration order with implementation-documented padding;
arrays are contiguous with no padding; an enum is a discriminant of documented width followed by
the largest payload, aligned. sizeof<T>() and alignof<T>() report the result (examples in §06,
"Sizes and limits of types"). You need this mostly when talking to foreign code or managing raw
memory (§20).
- Reach for an enum whenever a value is one of several cases;
matchforces you to handle each one. - Use
_sparingly: a wildcard arm silently absorbs variants you add later. - Give match arms a block body when they need statements, and remember the comma after each arm.
- Build aggregates whole with a literal, then mutate fields as places.
§17Modules and name resolution
spec/17-modules.mdmodule, export and import; re-export; private by default; a module's body may be another file; a fixed lookup order for every name.
Modules group declarations and control which names are visible where. They introduce no new semantics: ownership, borrowing and every other rule apply unchanged across module boundaries.
Declaring modules
module name { … } encloses any declarations, including nested modules. Everything is
private by default: usable from its own module and the modules nested inside it. export
on a declaration makes it usable from outside. An exported item inside a private module is only
reachable where that module is.
Each declaration adds one qualified name (bank::fee, bank::Account::open,
Shape::Circle), and a name may be added only once, so declaring fee twice in bank is
diag.duplicate-item. The same simple name in two different modules is two different names. An
associated function fn T::name belongs in the module that declares T; anywhere else it is
diag.foreign-associated-fn, so no program can add to, or replace, the functions of std's
Vec.
module bank
{
export fn fee(i32 amount) : i32
{
percent(amount, 2) // a private helper is visible inside its own module
}
fn percent(i32 v, i32 pct) : i32
{
v * pct / 100
}
export module audit // nested modules; both must be exported to reach `twice`
{
export fn twice(i32 v) : i32
{
fee(v) * 2 // the enclosing module's items are in scope
}
}
}
fn main()
{
std::assert(bank::fee(1000) == 20); // qualified with ::
std::assert(bank::audit::twice(1000) == 40);
}module inner
{
fn helper() : i32 // not exported
{
42
}
}
fn main()
{
inner::helper(); // ✗ diag.name-not-visible
}import
import a::b::name; lets name be used unqualified in the importing module. It creates no
alias and no copy; it only extends name lookup. There is no import … as renaming: if two
imports would give one name two meanings, qualify.
module maths
{
export fn square(i32 v) : i32
{
v * v
}
}
import maths::square;
fn main()
{
std::assert(square(6) == 36); // resolves through the import
std::assert(maths::square(7) == 49); // the qualified form still works
}import m; naming a module does two things: m itself resolves, as with any import, and
every item m exports, including the variants of its exported enums, can be used unqualified, except
its submodules, which keep their paths (std::text) or are imported themselves. This
is how programs use the standard library: import std;. A name is looked up in a fixed order: the
module's own declarations first, then single-name imports, then module imports, then the
enclosing modules. So a module's own printf wins over std::printf, which stays reachable by its
full name. If two imported modules both export a name, using that name unqualified is
diag.ambiguous-name; importing both is fine, so a module that gains an export never breaks a
program that does not use it. An import acts only in the module that writes it: a nested module, or
a module in a file of its own, writes its own import std;.
import std;
module shapes
{
import std;
export enum Shape
{
Square(i32),
Dot,
}
export fn describe(Shape s)
{
match (s)
{
Square(_) : printf("square\n"),
Dot : printf("dot\n"),
}
}
}
import shapes;
fn printf(str s) // the root's own printf is found before std::printf
{
std::printf("> ");
std::printf("%v", s);
}
fn main()
{
describe(Square(2)); // shapes::describe and shapes::Shape::Square, unqualified
printf("done\n");
}square > done
Re-export: export import
An import can be exported like any other declaration. export import p; in a module M
does what import p; does, and also passes on what p brings in as M's own items: every item
the module p exports, or the one item p names. A program that imports M then finds those names
as if M had declared them, and M::name works too. They are the same items, not copies: a type
passed on this way is one type under two paths. This is how a library divided into modules is still
used with one import, and how std itself is built (§21): import std; brings in std::text,
std::io and the rest because std re-exports each of them.
import std;
module geometry
{
export module shapes
{
export struct Square
{
export i32 side;
}
export fn area(Square s) : i32
{
s.side * s.side
}
}
export module units
{
export fn cm(i32 n) : i32
{
n * 10
}
}
export import geometry::shapes; // shapes' items become geometry's
export import geometry::units::cm; // one item only
}
import geometry;
fn main()
{
Square s = Square { .side = cm(2) }; // geometry::shapes::Square, through geometry
printf("%v\n", area(s));
geometry::shapes::Square t = geometry::Square { .side = 1 }; // one type, two paths
printf("%v\n", area(t));
}400 1
Only what is exported passes through: a private item never becomes reachable by being
re-exported. A name the module already has for a different item is diag.duplicate-item; the same
item arriving twice, or two modules re-exporting each other, is not an error.
Constants as items
A const declared at a module's top level is an item like a function: it is private unless
marked export, it can be imported, and other modules name it by its path (config::LIMIT).
Constants themselves, and the static_assert checks built on them, are described with
declarations (§11) and with assertions (§18). The specification keeps their rules in its modules
chapter (spec/17 §1a and §1b), because a constant is an item.
A module in another file
A module's body can be a separate file. module geo "./geometry.cb"; means exactly
module geo { … } with the file's declarations as the body: the same visibility, the same
import, the same lookup. The declaring file names the module; the other file's top level is
anonymous, just as an inline body is. The path is relative to the directory of the file that
writes it, so a file can itself declare file-backed modules with paths relative to its own
location, and a library moves as a unit. Use forward slashes and stay inside the tree (no leading
/, no ..); anything beyond that is up to the implementation.
// geometry.cb -- the body of a module. Nothing here names the module;
// the file that declares it does. Only `export` items are visible there.
export struct Rect
{
export f64 w;
export f64 h;
}
export fn area(Rect r) : f64
{
r.w * r.h
}
fn scale_factor() : f64 // private to this module
{
1.0
}// main.cb -- run this one: `coby main.cb`
module geo "./geometry.cb"; // module geo { … } with geometry.cb's items as the body
import geo::Rect;
fn main()
{
Rect r = Rect { .w = 3.0, .h = 4.0 };
std::assert(geo::area(r) == 12.0); // qualified, as with any module
// geo::scale_factor(); // ✗ diag.name-not-visible: private, exactly as in one file
}Three things are rejected before anything runs: a path that names no file
(diag.module-file-not-found), a file whose body would load itself again through some chain of
declarations (diag.module-cycle), and one file named by two declarations
(diag.module-file-duplicate — it would be two distinct modules and two distinct Rect types).
A fn main in a loaded file is just the item geo::main; the program's entry is the root file's
main, and a program with none is diag.no-main. Both implementations report a diagnostic
inside a loaded file against that file and its own line.
A library in several files
Files and re-export (above) together give a library that is split across files but used with
one import. Its root file declares the parts, keeps its helpers in a private module, and passes on
what users should see: here every exported item of shapes, and cm alone from units. The
helpers are usable everywhere inside the library and nowhere outside it, and the files can be
reorganized later without changing any program that imports the root.
// geometry.cb -- the library's root: what `import geometry;` brings in
export module shapes "shapes.cb";
export module units "units.cb";
module detail "detail.cb"; // private: the library's own helpers
export import geometry::shapes; // every exported item of shapes
export import geometry::units::cm; // one item; units::mm keeps its path// shapes.cb
export struct Square
{
export i32 side;
}
export fn Square::new(i32 side) : Square
{
Square { .side = side }
}
export fn area(Square s) : i32
{
detail::square(s.side) // geometry's private module, visible inside geometry
}// units.cb
export fn cm(i32 n) : i32
{
n * 10
}
export fn mm(i32 n) : i32
{
n
}// detail.cb -- exported from a private module: reachable only inside geometry
export fn square(i32 v) : i32
{
v * v
}// main.cb -- run this one: `coby main.cb`
import std;
module geometry "geometry.cb";
import geometry;
fn main()
{
Square s = Square::new(cm(2)); // shapes and units, through one import
printf("%v\n", area(s));
printf("%v\n", geometry::units::mm(5)); // not passed on: written in full
// geometry::detail::square(3); // ✗ diag.name-not-visible: detail is private
}400 5
How a name is found
An unqualified name is first looked up as a local variable or parameter. Failing that, in
this order: an item of the current module; an import in the current module whose last segment
matches; an item of an enclosing module, outward to the root; finally a variant: first of the
enums of this module, or else of the nearest enclosing module with one of that name, and only if
there is none, of an imported enum. So your own enum Shape { …, Empty } keeps its bare Empty even
though std's ParseError has an Empty too. More than one candidate at a step is
diag.ambiguous-name; none at all is
diag.unbound-name. A qualified name a::b::c starts by resolving a the same way and then walks
into it, checking visibility at the end.
Two special cases: an associated function declared as fn Type::name(…) is addressed as
Type::name (or mod::Type::name from outside), and an enum's variants are addressable as
Enum::Variant as well as bare.
fn main()
{
does_not_exist(1); // ✗ diag.unbound-name
}Structs and fields across modules
A struct or enum declared in a module is an item like any other: export it to use it
outside. Each field has its own visibility too; write export i32 x; for fields that
callers may read and write directly, and leave the rest private to give the module full control
over its invariants.
module shapes
{
export struct Rect
{
export i32 w; // visible to users of the module
export i32 h;
i32 cached_area; // private: only this module's functions may touch it
}
}module shapes
{
export struct Rect
{
export i32 w;
i32 cached_area;
}
}
fn main()
{
auto r = shapes::Rect { .w = 1, .cached_area = 0 }; // ✗ diag.name-not-visible: cached_area is private
}Naming a private field from outside its module — reading it, writing it, giving it in a
struct literal, or destructuring it — is diag.name-not-visible, the same diagnostic as for a
private function. Give the module a constructor function if callers need to build the struct.
There are no read-only (const) fields: in CobaltC const means a value known before the
program runs (§11 constants), not "fixed once built". A field that other code may read but not
change is a private field with an exported function that reads it. Only the module can then change
it, which is also what keeps the module's invariant in one place.
import std;
module bank
{
export struct Account
{
export u32 id; // callers may read and write it
i64 balance; // private: changed only by this module
}
export fn Account::open(u32 id, i64 deposit) : Account
{
std::assert(deposit >= 0, "an account opens with a deposit, not a debt");
Account { .id = id, .balance = deposit }
}
export fn Account::balance(ref<Account, shared> a) : i64
{
a.balance
}
export fn Account::withdraw(ref<Account, exclusive> a, i64 amount) : bool
{
if (amount > a.balance)
{
return false; // the invariant: the balance never goes below zero
}
a.balance -= amount;
true
}
}
fn main()
{
auto acct = bank::Account::open(7, 100);
assert(bank::Account::balance(&acct) == 100); // read through the module
assert(!bank::Account::withdraw(&mut acct, 500));
assert(bank::Account::withdraw(&mut acct, 30));
assert(bank::Account::balance(&acct) == 70);
// acct.balance = 1000000; // ✗ diag.name-not-visible: only bank can change it
}- Start every module with everything private; export only what callers need.
- Prefer qualified calls (
maths::square(x)) in code that reads like a library;importthe handful of names you use constantly. - Module boundaries are not safety boundaries:
unsafeis lexical (§20). To confine trusted code, keep the functions that wrap it un-exported. - Split a program across files with
module m "./m.cb";, one file per module; keep paths relative and let the declaring file do the naming.
§18Errors and failure
spec/18-error-failure-semantics.mdFatal checked faults that unwind, assertions checked at run time or before the program runs, and recoverable Result values with the ? operator. Nothing in between.
There are exactly two ways for something to go wrong in CobaltC, and they never mix. A
checked fault is the language catching a violated invariant (overflow, an index out
of range, a stale reference, a conflicting borrow): it is fatal. An error value is
your program reporting an expected failure (bad input, a full buffer) through Result or Option:
it is data, and the caller decides what to do. There are no exceptions and no catch.
Checked faults
When a run-time check fails, the current thread unwinds every open scope, running every destructor exactly as a normal scope exit would, and the program terminates reporting the fault by name. Nothing can intercept this within a thread. (A fault in a thread you spawned ends that thread alone, and its failure becomes a value at the thread's boundary: §19, When a thread fails.) The point is that a fault cannot corrupt state or leak an external resource on the way out, and that a program that reaches its end has never once violated a checked rule.
import std;
resource struct Guarded
{
i32 tag;
}
fn Guarded::drop(ref<Guarded, exclusive> self)
{
// runs during unwinding, before the program terminates
}
fn divide(i32 a, i32 b) : i32
{
a / b
}
fn main()
{
Guarded g = Guarded { .tag = 1 };
Vec<i32> v = Vec::new();
divide(1, 0); // ✗ diag.div-by-zero (dynamic): v and g are destroyed, then the program ends
}Assertions: assert
assert(c) states something the program relies on at that point: when c is false, the
program stops with the checked fault diag.assert-failed, naming the line, and unwinds like any
other fault. assert(c, "format", args…) adds a message, formatted as printf formats, so the
failure can say which values were wrong. The condition is always evaluated, exactly once: there is
no build in which assertions are left out. The message, and its arguments, are only evaluated when
the condition is false, so they cost nothing while it holds. Every example in this guide checks its
own results with assert; a condition that is constant is checked with static_assert (below)
before the program runs.
import std;
fn average(ref<Vec<u32>, shared> v) : u32
{
assert(Vec::len(v) > 0, "average of an empty list");
u32 sum = 0;
foreach (x in v)
{
sum += *x;
}
sum / narrow<u32>(Vec::len(v))
}
fn main()
{
Vec<u32> v = Vec::new();
Vec::push(&mut v, 4);
Vec::push(&mut v, 8);
assert(average(&v) == 6);
usize cap = 1;
assert(Vec::len(&v) <= cap, "%v items exceed the capacity %v", Vec::len(&v), cap);
// ✗ diag.assert-failed (dynamic), reporting "2 items exceed the capacity 1"
}Faults are named diag.… and listed in the appendix. Both the interpreter and a compiled
program print the name, the rule that fired, what was required and what was observed, and a repair
hint.
Static assertions
static_assert(c) and static_assert(c, "why") check a constant condition when the program
is compiled, not when it runs: a false one stops it with diag.static-assert-failed and shows the
message. As with assert, the message may carry values, static_assert(D == 201, "D is %v", D),
provided they are constants too. The condition is a constant expression (the same kind a const has, §11), so it can use
constants, sizeof, alignof, the limits and the conversions. It is checked in every function,
called or not, and in a generic function once for each type it is used with, so
static_assert(sizeof<T>() <= 8, "…") limits what T may be. At run time it does nothing. For a
condition only known as the program runs, use assert (above): a static_assert whose condition
names a local variable, calls a function or is not a bool is rejected before the program runs,
with diag.static-assert-not-constant, which says so.
import std;
struct Header
{
u32 magic;
u16 version;
u16 flags;
u64 length;
}
const usize BUF = 4096;
// Stores a value that must fit in one machine word.
fn word<T>(T x) : T
{
static_assert(sizeof<T>() <= 8, "word<T> needs T to fit in 8 bytes");
x
}
fn main()
{
static_assert(sizeof<Header>() == 16, "Header must match the C struct");
static_assert(BUF & (BUF - 1) == 0, "BUF must be a power of two");
assert(word(5: i64) == 5 && word(2.5) == 2.5);
}import std;
fn main()
{
i32 x = 1;
static_assert(x == 1); // ✗ diag.static-assert-not-constant: x is a variable;
// write assert(x == 1), or const i32 X = 1;
}fn word<T>(T x) : T
{
static_assert(sizeof<T>() <= 8, "word<T> needs T to fit in 8 bytes");
x
}
fn main()
{
word(1: i128); // ✗ diag.static-assert-failed (static): an i128 is 16 bytes
}Result, Option and ?
Result<T, E> is Ok(T) or Err(E); Option<T> is Some(T) or None. Both are plain
enums of std, taken apart with match. The postfix ? operator unwraps a Result: on Ok(v) it
yields v, on Err(e) it returns Err(e) from the enclosing function immediately. That function
must therefore return Result<_, E> with exactly the same E; convert first with
Result::map_err(r, f) if the error types differ. On an Option, ? works the same way inside a
function that itself returns an Option: Some(v) gives v, None returns None at once, so
usize end = checked_add(start, n)?; reads "the sum, or give up" (D-0130). Nothing converts between
the two: in a function returning a Result, Option::ok_or(o, e) turns the Option into a
Result — Ok(v) from Some(v), Err(e) from None — so
u8 c = Option::ok_or(peek(p), fail(p, "expected a value"))?; reads "the byte, or return that
error". Where a default will do, Option::unwrap_or(o, d) and
Result::unwrap_or(r, d) give the value, or d when there is none; the default (and ok_or's
error) is evaluated either way, and one that goes unused is destroyed. Result::ok(r) turns a
Result into an Option, dropping the error. Where there must be a value,
Option::unwrap(o) and Result::unwrap(r) give it or fault with diag.unwrap-failed, and
Option::expect(o, "…") and Result::expect(r, "…") do the same with your message.
import std;
fn main()
{
Vec<String> names = Vec::new();
Vec::push(&mut names, String::from_str("ada"));
Vec<String> copy = Vec::clone(&names); // each String copied by its clone (§12)
String first = Option::expect(Vec::pop(&mut copy), "the copy has one name");
first = String::from_str("grace"); // a String is quiet: the old one is destroyed
u32 n = Result::unwrap(String::parse<u32>(&String::from_str("42")));
assert(String::eq_str(&first, "grace") && n == 42 && Vec::len(&names) == 1);
Option<u32> none = None;
u32 fails = Option::expect(none, "no value where one was promised");
// ✗ diag.unwrap-failed (dynamic), reporting "no value where one was promised"
}import std;
fn digit(u8 b) : Result<i32, u8>
{
if (b > b'9' || b < b'0') // not a digit
{
return Err(b);
}
Ok(widen<i32>(b - b'0'))
}
fn two_digits(u8 hi, u8 lo) : Result<i32, u8>
{
i32 h = digit(hi)?; // Err returns from two_digits right here
i32 l = digit(lo)?;
Ok(h * 10 + l)
}
fn main()
{
match (two_digits(b'4', b'2'))
{
Ok(v) :
{
assert(v == 42);
},
Err(_) :
{
assert(false, "unreachable");
},
}
match (two_digits(b'4', b'x'))
{
Ok(_) :
{
assert(false, "unreachable");
},
Err(e) :
{
assert(e == b'x');
},
}
}import std;
fn f() : i32
{
Result<i32, i32> r = Ok(1);
r? // ✗ diag.propagate-outside-fallible-context: f does not return Result<_, i32>
}
fn main()
{
f();
}Because ? is just a match in disguise, it consumes a resource-bearing Result the
same way a match would, and the early return runs destructors like any other.
A Result is never dropped silently: an expression statement whose value is a
Result, such as a bare call write_all(&f, &data);, is diag.result-discarded, since the error it
may carry would vanish. Handle it (?, match, unwrap), or say that you mean to ignore it with
_ = e;, which evaluates e and destroys the value at once. _ = e; works for any value.
import std;
fn check(i32 n) : Result<i32, str>
{
if (n < 0) { Err("negative") } else { Ok(n) }
}
fn main()
{
_ = check(-1); // ignored on purpose
assert(Result::unwrap(check(2)) == 2);
}import std;
fn check(i32 n) : Result<i32, str>
{
if (n < 0) { Err("negative") } else { Ok(n) }
}
fn main()
{
check(-1); // ✗ diag.result-discarded: the error would vanish unseen
}Choosing a channel
| Situation | Use |
|---|---|
| Input that may legitimately be malformed, a container that may be empty, an allocation that may fail | Result / Option; let the caller decide |
| A condition that can only be false if the program is wrong (an index you computed, arithmetic on trusted values) | let the language's checked fault fire; do not wrap it |
| Recovering from overflow rather than faulting | checked_add and friends return Option (§06) |
| Recovering from allocation failure | call allocate yourself; Vec and Rc treat failure as a fault |
- Reserve faults for bugs. If a failure is part of normal operation, return a
Result. - Use
?to keep the happy path readable; keep error types consistent within a call chain andmap_errat the boundaries. - Trust the unwinding: a fault runs your destructors, so external handles are released even on the way down.
§19Concurrency
spec/19-concurrency.mdThreads owned by handles, a fault contained to its thread and handed over as a value, cancellation at any wait, deadlocks reported, mutexes reachable only through guards, channels, and the same aliasing rules everywhere.
CobaltC's concurrency is small: threads you start and must join, and a mutex whose contents can only be reached by locking it. The aliasing rules of §08 already forbid two threads from touching one object in conflicting ways, so most of the work is done before this section starts. What the primitives add is a way to share mutable state on purpose, safely.
Threads: spawn and join
spawn(f, args…) runs f(args…) on a new thread and returns a handle<R>, where R is
f's return type. f must be a named function or a move closure (a closure borrowing locals of
the spawning thread is rejected: diag.spawn-borrow-closure). Arguments are passed exactly as for a
call: plain values are copied, resources move to the new thread, references are lent. join(h)
waits for the thread and returns its result, consuming the handle. The result may itself be a
reference (or carry one, in a struct or an Option): the handle then holds the borrow until the
join hands it over, so the source cannot be written meanwhile. A handle is a resource: if it is
still alive when its scope ends, the thread is joined then and its result discarded, so a thread
never outlives its handle. What a thread borrows is still checked at every use, as in any call: if
the referent ends first (a handle kept in a variable declared outside the borrowed local's block,
or an element released by a reallocating Vec::push), the thread's next use of it is
diag.stale-binding.
import std;
fn work(i32 n) : i32
{
n * 2
}
fn main()
{
auto h = spawn(work, 21); // the handle owns the thread
assert(join(h) == 42); // wait and take the result
Vec<i32> data = Vec::new();
Vec::push(&mut data, 5);
auto h2 = spawn(move [data]() // a move closure carries the Vec to the other thread
{
Vec::len(&data)
});
assert(join(h2) == 1);
{
auto h3 = spawn(work, 1);
} // unjoined: joined automatically here, result discarded
}fn main()
{
i32 x = 5;
auto h = spawn([x](i32 n) // ✗ diag.spawn-borrow-closure: the closure borrows x
{
n + x
}, 1);
join(h);
}A reference passed to spawn is held by the new thread until it finishes, so the
spawning thread may not use the referent in a conflicting way before join(h). Whether the thread
happened to finish first would otherwise decide the outcome; the checker rejects the program
instead.
fn read(ref<i32, shared> r) : i32
{
*r
}
fn main()
{
i32 x = 1;
auto h = spawn(read, &x); // the thread borrows x until join(h)
i32 y = x; // ✓ reading alongside a shared borrow
x = 2; // ✗ diag.aliasing-conflict (static): the thread may still be reading x
join(h);
}Sharing state: mutex and guard
Mutex::new(v) wraps a value in a mutex<T>. The value inside is reachable only through
lock(&m), which returns a guard<T>: an exclusive path into the interior that exists for as long
as the guard does. *g is the place inside. The guard is a resource; dropping it, or letting its
scope end, unlocks the mutex. Only one guard can exist at a time; another thread's lock waits.
Locking a mutex your own thread already holds is a fault, diag.mutex-reentrant-lock, not a
deadlock.
A mutex is shared between threads by passing &m, a ref<mutex<T>, shared>. Any number
of threads may hold that shared reference; it is the one case where an exclusive path (the lock
guard) is allowed to coexist with shared references to the same object, because the lock itself
serialises access.
import std;
fn worker(ref<mutex<i32>, shared> m, i32 n) : i32
{
foreach (i in 0..n)
{
auto g = lock(m); // blocks until the mutex is free
*g += 1;
} // each iteration's guard is destroyed here, unlocking
n
}
fn main()
{
auto m = Mutex::new(0);
auto h1 = spawn(worker, &m, 3); // both threads hold a shared reference to m
auto h2 = spawn(worker, &m, 4);
i32 a = join(h1);
i32 b = join(h2);
assert(a + b == 7);
i32 total = *lock(&m); // a temporary guard: it lives until the semicolon
assert(total == 7); // whatever the interleaving, every increment was serialised
}fn main()
{
auto m = Mutex::new(0);
auto g1 = lock(&m);
auto g2 = lock(&m); // ✗ diag.mutex-reentrant-lock: this thread already holds it
}Through a guard, a field or element of the locked value is reached directly, as through a
reference: g.count, g.items[0], Vec::push(&mut g.items, x) mean (*g).count and so on. *g
is how you name the whole value.
A mutex may also be a field: a struct holding one or more mutexes is shared by
reference like a bare one, each thread locking the field it needs (lock(&board.scores)). Each
mutex field is its own lock.
import std;
struct Stats
{
u32 hits;
Vec<u32> seen;
}
struct Board
{
mutex<Stats> stats;
mutex<u32> rounds;
}
fn play(ref<Board, shared> b, u32 me) : u32
{
{
auto g = lock(&b.stats); // one mutex field...
g.hits += 1; // g.hits is (*g).hits
Vec::push(&mut g.seen, me);
}
auto r = lock(&b.rounds); // ...and another, locked independently
*r += 1;
me
}
fn main()
{
Board b = Board { .stats = Mutex::new(Stats { .hits = 0, .seen = Vec::new() }), .rounds = Mutex::new(0: u32) };
{
auto x = spawn(play, &b, 1);
auto y = spawn(play, &b, 2);
assert(join(x) + join(y) == 3);
}
auto g = lock(&b.stats);
assert(g.hits == 2 && Vec::len(&g.seen) == 2 && *lock(&b.rounds) == 2);
}Passing values: Channel
A Channel<T> from std (std::sync) is a queue between threads, shared by reference as a mutex is.
Channel::new(cap) holds at most cap values (at least 1). Channel::send(&ch, v) moves v in,
waiting while the channel is full; Channel::recv(&ch) moves the oldest value out as Some(v),
waiting while it is empty. A value in a channel has one owner, the channel, so a String or a
Vec travels between threads without being shared or copied. A waiting thread uses no processor
time.
Channel::close(&ch) says nothing more is coming: the values already sent are still
received, and then recv gives None, which is what ends a receiver's loop. A send after
close does not lose its value: it gives it back as Err(v). Destroying a channel destroys the
values left in it, oldest first. The usual shape is a pool of workers: the main thread sends the
jobs, closes the channel, and joins the workers, each of which stops at None.
import std;
fn worker(ref<Channel<u64>, shared> jobs, ref<Channel<u64>, shared> results)
{
while (true)
{
match (Channel::recv(jobs))
{
Some(n) : Channel::send(results, n * n),
None : break, // closed and empty: this worker is done
}
}
}
fn main()
{
Channel<u64> jobs = Channel::new(4); // at most 4 jobs waiting at once
Channel<u64> results = Channel::new(16);
{
auto a = spawn(worker, &jobs, &results);
auto b = spawn(worker, &jobs, &results);
foreach (i in 1..11: u64)
{
_ = Channel::send(&jobs, i); // waits while the channel is full
}
Channel::close(&jobs); // the workers stop once it is empty
join(a);
join(b);
}
Channel::close(&results);
u64 total = 0;
while (Some(sq) = Channel::recv(&results)) // None once it is closed and empty
{
total += sq;
}
assert(total == 385); // 1 + 4 + … + 100, whichever worker did which
Channel<String> names = Channel::new(1);
Channel::close(&names);
match (Channel::send(&names, String::from_str("late")))
{
Ok(_) : assert(false, "unreachable"),
Err(s) : assert(String::len(&s) == 4), // closed: the value comes back
}
}A wait that can never end because no other thread exists — the main thread receiving
from an empty channel, or sending to a full one, with no spawned thread running — is a fault,
diag.channel-deadlock. Other deadlocks are not detected.
import std;
fn main()
{
Channel<i32> ch = Channel::new(1);
_ = Channel::send(&ch, 1);
_ = Channel::send(&ch, 2); // ✗ diag.channel-deadlock: full, and no one to receive
}Without waiting for ever (D-0185). Channel::try_recv(&ch) answers at once:
Ok(Some(v)) a value, Ok(None) nothing there, Err(()) closed and empty — three answers, because a
loop that also watches a socket or interrupt_requested() must tell "nothing yet" from "the
senders are done". Channel::recv_timeout_ms(&ch, ms) waits at most ms milliseconds for the same
three answers. cpu_count() is how many threads the machine runs at once, the size to give a pool.
How threads run
Every CobaltC thread is a thread of the operating system. In a program built by cobc
they run at once, on as many cores as the machine has. The checks of §08 and §10 still run as the
program goes, in every thread, and two threads that use the same object meet in them: that is how
a conflict between threads is caught at the access that would have raced.
To keep threads from slowing each other down, the run-time checks keep each thread's records apart. What a thread makes, and what is moved into it, are its own; most checks on them involve no other thread. Where threads do meet:
- Data reached through a reference to another thread's (
spawn(f, &v)) stays the owner's, and each access through it is checked against the owner's records too. It works, and costs a little more on every access than data the thread owns. - Waiting — on
join, on alocksomeone else holds, on a full or emptyChannel— stops only the waiting thread, which spins briefly before it sleeps: most waits for a lock are short. Locking, sending and receiving involve no other thread's checks. - A block that only reads and writes through its guard (
{ auto g = lock(&m); *g += 1; }, plain data inside, no earlyreturn) costs a lock and an unlock and nothing more:cobcmakes no guard object for it. - Running code of the program on a value's end — a destructor you wrote, a
fnvalue, aBox— briefly pauses the other threads' checks. Everything else the runtime does at a block's end stays within the threads involved.
So the program that runs fastest in parallel gives each thread its work: move a chunk of the
data into the thread (an argument it takes by value, which then belongs to it) and hand the result
back through join or a Channel. The performance chapter's Threads
section has the figures.
import std;
fn total(Vec<u64> chunk) : u64
{
u64 t = 0;
foreach (x in &chunk)
{
t += *x;
}
t
}
fn main()
{
Vec<handle<u64>> hs = Vec::new();
foreach (k in 0..4: u64)
{
Vec<u64> chunk = Vec::new(); // made here, then moved into its thread
foreach (i in 0..1000: u64)
{
Vec::push(&mut chunk, k * 1000 + i);
}
Vec::push(&mut hs, spawn(total, chunk)); // `chunk` now belongs to that thread
}
u64 sum = 0;
while (Vec::len(&hs) > 0)
{
sum += join(Option::unwrap(Vec::pop(&mut hs)));
}
assert(sum == 7998000); // 0 + 1 + … + 3999
}The guarantee
What a program without unsafe can rely on, however its threads are timed (rule.conc.guarantee):
- No data races. Two threads never touch the same storage at once with one of them writing. Either something orders the two accesses (a
join, a mutex's lock, a value sent and received), or the second access isdiag.aliasing-conflict. - One step at a time. Every run is some interleaving of the threads' steps, each step whole: a thread never sees half of another's write, and there is no reordering to reason about.
- Nothing undefined. An access that is not allowed is a diagnostic, in every interleaving.
A reference passed to spawn belongs to the new thread from the moment of the spawn, until
the thread ends, even before it has run a single step. Timing can still choose between two permitted
outcomes: a thread that has already finished holds nothing, so an access that would have conflicted
with it while it ran is fine afterwards. Use join (or a channel) to make the order part of the
program. Deadlock is not ruled out, but it is reported rather than left to hang (below).
How the guarantee is checked: beyond the tests of the two implementations, a separate program,
cobmodel (impl/model), runs a fragment of the language straight from the rules, with every
interleaving of its threads, and lists every outcome they allow. Tens of thousands of generated
programs about references, threads, mutexes, channels and vectors of references have been compared
with what coby and cobc --explore do. This is evidence, not a proof: it covers that fragment, up to
the program sizes it tried.
import std;
fn bump(ref<i64, exclusive> x, ref<Channel<u8>, shared> go)
{
_ = Channel::recv(go); // waits, holding x
*x += 1;
}
fn main()
{
Vec<i64> v = Vec::filled(4, 0: i64);
Channel<u8> go = Channel::new(1);
{
auto h = spawn(bump, &mut v[0], &go);
i64 other = v[1]; // fine: another element
i64 same = v[0]; // ✗ diag.aliasing-conflict: the thread holds v[0]
_ = Channel::send(&go, 1);
join(h);
}
}An exclusive reference belongs to one thread. Copying a ref<T, exclusive>
copies one path, and two copies in two threads would be two writers no check could tell apart. So
spawn and Channel::send lend each exclusive reference they hand over: the thread gets a path
of its own, borrowed from yours, and your copy conflicts with it until that thread is done with it.
Lending the same reference to two threads at once is diag.aliasing-conflict, and so is writing through
your copy while it is lent. A thread may use an exclusive reference only if it was handed it this way:
one it finds inside a struct it was lent, inside a Vec moved to it, or inside a mutex belongs to the
thread that made it, and using it faults while that thread runs. To give a thread exclusive access to
something, pass the reference to spawn or send it through a channel.
fn bump(ref<i32, exclusive> a)
{
*a += 1;
}
fn main()
{
i32 x = 0;
ref<i32, exclusive> r = &mut x;
auto h1 = spawn(bump, r); // r is lent to this thread until join(h1)
auto h2 = spawn(bump, r); // ✗ diag.aliasing-conflict: r is lent already
join(h1);
join(h2);
}When a thread fails
A checked fault in a thread you spawned ends that thread: it unwinds, its destructors run, the threads it started are stopped and waited for (cancelled, then joined; what they report, their own fault or the cancellation, is discarded), and its result becomes the failure — its own, never one of theirs. The rest of the program goes on. This is what lets a server give each connection a thread and survive a bug in one of them. Nothing continues in a state the failed check protected:
- A mutex the thread held is poisoned. Its interior may be half-changed, so the next
lockof it faults withdiag.mutex-poisoned. - A thread lent an exclusive reference is not contained. Given one at
spawn, or by locking a mutex whose contents can hold one (receiving from aChannelof&mutreferences, say), it may be changing data that belongs to a thread still running, so its fault ends the program at once, as a fault always did. - The main thread's fault ends the program, as before.
Nobody has to ask for this. join(h) of a failed thread raises its failure again in the joiner,
and so does the handle's end if nobody joined it: a program that never looks at failures still fails
as a whole, one scope at a time. To take a failure as a value, use try_join(h), which returns
Result<R, ThreadFailure>. ThreadFailure::text is the report the program would have printed,
diagnostic its diag.… name. is_finished(&h) says, without waiting, whether a thread has ended,
so one loop can accept new work and collect the threads that are done.
import std;
fn handler(usize i) : u64
{
Vec<u64> v = Vec::filled(3, 7: u64);
v[i] // a bug when i >= 3
}
fn main()
{
usize served = 0;
usize failed = 0;
foreach (i in 0..5)
{
match (try_join(spawn(handler, i)))
{
Ok(_) : served += 1,
Err(f) :
{
String d = ThreadFailure::diagnostic(&f);
assert(String::as_view(&d) == "diag.index-out-of-bounds");
failed += 1;
},
}
}
assert(served == 3 && failed == 2); // the program went on after both
}import std;
fn handler(usize i) : u64
{
Vec<u64> v = Vec::new();
v[i]
}
fn main()
{
auto h = spawn(handler, 0);
join(h); // ✗ diag.index-out-of-bounds (dynamic): raised again here
}import std;
fn update(ref<mutex<i64>, shared> m, usize i)
{
auto g = lock(m);
*g += 1;
Vec<i64> v = Vec::new();
*g += v[i]; // faults while holding the lock
}
fn main()
{
mutex<i64> m = Mutex::new(0: i64);
_ = try_join(spawn(update, &m, 0));
auto g = lock(&m); // ✗ diag.mutex-poisoned (dynamic): *g may be half-updated
}Stopping a thread: cancel
cancel(&h) asks a thread to stop. Its current wait, or its next one, ends with the fault
diag.thread-cancelled, contained like any other: the thread unwinds and its result is that
failure. The waits are join, lock, a channel's send and recv, sleep_ms, and every socket
operation, so a handler blocked reading a client that never writes can be stopped. A thread that
never waits finishes its work, and one inside a call outside the program (input, a file, a child
process) is stopped at its next wait. A thread cancelled while it holds a guard unwinds like a
faulting one: the mutex is poisoned, since what it was changing may be half-changed. ThreadFailure::is_cancelled tells a cancellation from a bug, and a
handle whose thread you cancelled raises nothing when it ends. Together with a channel this is
"first answer wins" without a select: start one thread per source, all sending to one channel,
take the first value, and cancel the rest.
import std;
fn listen(ref<Channel<u8>, shared> c) : u8
{
Option::unwrap_or(Channel::recv(c), 0) // nobody ever sends
}
fn main()
{
Channel<u8> c = Channel::new(1);
auto h = spawn(listen, &c);
cancel(&h); // its wait ends at once
match (try_join(h))
{
Ok(_) : assert(false),
Err(f) : assert(ThreadFailure::is_cancelled(&f)),
}
}Deadlock
A wait that can never end is diag.deadlock, reported where it happens. A thread whose
join or lock would close a cycle (it waits on a thread that, through holders and joins, waits on
it) faults at once. And when every thread is waiting on another thread, a mutex or a channel, with
none on the clock, a socket or input, the runtime faults one thread of a cycle, or, if there is no
cycle, the threads at the ends of the chains of waits (those waiting on a channel); the threads
waiting on them then get the failure through join, or find the mutex poisoned. The result does
not depend on timing. A channel wait while some thread is in a socket call or asleep is never
called a deadlock: that thread may send when its call returns. Give such a wait a limit
(recv_timeout_ms) or cancel it from a supervisor. Like any fault in a spawned thread it is contained, so two deadlocked handlers in
a server are reported and the server goes on. (The main thread waiting alone on a channel is still
diag.channel-deadlock.)
import std;
fn waits(ref<Channel<u8>, shared> c)
{
_ = Channel::recv(c); // nobody will send
}
fn main()
{
Channel<u8> c = Channel::new(1);
auto h = spawn(waits, &c);
join(h); // ✗ diag.deadlock (dynamic): every thread waits, none can go on
}Exploring schedules: --explore
A result that depends on timing is easy to miss: the timing you test is rarely the one that
goes wrong. coby --explore N prog.cb runs the program N times, each under a different order of its
threads chosen by a seed, and groups the runs by outcome (output and exit status). One outcome is
what you want; more than one means the program's result depends on timing, and coby exits with 1.
Each outcome comes with a seed, and coby --schedule-seed S prog.cb replays that run exactly, as
often as you need to find out why. Under a schedule the threads take turns one at a time, so
exploration needs no cores and finds orders a parallel run would rarely produce.
cobc --explore N prog.cb does the same for the compiled program: it compiles once and runs the
executable N times. There a thread keeps its turn between the points where threads interact -- a
spawn, a wait, a mutex released, a call outside the program -- and now and then between two of the
program's checks, which is where a program whose checks all pass can end differently.
cobc --schedule-seed S prog.cb replays one run, and so does setting COBALTC_SCHEDULE_SEED=S in a
compiled program's environment. In either tool, a thread that is asleep or in a socket or input call
leaves the schedule until the call returns, so a program that talks to the network explores only
the orders around those calls.
import std;
fn bump(ref<mutex<i64>, shared> m)
{
i64 seen = 0;
{
auto g = lock(m);
seen = *g;
}
auto g = lock(m);
*g = seen + 1; // a lost update when the other thread ran in between
}
fn main()
{
mutex<i64> m = Mutex::new(0: i64);
{
auto a = spawn(bump, &m);
auto b = spawn(bump, &m);
}
auto g = lock(&m);
printf("total %v
", *g);
}$ coby --explore 50 counter.cb
explored 50 schedules of counter.cb: 2 distinct outcomes
19 runs, exit 0 -- replay: coby --schedule-seed 1 counter.cb
| total 1
31 runs, exit 0 -- replay: coby --schedule-seed 2 counter.cb
| total 2Data parallelism: slice_parts
slice_parts(&mut v[0..$], k) splits a vector (or any exclusive slice) into k consecutive
runs as nearly equal as they can be, each an exclusive slice of its own. Give each run to a thread
and the threads write one vector at once, with no lock: the runs are disjoint, and the checker
proves it the way it proves any two ranges apart. The vector is usable again once every thread is
joined. A thread takes a named function here, not a closure: a closure is passed by move (§15), so it
goes to one thread, and one closure cannot run in several threads at once.
import std;
fn square_all(slice<u64, exclusive> s)
{
for (usize i = 0; i < slice_len(s); i += 1)
{
s[i] = s[i] * s[i];
}
}
fn main()
{
Vec<u64> v = Vec::new();
foreach (i in 0..1000: u64)
{
Vec::push(&mut v, i);
}
{
Vec<slice<u64, exclusive>> parts = slice_parts(&mut v[0..$], cpu_count());
Vec<handle<void>> hs = Vec::new();
while (Vec::len(&parts) > 0)
{
Vec::push(&mut hs, spawn(square_all, Vec::swap_remove(&mut parts, 0)));
}
foreach (h in hs)
{
join(h);
}
}
assert(v[999] == 998001);
}What is not provided
No atomics, no memory orderings, no condition variables, no thread-local storage. Steps of different threads interleave at
the granularity of single operations and every operation sees a consistent state; a join, a
lock, a send or a recv that cannot proceed simply waits. Rc<T> is not thread-safe; to share
across threads, share a mutex or pass values through a Channel.
cobc runs threads in parallel on separate cores (above); the interpreter coby
runs them one at a time. A program that follows the rules gets the same result from both. Where the
language leaves an outcome to timing — writing a variable while another thread may still be reading
it through a reference is diag.aliasing-conflict if the thread is still running, and fine if it has
finished — each is free to show either permitted outcome, and they may differ.- Every thread has an owner: the handle. Join it explicitly to get the result; let scope end join it if you do not care.
- Supervise threads that may fail:
try_jointakes the failure as a value;cancelthe ones you no longer need. - Lend a thread exclusive access to your data only when its failure should end the program; otherwise move the data in.
- Share a
mutexby shared reference; keep each guard in the smallest block that needs it. - To hand values from thread to thread, send them through a
Channel;closeit when the last value is sent. - Move data into a thread with a
moveclosure or by-value arguments; lend it with references only when the mutex rule allows.
§20Unsafe, raw pointers and FFI
spec/20-trust-boundaries.mdunsafe blocks record what you assert; rawptr, reclaim, allocate and extern fn are the only doors to the outside.
Everything so far is checked. This section is about the doors to the outside world:
raw addresses, manual allocation and foreign functions. CobaltC does not pretend those can be
checked; instead it makes the boundary explicit. Each trusted operation is only legal inside an
unsafe { } block, and the block is your signed statement that the operation's preconditions
hold.
unsafe blocks
unsafe { … } is an ordinary block (it has a value, it opens a scope) with one extra
permission: the trusted operations listed below may appear inside it. Outside, they are rejected
with diag.trusted-outside-unsafe. Nothing else changes: the aliasing, ownership and initialisation
rules still apply to every ordinary access inside an unsafe block.
| Operation | What you assert |
|---|---|
*p (read) / *p = v (write) for p : rawptr<T> | p addresses sizeof<T>() initialised bytes of a T, no live reference reaches them, and no other thread is writing them |
p + n (pointer offset, n : isize) | the result stays within, or one past the end of, one allocation or object |
reclaim<T>(p) | the bytes at p hold a valid T that no ordinary object owns; you now treat them as an object |
release(p, n), copy_raw(dst, src, n), deallocate(p, n, align) | the ranges are valid and nothing live overlaps them; deallocate matches one earlier allocate |
an unsafe extern fn declaration | the linked code has a function of this name with exactly this signature |
a call to an unsafe extern fn | the foreign code honours its signature and touches only what the pointer arguments reach |
Who asserts what
The standard library's own bodies use unsafe: Vec::push writes an element through a raw
pointer, File::write calls the operating system, Rc::clone reclaims its box. Those blocks are not
yours. They belong to the implementation, which must keep every one of them true to conform, and the
specification lists each of them with the fact that makes it hold (spec/21 §0, "What std asserts":
for Vec::push, that len < cap after grow and that no element object was ever established in the
slot). So a program that writes no unsafe of its own has the language's guarantee unconditionally,
and the guarantee is lost only by a false claim in a block the program itself wrote.
Raw pointers
rawptr<T> is an address. It has no mode, no lifetime, and no checks; it is plain and
copyable. Getting one is safe: rawptr_of(&x) (or rawptr_of(&mut x)) takes the address of a
place, and reinterpret_ptr<U>(p) changes the pointee type without changing the address. Using one
to reach memory is trusted.
import std;
fn read_through(rawptr<i32> p) : i32
{
unsafe
{
*p // trusted: the caller passed a pointer to a live i32
}
}
fn main()
{
i32 x = 7;
assert(read_through(rawptr_of(&x)) == 7);
auto bytes = [1: u8, 2: u8, 3: u8];
rawptr<u8> b = reinterpret_ptr<u8>(rawptr_of(&bytes));
unsafe
{
assert(*(b + 2) == 3); // pointer offset, in units of the pointee size
}
}fn bad(rawptr<i32> p) : i32
{
*p // ✗ diag.trusted-outside-unsafe
}
fn main()
{
i32 x = 1;
bad(rawptr_of(&x));
}From raw bytes back to objects: reclaim
Raw memory is not an object until you say it is. reclaim<T>(p) is a place
expression: it establishes (or re-attaches to) an object of type T over the bytes at p, and
from then on every ordinary rule applies to it. You can borrow it (&reclaim<T>(p)), assign to
it, or drop it. Reclaiming the same address twice gives the same object, so two references
obtained that way are checked against each other like any two references. This is how Vec
hands out references to its elements.
import std;
fn main()
{
Vec<i32> v = Vec::new();
Vec::push(&mut v, 7);
auto p = rawptr_of(&v[0]); // the element's address; obtaining it is safe
unsafe
{
auto r = &reclaim<i32>(p); // a checked reference to the element, via its address
assert(*r == 7);
}
}Manual allocation
allocate(n, align) returns Result<rawptr<u8>, AllocError>: fresh uninitialised bytes,
or Err. It is safe to call (failure is a value). deallocate(p, n, align) gives them back and is
trusted. A resource written into raw memory with *p = v moves there and is tracked; reading
*p of a resource type moves it back out. Plain data that holds references is tracked too: the cells
it is written to hold those references, exactly as a variable would, until they are released. The
library's Vec::grow in §21 is the reference example of the whole pattern.
When a promise is false
An unsafe block's promises are not checked; that is what makes them promises. If one is
false — *p reads bytes that are not a valid T, reclaim claims cells another object owns, a
foreign function writes past what it was given — the program has left the language's rules at that
step. Everything it did before that step happened exactly as the rules say. From that step on, no
rule says what happens: the program may carry on, print nonsense, fault, or crash, and every one of
those is allowed. None of the guarantees in this guide — no use after free, no data race, no
out-of-bounds access — covers what comes after. A program with no unsafe block makes no promises,
so they cover all of it.
struct Holder
{
Vec<i32> v;
}
fn main()
{
Holder h = Holder { .v = Vec::new() };
rawptr<Vec<i32>> p = reinterpret_ptr<Vec<i32>>(rawptr_of(&h));
unsafe
{
auto taken = *p; // false promise: h still owns these bytes
}
h.v = Vec::new(); // from the line above on, no rule applies
}The tools do not have to notice a broken promise, and in general they cannot. When one does,
it is encouraged to stop with a diagnostic rather than crash, since a report names the problem and a
crash does not; but that is a courtesy, not a rule. Keep unsafe blocks small, so that what each one
promises can be read and checked by eye.
Foreign functions
unsafe extern fn name(params) : ret; declares a function provided from outside. Parameters and
result may only be integers, floats, bool, void and rawptr<T>; anything else, including a
ref or a struct, is rejected (diag.extern-non-ffi-type). The declaration itself says unsafe:
no tool can check that the C function really has that name and exactly those
types, and if it does not, every call is wrong, so the promise is marked where it is made, where a
search for unsafe finds it. Without unsafe the declaration is a syntax error. Calling it needs
unsafe too, for what the function does. Its result
is a claim: a genuine value of its declared type, but one whose relationship to anything
else (that a returned length is right, that a pointer is valid) is not established until your code
checks it.
std declares two externs: stdout_write(rawptr<u8> buf, usize len) : isize, which
printf (§21) writes through, with the unsafe block inside std, and its counterpart
stdin_read, which read_line reads through. Calling conventions and how code is linked are outside the language
and documented by each implementation; a program names the C code it needs with extern "…";
(below).
Both implementations call real C functions on x86-64 Linux: an unsafe extern fn names a
function in the C library or the maths library by its declared name. The compiler passes any
FfiType, pointers included. The interpreter's memory is simulated, so it can pass only integers,
bool and floats, and reports a call that needs a pointer as unsupported rather than guessing.
import std;
unsafe extern fn sqrt(f64 x) : f64; // from the maths library
unsafe extern fn labs(i64 x) : i64; // from the C library
unsafe extern fn toupper(i32 c) : i32;
fn main()
{
f64 root = unsafe
{
sqrt(2.0)
};
assert(root * root > 1.9999 && root * root < 2.0001); // a claim, checked before it is trusted
i64 distance = unsafe
{
labs(3 - 10)
};
assert(distance == 7);
assert(unsafe { toupper(99) } == 67); // 'c' -> 'C'
}import std;
fn main()
{
auto msg = b"Hi\n"; // a byte-array literal: array<u8, 3>
rawptr<u8> p = reinterpret_ptr<u8>(rawptr_of(&msg));
isize written = unsafe
{
stdout_write(p, 3) // the extern call; its result is a claim about how much was written
};
}Hi
import std;
unsafe extern fn takes_vec(Vec<i32> v); // ✗ diag.extern-non-ffi-type: a Vec cannot cross the boundary
fn main()
{
}Your own C code
extern "…"; names C code for the compiler to link, so that the program's unsafe extern fn
declarations can be found in it. It can go anywhere an item can, declares no name, and can be
repeated; each piece of code is linked once. The program still builds with nothing but
cobc main.cb.
| Written | Links | Notes |
|---|---|---|
extern "./shapes.c"; | a C source file | compiled on its own, with the C compiler's warnings shown |
extern "./lib/fast.o";, extern "./lib/libfast.a"; | an object file or static library | linked as it is |
extern "./lib/libplugin.so"; | a shared library | found wherever the program is run from |
extern "z"; | an installed library | as cc -lz |
A string beginning ./, ../ or / is a file, and a relative path is relative to the
file it is written in, exactly as in module m "./m.cb";. So a module that wraps some C code can
name it next to itself, and every program that uses the module gets it linked. Anything else is a
library name. A missing file or library is reported at the line that names it.
extern "…"; and ignores it;
the first call into that code stops with unsupported: no C function … which only cobc links
(exit status 3), and everything before it runs as usual.The usual shape is a module that keeps the unsafe calls to itself and exports safe
functions. Two things make the wrappers below safe. First, rect_area takes u8 sides, so the
product always fits in the C function's int32_t. Second, first_squares lets the C function fill
storage that comes from allocate, because a C function may write only outside the program's live
objects (§20's rule for extern calls); the count it returns is a claim, checked before it is used,
and the values are copied into a Vec.
import std;
module geometry "./geometry.cb";
fn main()
{
printf("%v", geometry::rect_area(6, 7));
printf("\n");
Vec<i64> sq = geometry::first_squares(5);
foreach (x in &sq)
{
printf("%v ", x);
}
printf("\n");
}42 0 1 4 9 16
import std;
extern "./shapes.c"; // relative to this file
unsafe extern fn area(i32 w, i32 h) : i32;
unsafe extern fn squares(rawptr<i64> out, i64 n) : i64;
// u8 sides: the product always fits the C function's int32_t.
export fn rect_area(u8 w, u8 h) : i32
{
unsafe { area(widen<i32>(w), widen<i32>(h)) }
}
// C fills storage from allocate; the count it returns is checked,
// then the values are copied out.
export fn first_squares(u8 n) : Vec<i64>
{
Vec<i64> out = Vec::new();
usize bytes = widen<usize>(n) * sizeof<i64>();
rawptr<u8> raw = match (allocate(bytes, alignof<i64>()))
{
Ok(p) : p,
Err(_) :
{
return out;
},
};
rawptr<i64> buf = reinterpret_ptr<i64>(raw);
i64 written = unsafe { squares(buf, widen<i64>(n)) };
if (written >= 0 && written <= widen<i64>(n))
{
foreach (i in 0..narrow<usize>(written))
{
Vec::push(&mut out, unsafe { *(buf + reinterpret<isize>(i)) });
}
}
unsafe
{
deallocate(raw, bytes, alignof<i64>());
}
out
}#include <stdint.h>
int32_t area(int32_t w, int32_t h)
{
return w * h;
}
// Fills out[0..n) with the first n squares; returns how many it wrote.
int64_t squares(int64_t *out, int64_t n)
{
for (int64_t i = 0; i < n; i++)
{
out[i] = i * i;
}
return n;
}From claim to fact
Data from outside (bytes read by an extern, a pointer handed back by a library, a length
in a header) starts as a claim. The pattern for turning it into a fact is a validator that returns
Result: the library's String::from_utf8(bytes) (§21) is the canonical example. Once the
validator has accepted the bytes, the resulting String is guaranteed valid UTF-8 for the rest of
its life, because there is no operation that could break it.
- Keep
unsafeblocks tiny and wrap them in a safe function whose signature makes the precondition impossible to violate from outside. - Never let a raw pointer stand in for a reference in an API. Return
ref<T, m>(fromreclaim) so callers get checked access. - Validate external data once, at the boundary, into a type that cannot represent the invalid state.
§21The standard library, std
spec/21-standard-library-semantics.mdimport std; Option, Result, the intrinsics, str and printf, and Vec, String, HashMap, HashSet, Queue, PriorityQueue, Rc and Box written in CobaltC.
The standard library is the module std. It holds the Option and Result enums and
the library's error types; the containers and text types, written in CobaltC itself: Vec<T>,
String, StringView, HashMap<K, V>, HashSet<K>, Queue<T>, PriorityQueue<T>, Box<T>, Rc<T>, Weak<T> and Channel<T>
(§19); output, input and files: printf, eprintf, sprintf, read_line, read_file, write_file,
File and the directory functions; the program's arguments, arg_count and arg; and smaller helpers
such as swap, replace, min, max, crc32 and Rng. A program uses it with import std;, or
by qualified paths such as std::Vec. Its source is part of the specification, which is the
language's proof that the mechanisms in this guide are enough to build real containers. A function
joins std when it names one very common intent, so that its name says what a loop or a match
would otherwise have to be read to discover; one that is only shorter, not clearer, stays out. Being
a module, it keeps its private parts private: no program can touch Vec's length or buffer,
String's bytes, a HashMap's table or Rc's count.
std is divided into submodules by subject, and its root passes each one's items on as its
own (export import, §17), so import std; brings in everything and std::Vec names the same type
as std::collections::Vec. A program that wants one subject alone may import just that one. Four
submodules divide again, the same way: std::crypto into digest, kdf, aead and pk,
std::http into client and server, std::database into one submodule per database server,
postgres today, and std::encoding into one submodule per data format or encoding of bytes as
text, json, hex and base64. Each is
re-exported by its parent, so sha256 is also std::crypto::digest::sha256, PgConnection is
std::database::postgres::PgConnection and Json is std::encoding::json::Json. One submodule is declared and not
re-exported: std::extensions, the area an implementation may fill with modules of its own, which the
specification leaves undefined and conformance never tests. A program reaches one only by naming it
(import std::extensions::m;), so its dependence on that implementation shows in its source; the
reference implementation's std::extensions is empty.
What the library asserts, and what it does not
A claim about std rests on one of four tiers, each on the one before: the language's own
guarantee (a program without unsafe blocks of its own has no undefined behaviour); the trusted
library base, the unsafe blocks inside std whose claims the implementation keeps (every one is
listed in spec/21 §0); the conformance suite, which shows that the bodies do what its rows say on the
inputs the rows give; and, for the security-relevant modules, a security profile: the standard
targeted, the vectors held, what is claimed about timing, the limits on input, and what was tested
outside the suite. A green suite is evidence for the third tier and nothing more. The full table is in
spec/21 §0 ("Security profiles"); the short form:
| Module | Standard | Constant time | Limits on input |
|---|---|---|---|
std::crypto::digest | SHA-1, SHA-2, BLAKE2b, CRC-32 | yes; digest_eq compares every byte | none |
std::crypto::kdf | HMAC, HKDF, PBKDF2, Argon2id | yes, except Argon2id's data-dependent half (by design) | Argon2id parameters are checked |
std::crypto::aead | ChaCha20-Poly1305, AES-GCM | yes (bitsliced AES, constant-time GHASH) | key and nonce lengths are checked |
std::crypto::pk | X25519, P-256/P-384, ECDSA, Ed25519, RSA | the private operations, yes; verification and RSA key generation, no | encodings are checked |
std::x509 | X.509, PEM, PKCS #8 keys | not claimed (public data) | a chain of at most ten certificates |
std::tls | TLS 1.3 client and server | the primitives'; digest_eq for Finished | records of at most 16 KiB (+256 encrypted); timeouts |
std::http | HTTP/1.1 | — | head 64 KiB; body max_body (16 MiB); max_redirects (10); timeout_ms |
std::compress | DEFLATE, zlib, gzip | — | output at most the max you pass |
std::encoding::json | JSON | — | nesting at most 512 deep; numbers are f64 |
std::database::postgres | PostgreSQL protocol 3.0, SCRAM-SHA-256 | digest_eq for the server signature | messages at most 1 GiB; results at most max_result (16 MiB) |
Every parser in that table was also fed ten thousand mutated inputs under GCC, Clang and
AddressSanitizer, each in its own thread (the hostile-input round, D-0211): the expectation is an
Err or a clean Ok for every one, never a fault or a crash. The round found two faults, in the PEM
readers and in the HTTP client's status line, both now an Err.
Every module and its items
Below is every module of std with every item it declares, generated from the
reference implementation's source. Names in faint red are not exported: they are the
module's own working parts, which a program cannot name or call, listed so that nothing in the source
is left out. Everything else is exported. An item marked n has no CobaltC body: both
tools build it in (printf, sqrt, exit, …). A row headed T:: lists T's
associated functions. A parent module also re-exports the submodules it lists, so their items are its
own too.
std, std::core, std::collections, std::text, std::io, std::fs, std::env, std::memory, std::sync, std::random, std::math, std::time, std::process, std::net, std::crypto, std::crypto::digest, std::crypto::kdf, std::crypto::aead, std::crypto::pk, std::x509, std::tls, std::http, std::http::client, std::http::server, std::database, std::database::postgres, std::encoding, std::encoding::json, std::encoding::hex, std::encoding::base64, std::compress, std::extensions
std impl/std/std.cb
| Kind | Items |
|---|---|
| Submodules | core, collections, text, io, fs, env, memory, sync, random, math, time, process, net, crypto, x509, tls, http, database, encoding, compress, extensions |
| Functions | byte_answer, byte_op, bytes_ptr, code_error, file_error, fs_answer, path_result, path_text, printn |
Externs (unsafe) | clock_read, event_op, file_at, file_op, file_read, file_write, fs_op, fs_query, net_op, os_random, path_op, proc_op, sleep_ns, stderr_write, stdin_read_line, stdout_flush, task_op, tz_offset |
std::core impl/std/core.cb
| Kind | Items |
|---|---|
| Types | AllocError, Option, Result |
| Functions | assertn, overwrite, replace, swap |
Option:: | expect, is_none, is_some, ok_or, unwrap, unwrap_or |
Result:: | expect, is_err, is_ok, map_errn, ok, unwrap, unwrap_or |
std::collections impl/std/collections.cb
| Kind | Items |
|---|---|
| Types | Drain, HashDrain, HashMap, HashSet, PriorityQueue, Queue, SetDrain, Vec |
| Functions | key_before, read_be, read_le, slice_eq, slice_parts, slice_swap |
Vec:: | append, apply_order, binary_search, binary_search_by, clear, clone, clone_by, contains, dedup, drain, drop, extend_from, filled, from_fn, from_slice, grow, index_exclusive, index_of, index_shared, insert, len, new, pop, position, push, push_be, push_le, remove, remove_at, replace, reserve, retain, reverse, sort, sort_by, sorted_order, sorted_order_keys, swap, swap_remove, take_raw, truncate |
Drain:: | drop, take, total |
HashMap:: | clear, clone, contains, contains_str, drain, entry, find, find_str, get, get_mut, get_mut_str, get_str, index_of, insert, key_at, len, new, remove, remove_slot, remove_str, reserve_one, reslot, swap_remove, unslot, value_at |
HashDrain:: | drop, take_key, take_value, total |
HashSet:: | clear, clone, contains, contains_str, difference, drain, insert, intersection, key_at, len, new, remove, remove_str, swap_remove, union |
SetDrain:: | take, total |
Queue:: | back, clear, drop, front, len, new, pop_back, pop_front, push_back, push_front, shift |
PriorityQueue:: | before, clear, len, new, new_by, peek, pop, push, sift_up |
std::text impl/std/text.cb
| Kind | Items |
|---|---|
| Types | ParseError, String, StringView, Utf8Error |
| Functions | ascii_is_alnum, ascii_is_alpha, ascii_is_digit, ascii_is_lower, ascii_is_space, ascii_is_upper, ascii_to_lower, ascii_to_upper, byte_index_from, parse_bytes, space_run_end, sprintfn, utf8_error_at |
Utf8Error:: | text |
ParseError:: | text |
String:: | append, append_str, append_string, append_text, append_view, appendfn, as_bytes, as_view, char_count, chars, clear, clone, code_point, contains, ends_with, eq, eq_str, eq_view, find, from_str, from_utf8, from_view, into_bytes, join, len, less, lines, new, parse, push_ascii, push_code_point, replace, reserve, split, split_whitespace, starts_with, strip_prefix, strip_suffix, to_ascii_lower, to_ascii_upper, trim, trim_end, trim_start, truncate, view |
StringView:: | as_bytes, at, boundary, char_count, chars, code_point, contains, ends_with, eq, eq_str, find, from_utf8, join, len, less, lines, of, parse, ptr, replace, space, split, split_whitespace, starts_with, strip_prefix, strip_suffix, sub, trim, trim_end, trim_start |
std::io impl/std/io.cb
| Kind | Items |
|---|---|
| Functions | eprint_str, eprintfn, flush_stdout, print_str, print_string, printfn, read_all, read_all_bytes, read_line |
File:: | printfn |
Externs (unsafe) | stdin_read, stdout_write |
std::fs impl/std/fs.cb
| Kind | Items |
|---|---|
| Types | File, FileError, FileInfo, PathKind |
| Functions | copy_file, file_error_at, file_info, fs_path_op, list_dir, make_dir, make_dir_all, path_canonical, path_extension, path_file_name, path_is_absolute, path_join, path_kind, path_normalize, path_parent, path_stem, read_bytes, read_file, remove_dir, remove_dir_all, remove_file, rename, write_bytes, write_file |
FileError:: | text |
File:: | append, close, create, create_new, drop, flush, len, open, open_rw, position, read, read_line, read_to_end, seek, start, take, write, write_formatted, write_text |
std::env impl/std/env.cb
| Kind | Items |
|---|---|
| Types | Args |
| Functions | arg, arg_count, current_dir, current_exe, env_var, home_dir, hostname, set_current_dir, temp_dir |
Args:: | finish, flag, from, help, is_option, new, option, options, spec |
Externs (unsafe) | arg_bytes |
std::memory impl/std/memory.cb
| Kind | Items |
|---|---|
| Types | Box, Rc, RcBox, Weak |
Rc:: | clone, downgrade, drop, get, new |
Weak:: | clone, drop, upgrade |
Box:: | cells, clone, drop, get, get_mut, into_inner, new |
std::sync impl/std/sync.cb
| Kind | Items |
|---|---|
| Types | Channel, ChannelState, ThreadFailure |
| Functions | cpu_count, failure_text, is_finished, try_join |
ChannelState:: | pop |
Channel:: | close, drop, new, recv, recv_timeout_ms, send, try_recv |
ThreadFailure:: | diagnostic, is_cancelled, text |
std::random impl/std/random.cb
| Kind | Items |
|---|---|
| Types | Rng |
| Functions | os_random_bytes, os_random_u64, os_random_vec, uuid_v4 |
Rng:: | below, from_os, new, next_u64, rotl, shuffle, splitmix, unit_f64 |
std::math impl/std/math.cb
| Kind | Items |
|---|---|
| Types | BigDivision, BigUint |
| Functions | abs, acosn, asinn, atann, atan2n, ceiln, clamp, cosn, expn, floorn, gcd, is_finite, is_nan, lnn, log10n, log2n, max, min, pow, powfn, roundn, sinn, sqrtn, tann, truncn |
BigUint:: | add, bit, bit_len, clone, compare, div_rem, eq, from_bytes_be, from_u64, is_zero, less, mod_inverse, mod_pow, mul, parse, rem, shift_left, shift_right, shifted_limbs, sub, text, to_bytes_be, to_bytes_be_padded, trim, zero |
std::time impl/std/time.cb
| Kind | Items |
|---|---|
| Types | DateTime, Weekday |
| Functions | days_in_month, floor_div, floor_mod, http_date_words, http_time, iso_byte, iso_digits, iso_from_unix_ms, leap_year, local_offset_seconds, monotonic_ns, month_name, month_number, sleep_ms, unix_ms, unix_ms_from_iso, unix_seconds |
DateTime:: | eq, from_http_date, from_iso, from_unix, less, new, now, read_iso, to_http_date, to_iso, to_local, to_unix, valid, weekday |
Weekday:: | text |
std::process impl/std/process.cb
| Kind | Items |
|---|---|
| Types | Child, ChildInput, ChildOutput, Command, Output |
| Functions | exitn, interrupt_requested, pipe_op, process_id, watch_interrupts |
Command:: | arg, clear_env, current_dir, encode, env, new, output, output_with_input, put, run, spawn, status |
Child:: | cap, close_input, done, drop, id, kill, kill_tree, op, read_error, read_error_line, read_output, read_output_line, take, take_input, take_output, terminate, try_wait, wait, write_input |
ChildInput:: | drop, write |
ChildOutput:: | drop, read, read_line |
std::net impl/std/net.cb
| Kind | Items |
|---|---|
| Types | Datagram, IpAddr, NetError, SocketAddr, TcpListener, TcpStream, UdpSocket |
| Functions | net_addr, net_error, net_num, net_open, resolve |
IpAddr:: | localhost_v4, localhost_v6, parse, text, unspecified_v4, unspecified_v6 |
SocketAddr:: | from_record, new, parse, record, text |
NetError:: | text |
TcpListener:: | accept, bind, drop, local_addr, set_accept_timeout_ms |
TcpStream:: | connect, connect_timeout, drop, local_addr, peer_addr, printfn, read, read_exact, read_line, set_keepalive, set_nodelay, set_read_timeout_ms, set_write_timeout_ms, shutdown_write, take, try_clone, write, write_formatted, write_text |
UdpSocket:: | bind, connect, drop, local_addr, recv, recv_from, send, send_to, set_read_timeout_ms, try_clone |
std::crypto impl/std/crypto.cb
| Kind | Items |
|---|---|
| Submodules | digest, kdf, aead, pk |
| Functions | block_len, hash_len |
| Constants | BLAKE2B_G |
std::crypto::digest impl/std/crypto/digest.cb
| Kind | Items |
|---|---|
| Types | Blake2b, Hasher, HashKind, Sha1, Sha256, Sha384, Sha512 |
| Functions | blake2b, blake2b_absorb, blake2b_block, crc32, digest_eq, hash, sha1, sha1_absorb, sha1_block, sha256, sha256_absorb, sha256_block, sha384, sha512, sha512_absorb, sha512_block, sha512_bytes |
Sha256:: | finish, new, update |
Sha512:: | final_words, finish, new, update |
Sha384:: | finish, new, update |
Sha1:: | finish, new, update |
Hasher:: | finish, new, update |
Blake2b:: | finish, new, new_keyed, update |
| Constants | BLAKE2B_IV, BLAKE2B_SIGMA, SHA256_K, SHA512_K |
std::crypto::kdf impl/std/crypto/kdf.cb
| Kind | Items |
|---|---|
| Types | Hmac, HmacSha256 |
| Functions | argon2_g, argon2_get, argon2_h_prime, argon2_load, argon2_p, argon2_put, argon2id, b64_decode_unpadded, b64_unpadded, hash_password, hkdf_expand, hkdf_expand_label, hkdf_expand_label_with, hkdf_expand_with, hkdf_extract, hkdf_extract_with, hkdf_sha256, hkdf_with, hmac, hmac_sha256, pbkdf2, pbkdf2_sha256, verify_password |
HmacSha256:: | finish, new, update |
Hmac:: | finish, new, update |
std::crypto::aead impl/std/crypto/aead.cb
| Kind | Items |
|---|---|
| Types | AesKey, Poly1305 |
| Functions | aead_tag, aes_block, aes_encrypt4, aes_encrypt_block, aes_expand, aes_gcm_open, aes_gcm_seal, aes_mix_columns, aes_pack, aes_rot1, aes_rot2, aes_rot3, aes_sbox_bytes, aes_sbox_ct, aes_shift_rows, aes_transpose8, aes_unpack, aes_xtime, chacha20, chacha20_poly1305_open, chacha20_poly1305_seal, chacha_block, chacha_quarter, crypto_check_lengths, gcm_block, gcm_bytes, gcm_ctr, gcm_j0, gcm_tag, ghash_bmul64, ghash_mul, ghash_rev64, ghash_update, poly1305, poly1305_blocks |
Poly1305:: | block, finish, new, update_last, update_padded |
| Constants | AES_ROW0, AES_ROW1_HI, AES_ROW1_LO, AES_ROW2_HI, AES_ROW2_LO, AES_ROW3_HI, AES_ROW3_LO |
std::crypto::pk impl/std/crypto/pk.cb
| Kind | Items |
|---|---|
| Types | Borrowed, DerSpan, EcCurve, Ed, EdExpanded, Field, Jacobian, Mont, RsaPrivateKey, RsaPublicKey |
| Functions | biguint_of, curve_b_hex, curve_base, curve_hex, curve_point, der_positive_int, der_push_uint, digest_info_prefix, digest_scalar, ec_affine, ec_mul, ec_private_key, ec_public_key, ec_scalar, ecdh, ecdsa_sign, ecdsa_verify, ed25519_private_key, ed25519_public_key, ed25519_sign, ed25519_verify, ed_add, ed_base, ed_decode, ed_double, ed_encode, ed_expand, ed_identity, ed_mul, ed_order, ed_select, f_add, f_add_raw, f_bit, f_from_be, f_from_mont, f_invert, f_is_zero, f_less, f_nibble, f_select, f_small, f_sub, f_sub_borrow, f_sub_raw, f_to_mont, fe_add, fe_carry, fe_cswap, fe_eq, fe_from_bytes, fe_invert, fe_is_zero, fe_mul, fe_neg, fe_pow_p58, fe_select, fe_sub, fe_to_bytes, field_of, jac_add, jac_double, jac_select, jac_table, jac_twin, jac_twin_bits, le_limbs, limbs7_add, limbs_be, limbs_of, limbs_sub_mod, mgf1, mont_mul, mont_mul_v, mont_new, mont_pow, mont_pow_limbs, p256_ecdh, p256_private_key, p256_public_key, pkcs1v15_encode, probably_prime, random_prime, rsa_generate_key, rsa_open, rsa_private_op, rsa_sign_pkcs1v15, rsa_sign_pss, rsa_verify_pkcs1v15, rsa_verify_pss, sc_bytes, sc_from_hash, sc_mont, small_primes, x25519, x25519_ladder, x25519_private_key, x25519_public_key |
RsaPrivateKey:: | new, public_key |
| Constants | ED_2D, ED_BX, ED_BY, ED_D, ED_SQRT_M1, FE_MASK |
std::x509 impl/std/x509.cb
| Kind | Items |
|---|---|
| Types | CertError, Certificate, CertKey, Der, EcPrivateKey, EcPublicKey, PrivateKey, SignatureScheme, TrustStore |
| Functions | ascii_lower_eq, certificates_from_pem, check_time, common_name_of, curve_oid_hex, der_big, der_bit_string, der_bytes, der_eq_hex, der_expect, der_oid_hex, der_push_len, der_read, der_text, der_time, der_uint, der_wrap, digits, ec_private_of, hash_oid, key_algorithm, name_matches, pem_block, pem_of, public_key_of, rsa_private_of, same_bytes, sign, signature_algorithm, verify_chain, verify_signature |
CertError:: | text |
CertKey:: | from_der, from_pem, to_der, to_pem |
Certificate:: | matches_host, parse |
TrustStore:: | add, from_pem_file, new, system |
PrivateKey:: | from_der, from_pem, public_key, to_der, to_pem |
std::tls impl/std/tls.cb
| Kind | Items |
|---|---|
| Types | ClientHello, Handshake, Inner, Record, RecordKeys, ServerHello, TlsError, TlsStream |
| Functions | certificate_list, certificate_verify_ok, client_hello, client_hello_parse, derive_secret, finished_data, handshake_record, keys_from, next_message, open_record, push_extension, push_u24, read_record, record_nonce, same_session, scheme_of, seal_record, send_handshake, send_plain_alert, server_hello, server_hello_bytes, server_scheme, suite_hash, suite_key_len, transcript_add, transcript_hash, u16_list, write_all |
TlsError:: | text |
TlsStream:: | client, close, connect, fill, local_addr, peer_addr, peer_certificate, read, read_exact, read_line, server, write, write_text |
std::http impl/std/http.cb
| Kind | Items |
|---|---|
| Submodules | client, server |
| Types | BodyLength, Conn, Header, HttpError, Param, Reader, Request, Response, Url |
| Functions | ascii_ieq, ascii_ieq_str, available, body_length, conn_close, conn_read, conn_write, default_port, fill_buffer, find_header, find_param, form_decode, form_encode, form_encode_part, form_part, has_header, hex_digit, host_text, http_line, http_newline_from, is_unreserved, name_eq, names_eq, percent_decode, percent_encode, read_body, read_headers, read_n, read_to_end, reason_phrase |
HttpError:: | text |
Url:: | parse, query_pairs, target, text |
Param:: | new |
Header:: | new |
Request:: | header, new |
Response:: | header, text |
| Constants | MAX_HEAD |
std::http::client impl/std/http/client.cb
| Kind | Items |
|---|---|
| Types | HttpClient, IdleConn, StatusLine |
| Functions | exchange, exchange_on, http_get, http_post, idempotent, method_has_body, not_content_encoding, open_connection, origin_of, parse_status_line, redirect_target, request_bytes |
HttpClient:: | new, send |
std::http::server impl/std/http/server.cb
| Kind | Items |
|---|---|
| Types | HttpConnection, HttpServer |
HttpServer:: | accept, bind, bind_tls, local_addr |
HttpConnection:: | close, peer_addr, request, respond |
std::database impl/std/database.cb
| Kind | Items |
|---|---|
| Submodules | postgres |
| Types | Scram |
| Functions | attribute, scram_salted_password |
Scram:: | client_first, respond, start, verify |
std::database::postgres impl/std/database/postgres.cb
| Kind | Items |
|---|---|
| Types | Message, PgColumn, PgConn, PgConnection, PgError, PgParameter, PgReader, PgResult, PgRow, PgServerError, PgSslMode, PgUrl, PgValue |
| Functions | authenticate, broken, collect, connect_url, cstr_at, decode_columns, decode_row, decode_server_error, decoded, float_text, frame, open_tcp, pg_available, pg_close, pg_fill, pg_read, pg_read_n, pg_write, push_cstr, read_message, tag_count |
PgError:: | text |
PgUrl:: | parse |
PgValue:: | text |
PgRow:: | get, get_bool, get_bytes, get_f64, get_i64 |
PgResult:: | column |
PgConnection:: | close, connect, connect_with, execute, parameter, query, run_script |
| Constants | MAX_MESSAGE |
std::encoding impl/std/encoding.cb
| Kind | Items |
|---|---|
| Submodules | json, hex, base64 |
std::encoding::json impl/std/encoding/json.cb
| Kind | Items |
|---|---|
| Types | Json, JsonMember, JsonReader, JsonStr |
| Functions | json_digits_end, json_hex4, json_indent, json_number, json_quote, json_quote_bytes, json_space_end, json_string, json_write |
Json:: | as_array, as_bool, as_f64, as_i64, as_object, as_text, at, get, is_null, parse, pretty, push, set, text |
JsonReader:: | digits, number, peek, skip_space, text, value, word |
| Constants | MAX_DEPTH |
std::encoding::hex impl/std/encoding/hex.cb
| Kind | Items |
|---|---|
| Functions | from_hex, hex_decode, hex_value, to_hex |
std::encoding::base64 impl/std/encoding/base64.cb
| Kind | Items |
|---|---|
| Functions | base64_decode, base64_decode_bytes, base64_encode, base64_value |
std::compress impl/std/compress.cb
| Kind | Items |
|---|---|
| Types | BitOut, BitReader, BitsStep, BitWriter, CompressError, DeflateScan, Huffman, HuffStep, InflateRun, Token, TokenCounts |
| Functions | adler32, bits_step, canonical_codes, code_lengths, deflate, deflate_scan, distance_code, fixed_codes, fixed_distances, gunzip, gzip, hash3, huff_step, inflate, inflate_codes, inflate_from, inflate_run, length_code, put_tokens, read_dynamic, token_counts, write_block, zlib_deflate, zlib_inflate |
CompressError:: | text |
BitReader:: | align, take |
Huffman:: | build, decode |
BitWriter:: | flush, put, put_code |
| Constants | CHAIN_LIMIT, CLEN_ORDER, DIST_BASE, DIST_EXTRA, HASH_SIZE, LENGTH_BASE, LENGTH_EXTRA, MAX_TOKENS, WINDOW |
std::extensions impl/std/extensions.cb
No items: the reference implementation declares nothing here.
import std::text;
import std::io;
fn main()
{
String s = "only text and output";
printf("%s\n", &s);
}only text and output
This guide writes import std; and the short names throughout; each part below says which
submodule it lives in.
The intrinsics and the text value type str are not part of std. They belong
to the language itself and need no import. This section keeps the two apart, as the specification's
tables do: the intrinsics first, then std at a glance, then each part of std in turn.
Types in std
export enum Option<T>
{
Some(T),
None
}
export enum Result<T, E>
{
Ok(T),
Err(E)
}
export struct AllocError
{
}
export struct Utf8Error
{
export usize offset; // where validation failed
}
export enum FileError // why reading or writing failed: a file, or standard input
{
NotFound,
Denied,
Io,
Utf8(Utf8Error)
}
export enum ParseError // why parse failed
{
Empty,
Invalid(usize), // the offset where the number stops
OutOfRange
}Intrinsics: part of the language
Intrinsics are called like functions; their behaviour is given by rules rather than by a
body, and they need no import. Type arguments are written name<T>(…) where the signature needs
one. Their names are not reserved: a function or variable of your own with the same name is what the
name means, and the intrinsic is used only where there is none.
Values and arithmetic
| Intrinsic | Purpose | See |
|---|---|---|
drop(x) | destroy a resource now | §07 |
wrapping_*, saturating_*, checked_* | alternative overflow policies | §06 |
widen<U>, narrow<U>, checked_narrow<U>, narrow_wrapping<U>, reinterpret<U>, to_float<U>, to_int<U> | numeric conversions | §06 |
count_ones(x), leading_zeros(x), trailing_zeros(x), rotate_left(x, k), rotate_right(x, k) | an integer's bits | §06 |
sizeof<T>(), alignof<T>() | layout queries, in bytes | §06, §16 |
min_value<T>(), max_value<T>() | a number type's smallest and largest value (a float's finite extremes) | §06 |
Text, slices, checks and faults
| Intrinsic | Purpose | See |
|---|---|---|
str_len(s), str_byte(s, i), str_ptr(s) | a str's byte count, one byte (bounds-checked), and the address of its bytes (safe) | §21 |
slice_len(s) | a slice's number of elements (also through a reference) | §16 |
static_assert(c), static_assert(c, "why") | reject the program when the constant c is false | §18 |
fault(name), fault(name, msg) | stop with the run-time fault diag.name (a dynamic diagnostic, _ for -, such as index_out_of_bounds), with msg if given; for code that implements containers as std does | §18 |
Raw memory and threads
| Intrinsic | Purpose | See |
|---|---|---|
dangling<T>() | a well-aligned non-null address that must never be dereferenced; used for empty containers | §21 |
rawptr_of(&x), reinterpret_ptr<U>(p) | obtain and retype raw pointers (safe) | §20 |
reclaim<T>(p), release(p, n), copy_raw(dst, src, n) | raw memory as objects (unsafe) | §20 |
allocate(n, align), deallocate(p, n, align) | raw allocation (deallocate is unsafe) | §20 |
read_volatile(p), write_volatile(p, v) | raw accesses performed exactly as written (unsafe; below) | §20 |
Mutex::new(v), lock(&m) | mutual exclusion | §19 |
spawn(f, args…), join(h) | threads | §19 |
read_volatile(p) and write_volatile(p, v) are raw accesses that the compiler must perform
exactly as written — never merged, dropped or reordered against another volatile access or a call —
for storage that changes on its own, such as a device register (D-0121). They take a rawptr<T> to
a plain T, inside unsafe; a bitstruct (§16) names the register's fields and
Name::from_bits(read_volatile(p)) reads them.
std at a glance
Everything here is an item of std, most of it written in CobaltC, and needs import std;
(or a qualified path). The containers and text types have their own parts of this section below; this
is the rest, for finding a name quickly.
Option, Result and the errors
| Function | Purpose | See |
|---|---|---|
Option::unwrap(o), Option::expect(o, msg), Result::unwrap(r), Result::expect(r, msg) | the value, or the fault diag.unwrap-failed (with msg) when there is none | §18 |
Option::unwrap_or(o, d), Result::unwrap_or(r, d) | the value, or the default d when there is none | §18 |
Option::ok_or(o, e) | the Option as a Result, Err(e) for None, so ? applies to it | §18 |
Result::ok(r) | the Result as an Option: Some of the value, None for an error (which is destroyed) | §18 |
Option::is_some(&o), is_none, Result::is_ok(&r), is_err | which variant, without taking the value | §18 |
Result::map_err(r, f) | convert a Result's error type with f | §18 |
FileError::text(&e), ParseError::text, Utf8Error::text | a std error as a sentence for a person, a str | §21 |
Output, input and the program
| Function | Purpose | See |
|---|---|---|
printf("%v", x) | write a str, a number, a bool, a String (through &), a StringView, or an enum's variant name to standard output | §21 |
printf(fmt, args…), eprintf(fmt, args…), sprintf(fmt, args…), String::appendf(&mut s, fmt, args…) | C-style formatted output, to standard output, to standard error, as a new String or into one, checked when compiled | §21 |
assert(c), assert(c, fmt, args…) | fault with diag.assert-failed (and the formatted message) when c is false | §18 |
flush_stdout() | write out what printf has buffered, for output watched through a pipe while the program runs | §21 |
read_line() | read the next line of standard input as a String | §21 |
read_all(), read_all_bytes() | the rest of standard input at once, as a String or as bytes | §21 |
arg_count(), arg(i) | the number of program arguments, and argument i as a String | §21 |
read_file(path), write_file(path, text) | read a whole file as a String, or replace a file's contents; path and text are StringViews: a literal, or &s[0..$] of a String | §21 |
stdout_write(buf, len), stdin_read(buf, len), arg_bytes(i, buf, len) | the externs beneath them: raw bytes to standard output, from standard input, and of argument i (unsafe) | §20 |
Numbers
| Function | Purpose | See |
|---|---|---|
String::parse<T>(&s) | read a whole String as a number of type T | §21 |
min(a, b), max(a, b) | the smaller or larger of two values of an ordered type (§12) | §21 |
abs(x), pow(x, n) | absolute value and power (n a u32) of a number type (§12), checked as the operators they use | §21 |
sqrt(x), floor(x), ceil(x), round(x), trunc(x) | an f32 or f64's square root and its roundings to an integral value (round: halfway cases away from zero), exact or correctly rounded, so the same bits everywhere | §21 |
ln(x), exp(x), log2(x), log10(x), sin(x), cos(x), tan(x), atan2(y, x), powf(x, y) | an f32 or f64's logarithms, exponential, trigonometry and power, from the platform's maths library (within one unit in the last place; the two operands of atan2 and powf of one type) | §21 |
abs and pow are checked like the operators they use: abs of a signed type's minimum, or a
power that does not fit, is diag.arith-overflow. The floating-point functions (std::math) take
an f32 or an f64 and give the same type; an integer is converted first, sqrt(to_float<f64>(n)).
The logarithms, the exponential, the trigonometric functions and powf come from the platform's
maths library, so their last bit may differ between platforms; sqrt and the roundings are exact
everywhere.
str
CobaltC has two text types, and the short version is: str is a text constant,
String is text you own. Fixed text you write in your program is a str; text you build,
receive or keep ownership of is a String (below). The end of this section compares them side by
side.
A string literal "…" is a value of type str: a sequence of bytes that is valid UTF-8
by construction, because the literal syntax has no way to write an arbitrary byte. A str is not a
reference and not a resource. It behaves exactly like an integer: copying it is free, nothing owns
it, nothing frees it, and you can pass it, store it in a struct or a Vec<str>, compare it with
==, and hand it to another thread without any annotation. Its bytes are reached with str_len
and str_byte; there is no indexing or slicing syntax. Escapes are \n \r \t \0 \\ \" and
\u{…} for a Unicode scalar value; a literal must close on its own line. Adjacent literals are
one literal, as in C, so longer text is written a line at a time: "one\n" "two\n" is the
single str "one\ntwo\n", and a long printf format can be split the same way.
printf("%v", s) writes a str to standard output. It prints numbers, bool and String
too (see "Printing: printf" below). The unsafe block that reaches the stdout_write extern is inside std, so a
program that only prints never writes unsafe itself.
import std;
struct Named
{
str name;
i32 n;
}
fn shout(str s)
{
printf("%v!\n", s);
}
fn main()
{
str greeting = "hello";
str again = greeting; // copied, not moved: greeting stays valid
assert(str_len(greeting) == 5);
assert(str_byte(greeting, 0) == 104);
assert(greeting == again);
static_assert("caf\u{e9}" != "cafe"); // \u{e9} is é, two bytes
assert(str_len("caf\u{e9}") == 5);
auto named = Named { .name = greeting, .n = 1 };
auto copy = named; // a struct of plain values is plain
assert(copy.name == "hello");
Vec<str> words = Vec::new();
Vec::push(&mut words, greeting);
Vec::push(&mut words, "world");
shout(words[1]);
printf("%v\n", greeting);
}world! hello
fn main()
{
str s = "hi";
u8 b = str_byte(s, 2); // ✗ diag.index-out-of-bounds (static): a literal index, refuted ahead of time
}A byte literal b"…" is different: it is an array<u8, N>, exactly the array you
would have written by hand, and admits \xNN for any byte value (and only ASCII characters
otherwise). Use it where bytes are the honest type, such as data handed to stdout_write or to a
validator.
Vec<T>
In std::collections.
A growable array with a raw buffer, a length and a capacity. It is a resource whose
destructor destroys each element and frees the buffer. A plain element is copied when read
(v[i]); a resource element is only borrowed, or moved out by pop or remove, never copied (so
Vec<Vec<i32>> works without any special support).
| Function | Signature | Notes |
|---|---|---|
Vec::new | () : Vec<T> | T from the expected type or written Vec<i32>::new(); allocates nothing |
Vec::len | (ref<Vec<T>, shared>) : usize | |
Vec::push | (ref<Vec<T>, exclusive>, T) | moves a resource element in; grows 0 → 4 → 8 → …, which relocates elements |
Vec::reserve | (ref<Vec<T>, exclusive>, usize n) | room for n more elements, so the next n pushes do not grow it; the elements are unchanged (D-0129) |
Vec::pop | (ref<Vec<T>, exclusive>) : Option<T> | None when empty; moves a resource element out |
Vec::insert | (ref<Vec<T>, exclusive>, usize i, T) | puts the value at i (i ≤ len), moving the later elements up one |
Vec::remove | (ref<Vec<T>, exclusive>, usize i) : T | takes the element at i out, moving the later elements down one |
Vec::swap_remove | (ref<Vec<T>, exclusive>, usize i) : T | takes the element at i out and moves the last element into its place: no other element moves, the order is not kept |
Vec::retain | (ref<Vec<T>, exclusive>, fn(ref<T, shared>) : bool keep) | keeps the elements keep accepts, in order, and destroys the rest (D-0181) |
Vec::dedup | (ref<Vec<T>, exclusive>) | removes each element equal to the one before it, so a sorted Vec keeps one of each value (T must be eq, D-0181) |
Vec::index_shared | (ref<Vec<T>, shared>, usize) : ref<T, shared> | bounds-checked: diag.index-out-of-bounds |
Vec::index_exclusive | (ref<Vec<T>, exclusive>, usize) : ref<T, exclusive> | as above, for writing |
v[i], &v[i..j] | indexing and slicing (§16) | v[i] reads through index_shared, writes through index_exclusive; $ is Vec::len(&v) |
Vec::clear | (ref<Vec<T>, exclusive>) | destroys every element, first to last; keeps the buffer for reuse |
Vec::truncate | (ref<Vec<T>, exclusive>, usize n) | keeps the first n elements and destroys the rest, last first; n past the length changes nothing |
Vec::clone | (ref<Vec<T: clone>, shared>) : Vec<T> | a copy, element by element, each by clone (§12): Vec<String>, Vec<Vec<u8>>, … |
Vec::clone_by | (ref<Vec<T>, shared>, fn(ref<T, shared>) : T) : Vec<T> | a copy made by the function, one element at a time, when the copy wanted is not the type's own clone |
Vec::from_slice | (slice<T: clone, shared>) : Vec<T> | a new Vec of the slice's elements, each by clone: Vec::from_slice(&bytes[4..$]) |
Vec::filled | (usize n, T x) : Vec<T> | n copies of x by clone: Vec::filled(80, b' '), Vec::filled(3, Vec::filled(4, 0: u8)); T: clone |
Vec::from_fn | (usize n, fn(usize) : T) : Vec<T> | f(0), f(1), …, f(n - 1), any T: a grid is Vec::from_fn(rows, [](usize r) : Vec<u8> { Vec::filled(cols, 0) }) |
Vec::append | (ref<Vec<T>, exclusive>, Vec<T> b) | moves b's elements onto the end, in order; b is consumed |
Vec::extend_from | (ref<Vec<T>, exclusive>, slice<T, shared>) | copies a slice's elements onto the end by clone: Vec::extend_from(&mut out, &bytes[0..$]); T: clone |
Vec::position | (ref<Vec<T>, shared>, fn(ref<T, shared>) : bool) : Option<usize> | the first index whose element satisfies the test, or None; a closure may capture the key it looks for |
Vec::index_of, Vec::contains | (ref<Vec<T>, shared>, ref<T, shared>) : Option<usize> / : bool | the first index equal to the value, or whether there is one; T: eq — a number, bool, str or String (a struct takes position) |
Vec::reverse | (ref<Vec<T>, exclusive>) | the order inverted in place, by swaps; nothing is destroyed |
slice_eq | (slice<T, shared>, slice<T, shared>) : bool | the same length and equal elements in order; T: eq. == does not compare vectors (they are resources): slice_eq(&a[0..$], &b[0..$]) |
Vec::push_le(&mut v, x), Vec::push_be | (ref<Vec<u8>, exclusive>, T: integer) | x's bytes onto a byte Vec, least (le) or most (be) significant first; any width, signed as two's complement (D-0119) |
read_le<T>(&v[0..$], at), read_be<T> | (slice<u8, shared>, usize) : T | the T whose bytes start at at; diag.index-out-of-bounds when fewer than sizeof<T>() remain |
crc32(&v[0..$]) | (slice<u8, shared>) : u32 | CRC-32/IEEE, the checksum of zlib, gzip and PNG; crc32 of "123456789"'s bytes is 0xCBF43926 (§21 of the specification writes the algorithm out) |
When you know how many elements are coming, say so first: Vec::reserve(&mut v, n)
makes room for n more at once, so filling the Vec allocates and copies once instead of every
time it doubles. String::reserve(&mut s, n) does the same for bytes. Neither changes what the
program computes: no function reports a capacity, the length and the elements stay as they were,
and reserving room that is already there does nothing. Like a push that grows, a reserve that
grows moves the elements, so a reference into the old cells is stale afterwards. std's own
builders (String::from_str, String::clone, Vec::from_slice) already reserve what they
need.
import std;
fn main()
{
Vec<u32> squares = Vec::new();
Vec::reserve(&mut squares, 1000); // one allocation for all of them
foreach (i in 0..1000: u32)
{
Vec::push(&mut squares, i * i);
}
assert(Vec::len(&squares) == 1000 && squares[999] == 998001);
String line = String::new();
String::reserve(&mut line, 80);
foreach (i in 0..10)
{
String::append(&mut line, "-=-=-=-=");
}
assert(String::len(&line) == 80);
}import std;
fn main()
{
Vec<i32> v = Vec::new();
foreach (i in 0..9: i32) // crosses two growth boundaries (4 and 8)
{
Vec::push(&mut v, i * i);
}
assert(Vec::len(&v) == 9);
assert(*Vec::index_shared(&v, 8) == 64);
{
auto slot = Vec::index_exclusive(&mut v, 0);
*slot = 100; // write through an exclusive element reference
} // slot must be gone before v is borrowed again
assert(*Vec::index_shared(&v, 0) == 100);
match (Vec::pop(&mut v))
{
Some(x) :
{
assert(x == 64);
},
None :
{
assert(false, "unreachable");
},
}
Vec<Vec<i32>> nested = Vec::new();
Vec::push(&mut nested, v); // v moves into the outer Vec
assert(Vec::len(Vec::index_shared(&nested, 0)) == 8);
} // nested is destroyed: each inner Vec, then the outer bufferimport std;
fn main()
{
Vec<i32> v = Vec::new();
Vec::push(&mut v, 1);
Vec::index_shared(&v, 5); // ✗ diag.index-out-of-bounds (dynamic)
}The helpers of D-0112 name the loops programs kept writing: filling, building by index,
joining, searching and reversing. filled and extend_from copy plain elements; from_fn and
append are the forms for resources.
import std;
fn main()
{
Vec<u8> row = Vec::filled(4, b'.'); // "...."
Vec<Vec<u8>> grid = Vec::from_fn(3, [](usize r) : Vec<u8> { Vec::filled(4, b'.') });
grid[1][2] = b'#';
Vec<String> names = Vec::from_fn(3, [](usize i) : String { sprintf("n%d", i) });
Vec<String> more = Vec::new();
Vec::push(&mut more, String::from_str("x"));
Vec::append(&mut names, more); // more is consumed
Vec::extend_from(&mut row, &grid[1][0..$]); // copies bytes
assert(Vec::len(&names) == 4 && Vec::len(&row) == 8 && row[6] == b'#');
String key = "n1";
assert(Vec::index_of(&names, &key) == Some(1));
assert(Vec::contains(&row, &b'#') && !Vec::contains(&row, &b'?'));
assert(Vec::position(&names, [key](ref<String, shared> s) : bool { *s == key }) == Some(1));
Vec::reverse(&mut names);
assert(String::eq_str(&names[0], "x"));
}A String in hand can be searched and split directly: String::find, starts_with,
ends_with, contains, replace, trim and split are the StringView functions applied to the whole-string view
&s[0..$]. And a map keyed by String can be asked with a literal: HashMap::get_str,
contains_str and remove_str find the entry a String of the same text would, without building
one (D-0113).
import std;
fn main()
{
String line = " name = Ada, born = 1815 ";
assert(String::trim(&line) == "name = Ada, born = 1815");
assert(String::find(&line, "born") == Some(14) && String::ends_with(&line, " "));
HashMap<String, i64> fields = HashMap::new();
foreach (part in String::split(&line, ","))
{
Vec<StringView> kv = StringView::split(part, "=");
match (StringView::parse<i64>(StringView::trim(kv[1])))
{
Ok(n) : { HashMap::insert(&mut fields, String::from_view(StringView::trim(kv[0])), n); },
Err(_) : {},
}
}
assert(HashMap::contains_str(&fields, "born") && !HashMap::contains_str(&fields, "name"));
match (HashMap::get_str(&fields, "born"))
{
Some(y) : assert(*y == 1815),
None : assert(false),
}
assert(HashMap::remove_str(&mut fields, "born") == Some(1815) && HashMap::len(&fields) == 0);
}Binary data: Vec::push_le and Vec::push_be write an integer's bytes in a fixed order and
read_le/read_be read them back, so a file header or a network message never spells a shift by
hand; all four are in std::collections. crc32, the CRC-32/IEEE checksum those formats use
(D-0119), is in std::crypto beside the hashes, but it only catches accidents: anyone can make
data with a chosen CRC, so use a hash or a MAC against tampering (D-0177). A bitstruct (§16)
crosses into bytes the same way, through Name::bits(v).
import std;
fn main()
{
Vec<u8> header = Vec::new();
Vec::push_be(&mut header, 0x89504E47: u32); // a PNG signature's first four bytes, big-endian
Vec::push_le(&mut header, 640: u16); // a little-endian width
Vec::push_le(&mut header, -1: i8);
assert(Vec::len(&header) == 7 && header[0] == 0x89 && header[4] == 0x80 && header[5] == 2);
assert(read_be<u32>(&header[0..$], 0) == 0x89504E47 && read_le<u16>(&header[0..$], 4) == 640);
assert(read_le<i8>(&header[0..$], 6) == -1);
String text = "123456789";
assert(crc32(&String::as_bytes(&text)[0..$]) == 0xCBF43926);
}A Vec can hold references. While it does, they count as live borrows: the referent
cannot be written past them, and a stored &mut is the only way in until the element is popped or
the Vec is destroyed.
import std;
fn main()
{
i32 a = 10;
i32 x = 1;
Vec<ref<i32, shared>> readers = Vec::new();
Vec::push(&mut readers, &a);
assert(*readers[0] == 10);
Vec<ref<i32, exclusive>> writers = Vec::new();
Vec::push(&mut writers, &mut x);
*writers[0] = 7; // write through the stored reference
Vec::pop(&mut writers); // the Vec lets go of it
assert(x == 7);
x = 8; // x is free again
}import std;
fn main()
{
i32 x = 1;
Vec<ref<i32, exclusive>> writers = Vec::new();
Vec::push(&mut writers, &mut x);
x = 5; // ✗ diag.aliasing-conflict: the Vec still holds &mut x
}Vec::push is written in CobaltC: a resource struct with a rawptr<T>, an
unsafe write of the new element at ptr + len, and a private grow that calls allocate,
copy_raw and deallocate. Reading it in the specification is the best way to see how the
pieces of §07 and §20 fit together.Random numbers
In std::random.
Rng is a general-purpose pseudo-random generator — for simulations, games, sampling,
randomised tests, shuffling — whose sequence the specification fixes, so a program that seeds it
with a constant prints the same numbers under both tools and on every machine; seed it with
Rng::from_os() for a fresh sequence each run. below(&mut r, n) is unbiased, unit_f64 is in [0, 1).
It is not suitable for cryptographic purposes: its output is predictable from a few values, so
never use it for keys, tokens, nonces, passwords, or anything an adversary must not guess (D-0120).
| Function | Does |
|---|---|
Rng::new(s), Rng::next_u64(&mut r), Rng::below(&mut r, n), Rng::unit_f64(&mut r) | a general-purpose pseudo-random generator (xoshiro256**, fixed by the specification): the same seed gives the same numbers everywhere; below is unbiased in 0 .. n. It is not suitable for cryptographic purposes — keys, tokens, nonces, anything an adversary must not predict (D-0120) |
Rng::shuffle(&mut r, &mut v[0..$]), uuid_v4() | a slice put in random order (Fisher-Yates; the same seed, the same order everywhere); a version-4 UUID as text, from the operating system's secure source (D-0181) |
import std;
fn main()
{
Rng r = Rng::new(42);
assert(Rng::next_u64(&mut r) == 1546998764402558742); // the specification's check value
Rng dice = Rng::new(7);
u64 total = 0;
foreach (k in 0..100)
{
total += Rng::below(&mut dice, 6) + 1; // a die: 1 .. 6
}
assert(total >= 100 && total <= 600);
f64 x = Rng::unit_f64(&mut dice);
assert(x >= 0.0 && x < 1.0);
}For anything secret, take bytes from the operating system's cryptographically secure source
instead (std::random): os_random_bytes(&mut buf[0..$]) fills a slice, os_random_u64() gives one
number. These are what keys, session tokens and nonces are made of. Rng::from_os() seeds an Rng
from them, which makes its sequence differ between runs but not secret: the sequence is still
Rng's. Every system both tools run on has such a source; if one did not, the call would be the
fault diag.entropy-unavailable, never a quiet fall back to the clock.
import std;
fn main()
{
Vec<u8> token = Vec::filled(16, 0: u8);
os_random_bytes(&mut token[0..$]);
String hex = String::new();
foreach (byte in &token)
{
String::appendf(&mut hex, "%02x", *byte);
}
assert(String::len(&hex) == 32);
Rng shuffle = Rng::from_os(); // differs each run; not for secrets
assert(Rng::below(&mut shuffle, 52) < 52);
}String
In std::text.
A String is a Vec<u8> that is known to be valid UTF-8: owned, heap-allocated text,
a resource. String::new() makes an empty one, String::from_str copies a str's bytes
(already valid, so nothing is checked), and String::from_utf8 validates bytes that came from
anywhere else. Where the declared type already says String, a literal is enough: String s =
"text";, a struct literal's String field, an argument whose parameter is a String, and the
value a function declared : String returns are each String::from_str of the literal, written
short (D-0149), and so is a literal argument whose generic parameter the other arguments make a
String: Result::unwrap_or(arg(0), "default"), Vec::push(&mut names, "x") for a Vec<String>
(D-0162). That is the one place a literal allocates, and the word String in a declaration nearby
says so; a str variable is never made a String without String::from_str, and
auto s = "text"; is a str. It changes only through functions that keep it valid UTF-8 — append and
appendf add text, push_ascii an ASCII byte, truncate cuts at a character boundary, clear
empties it — so the guarantee holds for its whole life. from_utf8 is the trust-transition pattern of §20 in
miniature.
| Function | Signature |
|---|---|
String::new | () : String (empty) |
String::from_str | (str) : String (copies; cannot fail); a literal where a String is declared is this call already (D-0149) |
String::from_utf8 | (Vec<u8>) : Result<String, Utf8Error> |
StringView::from_utf8 | (slice<u8, shared>) : Result<StringView, Utf8Error> -- the same check, and a view of the bytes instead of a copy |
String::len | (ref<String, shared>) : usize (bytes) |
String::into_bytes | (String) : Vec<u8> (gives the bytes back, consuming the string) |
String::as_bytes | (ref<String, shared>) : ref<Vec<u8>, shared> (read the bytes in place) |
String::append | (ref<String, exclusive>, T) for any T %v takes: adds what printf("%v", x) would write |
String::clone | (ref<String, shared>) : String (a copy with the same bytes) |
String::reserve | (ref<String, exclusive>, usize n): room for n more bytes; the text is unchanged (D-0129) |
String::eq, String::eq_str | (ref<String, shared>, ref<String, shared>) : bool, (ref<String, shared>, str) : bool: the same text? (a == b does the same for two Strings, §12) |
String::less, StringView::less | (ref<String, shared>, ref<String, shared>) : bool: byte order, a prefix first, as a < b (§12); Vec::sort(&mut v) sorts text |
String::to_ascii_lower, String::to_ascii_upper | (ref<String, shared>) : String: a copy with ASCII letters changed; other bytes as they are |
String::truncate | (ref<String, exclusive>, usize n): keeps the first n bytes; n inside a character faults diag.not-char-boundary |
String::push_ascii | (ref<String, exclusive>, u8): adds one ASCII byte; one of 128 or more faults diag.not-ascii |
String::clear | (ref<String, exclusive>) (empties it, keeping the buffer) |
For bytes, std has the ASCII tests and conversions a parser needs: ascii_is_digit(b),
ascii_is_alpha, ascii_is_alnum, ascii_is_upper, ascii_is_lower, ascii_is_space (a space,
tab, newline, carriage return, vertical tab or form feed) take a u8 and give a bool;
ascii_to_lower(b) and ascii_to_upper(b) give the u8 with an ASCII letter's case changed.
Unicode case is not provided.
import std;
fn main()
{
assert(ascii_is_digit(b'7') && !ascii_is_alpha(b'7') && ascii_is_space(b'\t'));
assert(ascii_to_upper(b'q') == b'Q');
String shout = String::to_ascii_upper(&String::from_str("caf\u{e9} au lait"));
assert(String::eq_str(&shout, "CAF\u{e9} AU LAIT"));
}import std;
fn main()
{
String owned = "caf\u{e9}"; // a str's bytes, copied; no validation needed
assert(String::len(&owned) == 5); // 5 bytes: é is two of them
assert(String::char_count(&owned) == 4); // but 4 characters
assert(Vec::len(&String::chars(&owned)) == 4); // each a view of its bytes
Vec<u8> good = Vec::new();
Vec::push(&mut good, 226); // the three bytes of U+20AC, the euro sign
Vec::push(&mut good, 130);
Vec::push(&mut good, 172);
String s = match (String::from_utf8(good)) // good moves in
{
Ok(v) : v,
Err(_) :
{
return;
},
};
assert(String::len(&s) == 3);
Vec<u8> back = String::into_bytes(s); // and out again
assert(Vec::len(&back) == 3);
Vec<u8> bad = Vec::new();
Vec::push(&mut bad, 226); // a truncated sequence
Vec::push(&mut bad, 130);
match (String::from_utf8(bad))
{
Ok(_) :
{
assert(false, "unreachable");
},
Err(e) :
{
assert(e.offset == 0);
},
}
}StringView: part of a String, without copying
In std::text.
&s[i..j] on a String gives a StringView: a read-only view of the bytes i to j,
with no copy. Positions are byte positions, $ is the length, and both ends must fall on a
character boundary (cutting through a multi-byte character is diag.not-char-boundary). A view
prints with %s, compares with == to another view or a str, and can be sliced again,
&v[1..$]. It borrows its String exactly as a slice borrows a Vec: while the view lives, the
String cannot be changed, and a view cannot outlive it. String::from_view(v) copies it into a
String of its own when it must.
| Function | Result |
|---|---|
StringView::len(v) | its length in bytes |
StringView::of(t) | a view of a str t; a literal where a view is expected is one already (D-0134) |
StringView::as_bytes(v) | the view's bytes as a slice<u8, shared>, not a copy: what a hash, a MAC or a socket write takes (D-0163) |
StringView::find(v, t) | Option<usize>: where the text of the view t first occurs |
StringView::starts_with(v, t), ends_with | bool |
StringView::trim(v), trim_start, trim_end | the view without ASCII white space at its ends |
StringView::split(v, ",") | a Vec<StringView> of the parts, each a view of the same String |
StringView::chars(v) | a Vec<StringView> of its characters, in order, one view each (D-0128) |
StringView::char_count(v) | how many characters it holds (len counts bytes; D-0193) |
StringView::contains(v, t) | bool: whether the text of t occurs in v (D-0139) |
StringView::replace(v, from, to) | a new String: v with every occurrence of from replaced by to, left to right, no overlap; an empty from occurs nowhere |
StringView::join(&parts[0..$], sep), String::join(&strings[0..$], sep) | a new String: the parts with sep between each two; the inverse of split |
StringView::parse<T>(v) | a number, as String::parse |
String::from_view(v), String::append(&mut s, v) | the text copied into a String |
String::find(&s, t), starts_with, ends_with, contains, replace, trim, trim_start, trim_end, split, chars, char_count, as_view | the same, straight from a String: each is the StringView:: function applied to &s[0..$], the whole-string view (D-0113) |
Text that a function only reads is a StringView throughout std: a file's path, the text
to search for, the text to write (D-0134). A String passes its whole-string view, &s[0..$]. A
literal passes itself: a str literal where a StringView is expected is a view of it, and since a
str never ends, neither does the view. A str held in a variable is not a literal; view it with
StringView::of(t). (split's separator is the one str: split returns views of its first
argument, and a function returning views may borrow only one thing, §10.)
import std;
fn main()
{
String text = "the cat sat on the mat";
String word = "sat";
assert(String::find(&text, &word[0..$]) == Some(8)); // a String to search for
assert(String::find(&text, "mat") == Some(19)); // a literal: already a view
str w = "on";
assert(String::find(&text, StringView::of(w)) == Some(12));
StringView label = "fixed"; // a view of a literal, for the whole run
printf("%s %d\n", label, StringView::len(label));
}fixed 5
import std;
// The first word of a line: a view into the line itself.
fn first_word(ref<String, shared> s) : StringView
{
StringView t = StringView::trim(&(*s)[0..$]);
match (StringView::find(t, " "))
{
Some(i) : &t[0..i],
None : t,
}
}
fn main()
{
String line = " set width=80, height = 24 ";
assert(first_word(&line) == "set");
StringView args = StringView::trim(&line[6..$]); // "width=80, height = 24"
i64 area = 1;
foreach (field in StringView::split(args, ","))
{
match (StringView::find(field, "="))
{
Some(eq) :
{
StringView value = StringView::trim(&field[eq + 1..$]);
match (StringView::parse<i64>(value))
{
Ok(n) : { area *= n; },
Err(_) : { assert(false, "unreachable"); },
}
},
None : { assert(false, "unreachable"); },
}
}
assert(area == 1920);
printf("%s -> %d\n", args, area);
}width=80, height = 24 -> 1920
Three questions come up so often that they have their own names (D-0139). contains is
find with a yes-or-no answer. replace makes a new String with every occurrence of one text
replaced by another, scanning left to right so occurrences never overlap; the empty text occurs
nowhere, so replacing it changes nothing. join is the inverse of split: the parts with a
separator between each two, from a slice of views (what split gives) or a slice of Strings
(what a program builds). Each takes its texts as StringViews, so a literal, a String's
&s[0..$] and another view all serve.
import std;
fn main()
{
String path = "/usr/local/lib/cobaltc";
assert(String::contains(&path, "/local/") && !String::contains(&path, "bin"));
String windows = String::replace(&path, "/", "\\"); // a new String; path unchanged
printf("%s\n", &windows);
String aaa = "aaa";
printf("%s\n", &String::replace(&aaa, "aa", "b")); // left to right, no overlap: "ba"
Vec<StringView> parts = String::split(&path, "/"); // "", "usr", "local", "lib", "cobaltc"
printf("%s\n", &StringView::join(&parts[1..$], " > "));
assert(StringView::join(&parts[0..$], "/") == path); // join undoes split
Vec<String> names = Vec::new();
Vec::push(&mut names, String::from_str("ada"));
Vec::push(&mut names, String::from_str("grace"));
printf("%s\n", &String::join(&names[0..$], ", "));
}\usr\local\lib\cobaltc ba usr > local > lib > cobaltc ada, grace
import std;
fn main()
{
String name = "ada";
StringView v = &name[0..$];
String::push_ascii(&mut name, b'!'); // ✗ diag.aliasing-conflict (static): v still views name
assert(v == "ada");
}Going through the characters
foreach does not take a String itself, and there is no char type; String::chars(&s)
gives the characters instead: a Vec<StringView>, one view per character, in order. A character is
one to four bytes of UTF-8 (é is two, most emoji four), so Vec::len of it counts characters where
String::len counts bytes. Each view is text like any other: it prints, compares with == to a
str, is matched by text patterns, and appends to a String; and like any view it borrows the
String while it is in use. StringView::chars(v) does the same for a view, such as a word from
split. Building the Vec is one allocation; there is no form that visits the characters without
it, because foreach walks only the collections the language defines and there is no iterator
protocol for a library to plug into (D-0128 explains). Where that matters, walk
String::as_bytes(&s) instead.
import std;
fn main()
{
String s = "caf\u{e9} \u{1F600}!";
foreach (c in String::chars(&s))
{
printf("[%v]", c);
}
printf("\n%v characters in %v bytes\n", Vec::len(&String::chars(&s)), String::len(&s));
// Reversed by characters, so the two-byte é and the four-byte emoji stay whole.
Vec<StringView> cs = String::chars(&s);
Vec::reverse(&mut cs);
String back = String::new();
foreach (c in &cs)
{
String::append(&mut back, *c);
}
printf("%v\n", &back);
// Positions and text patterns work on each character.
foreach (i, c in String::chars(&s))
{
match (c)
{
"\u{e9}" : printf("\u{e9} is character %v\n", i),
"!" : printf("! is character %v\n", i),
_ : {},
}
}
}[c][a][f][é][ ][😀][!] 7 characters in 11 bytes !😀 éfac é is character 3 ! is character 6
Lines, words, prefixes and code points (D-0181). lines splits text at
, dropping a
before it and giving no empty line after a final newline; split_whitespace
gives the words between runs of ASCII whitespace, none empty; strip_prefix(v, t) and
strip_suffix give the rest of the text as Some(view) when it begins (or ends) with t, and
None otherwise, which is how --name=value is read. code_point(v) is the number of the first
character (None for empty text), and String::push_code_point(&mut s, cp) appends one as UTF-8,
turning a surrogate or a value above U+10FFFF into U+FFFD so the String stays valid. Each exists on
a String too (String::lines(&s), …). String::parse<bool> reads exactly true or false.
import std;
fn main()
{
String config = "name = demo\r\nlevel = 3\n\n# a comment\nflags = loud fast\n";
foreach (line in String::lines(&config))
{
if (StringView::starts_with(line, "#") || StringView::len(line) == 0)
{
continue;
}
Vec<StringView> words = StringView::split_whitespace(line);
printf("%v -> %v word(s)\n", words[0], Vec::len(&words) - 2);
}
foreach (a in ["--level=7", "--quiet", "file.txt"])
{
match (StringView::strip_prefix(StringView::of(a), "--level="))
{
Some(v) : printf("level %v\n", Result::unwrap_or(StringView::parse<u32>(v), 0)),
None : printf("other: %s\n", a),
}
}
String s = "é";
printf("U+%04X\n", Option::unwrap_or(String::code_point(&s), 0));
String t = String::new();
String::push_code_point(&mut t, 0x1F600);
printf("%v has %v bytes\n", &t, String::len(&t));
}name -> 1 word(s) level -> 1 word(s) flags -> 2 word(s) level 7 other: --quiet other: file.txt U+00E9 😀 has 4 bytes
Which text type?
Rust programmers will recognise the pair, and C programmers may wonder why there are two
text types at all. The reason is that a constant should not have to be managed. If "hi" were a
String, every literal would be a heap allocation with an owner: it would be moved, destroyed at
scope end, and allocated again on every pass through a loop, all for text that never changes. So a
literal is a str, which behaves like the number 5. String is kept for text that really does
need an owner.
str | String | b"…" | |
|---|---|---|---|
| What it is | a text constant | owned text | raw bytes |
| Written as | "hello" | String::from_str("hello"), or "hello" where a String is declared (D-0149); String::from_utf8(bytes) | b"hello" |
| Type | str | String | array<u8, N> |
| Valid UTF-8 | yes, by the literal grammar | yes, by construction or validation | not guaranteed |
| Resource (moved, destroyed) | no: copied like an integer | yes | no |
| Allocates | never | yes | no (an ordinary array) |
| Passed to | printf, ==, str_len, str_byte; any StringView parameter | printf (as &s), String::len, String::into_bytes; a StringView parameter as &s[0..$] | stdout_write and other raw-byte code |
- Text written in your source code: use
str. Pass it, store it in structs andVec<str>, and print it freely. - Text built at run time, or text you need to own: use
String, created explicitly from astrwithString::from_str. - Part of a
String, to read, compare, split or parse without copying: aStringView,&s[i..j]. - A function that only reads text, from anywhere, takes a
StringView, asstd's do (D-0134): a caller passes a literal as it is,f("key"), and aStringasf(&s[0..$]); nothing is copied. - Bytes from outside the program (a file, a socket, FFI): they stay
Vec<u8>untilString::from_utf8accepts them. - Bytes that are not text (a protocol header, data for
stdout_write): useb"…".
Unicode. All three are UTF-8 bytes, and std works on them as bytes: lengths
and indices count bytes, == and the orderings compare bytes, and String::chars hands out the code
points when you need them. std has no Unicode tables (D-0226): case mapping is ASCII only
(String::to_ascii_lower), there is no normalization, so "é" and "e\u{301}" are different strings, and no
splitting into user-perceived characters. A program that needs those brings its own tables.
The two text types never convert into each other silently (§12's no-implicit-conversion
rule). Going from a str value to a String is an explicit call that makes a copy, and nothing turns
a String back into a str: a str is always a literal's value, so it cannot refer to bytes that
something else owns. A literal is different in kind: like the 5 in u64 x = 5;, it has no
type until its position gives it one, so String s = "hi"; is the literal typed as a String
(String::from_str of it, D-0149) and StringView v = "hi"; as a view, not a value converted; the
same holds for a constructor's payload, so Option<StringView> o = Some("hi"); is a view too (D-0225). A
str variable in the same place is still a type error. A String compares with == and != to a
str or a StringView, either way round, by its bytes (name == "sha256", D-0164); the orderings
< … >= stay within one text type.
Printing: printf
In std::io (printf, eprintf, flush_stdout) and std::text (sprintf, String::appendf).
printf is the one way a program writes output. It is C's, with one difference that matters: the format is checked against its
arguments when the program is compiled. The format must be a string literal; each % specifier
must match its argument's type and there must be exactly one argument per specifier, or the
program is rejected (diag.type-mismatch, or diag.format-invalid for a malformed specifier). So
a printf can never misprint or crash at run time. sprintf(fmt, args…) returns the same text as a
new String (C's sprintf without a buffer to overflow), and String::appendf(&mut s, fmt, args…)
adds it to an existing one. eprintf(fmt, args…) writes the same text to standard error, for
messages that are not the program's output. There is no println; end a line with \n.
Standard output is buffered, so printing line by line is fast. The text reaches the
screen, a pipe or a file in pieces, but never later than the next eprintf, the next read of
standard input, the start of a child that shares standard output (Command::status), and the
program's end, however it ends. On a terminal each line also appears as soon as it is complete. So
the two streams keep the program's order, and a prompt is shown before the program waits for the
answer. Call flush_stdout() only when standard output goes to a pipe or a file and someone watches
it while the program runs: a progress line, a log.
| Specifier | Takes | Prints |
|---|---|---|
%d %i %u | any integer type | decimal; no %ld/%lld/%zu needed, the type is known |
%x %X %o %b | any integer type | hex, octal, binary (# adds 0x, 0X, 0o, 0b) |
%f %e %g (and %F %E %G) | f32, f64 | fixed, exponent, or shortest of the two, 6 digits by default |
%s | str, &String, StringView, bool | the text |
%v | any of the above | the value as CobaltC writes it (below) |
%% | nothing | a % |
Flags, a width and a precision work as in C: %-8s pads on the right, %05d pads with
zeros, %+d always shows a sign, %.2f rounds to two decimals, %8.3f does both. Widths count
characters, so text in any language lines up; to size a column to its text, count the characters
too, Vec::len(&String::chars(&s)), since String::len counts bytes. There is no %c. A * in place of the width or
the precision takes it from the next argument, which must be a usize: printf("%*d", w, n),
printf("%.*s", n, &s).
import std;
fn main()
{
usize w = 6;
printf("[%*d]\n", w, 42);
printf("[%-*s]\n", w, "ab");
printf("[%.*f]\n", 2: usize, 3.14159);
}[ 42] [ab ] [3.14]
import std;
fn main()
{
String item = "widget";
u32 qty = 12;
f64 price = 3.5;
printf("%-8s|%5d|%8.2f|%#06x\n", &item, qty, price, 255);
printf("%.1f%% done, %e left\n", 99.5, 0.00042);
String line = sprintf("%s x %d", &item, qty);
String::appendf(&mut line, " = %.2f", price * 12.0);
printf("%v\n", &line);
}widget | 12| 3.50|0x00ff 99.5% done, 4.200000e-04 left widget x 12 = 42.00
import std;
fn main()
{
printf("%d items\n", "twelve"); // ✗ diag.type-mismatch: %d takes an integer
}coby greet.cb with no argumentrunsimport std;
fn main() : u8
{
if (arg_count() != 1)
{
eprintf("usage: greet NAME\n"); // to standard error, not mixed into the output
return 2;
}
match (arg(0))
{
Ok(name) :
{
printf("hello, %s\n", &name);
},
Err(e) :
{
eprintf("greet: the name is not UTF-8 (byte %d)\n", e.offset);
return 1;
},
}
0
}%v: any value
%v is not C's: it writes a value the way CobaltC writes it, whatever its type, so
printf("%v\n", x) needs no thought about x. It takes any of the types below, and nothing else:
a struct or a Vec is a static type error, so printing a composite value means printing its
parts; an enum prints its variant's name. It takes a width and -, but no precision.
| Argument | Written as | Examples |
|---|---|---|
str | its bytes | printf("%v", "hi") → hi |
| any integer type | decimal, - if negative | -42, 18446744073709551615 |
f32, f64 | the shortest digits that read back as the same value | 0.1, 1.0, 0.3333333333333333, 1.0e21, 1.0e-7, NaN, inf |
bool | true or false | |
ref<String, shared> | the string's bytes | printf("%v", &s) |
StringView | its text | printf("%v", word) |
| an enum, or a reference to one | its variant's name (not its payload) | Some, Mul, NotFound |
A float prints as few digits as identify it exactly: 0.1 prints as 0.1, even though
the stored value is slightly more than a tenth, and reading the printed text back gives the same
value. A whole number keeps a .0, so floats and integers look different. Between 0.000001 and
1021 the number is written out in full; beyond that range it gets an exponent. An f32 chooses
its digits as an f32, so 0.1: f32 also prints 0.1. This is the rule JavaScript and Python
use.
%v's argument type is checked where the printf is, and a generic function can print
its own T. The check happens for each type the function is used with, so show below
accepts numbers and text, and would be rejected for a struct.
import std;
fn show<T>(T x) // T is checked where show is used
{
printf("%v\n", x);
}
fn main()
{
show(-42);
show(max_value<u64>());
show(true);
show(0.1);
show(1.0);
show(1.0 / 3.0);
show(0.1: f32);
show(1000000000000000000000.0);
f64 zero = 0.0;
show(zero / zero);
String name = "owned text";
printf("%v", &name); // a String through a reference: name stays usable
printf("\n");
printf("x = %v, y = %v\n", 3, 4.5);
}-42 18446744073709551615 true 0.1 1.0 0.3333333333333333 0.1 1.0e21 NaN owned text x = 3, y = 4.5
import std;
fn main()
{
String s = "text";
printf("%v", s); // ✗ diag.type-mismatch (static): a String prints through &s
}import std;
struct Point
{
i32 x;
i32 y;
}
fn main()
{
printf("%v", Point { .x = 1, .y = 2 }); // ✗ diag.type-mismatch (static): print the fields instead
}Raw bytes are not text, so printf does not take a u8 as a character or a Vec<u8>:
a u8 prints as a number. To write bytes, use the stdout_write extern the way std does, around an
unsafe block. put writes one byte and print_bytes a whole Vec<u8>:
import std;
// Write one byte to standard output.
fn put(u8 b)
{
auto buf = [b]; // an array<u8, 1>
rawptr<u8> p = reinterpret_ptr<u8>(rawptr_of(&buf));
unsafe
{
stdout_write(p, 1); // trusted: p points at one valid byte
}
}
fn print_bytes(ref<Vec<u8>, shared> v)
{
foreach (b in v)
{
put(*b);
}
}
fn main()
{
put(b'a'); // the byte of `a`
printf("%v", b'a'); // a u8 prints as a number: 97
put(b'\n');
Vec<u8> v = Vec::new();
Vec::push(&mut v, b'o');
Vec::push(&mut v, b'k');
Vec::push(&mut v, b'\n');
print_bytes(&v);
}a97 ok
import std;
fn main()
{
str t = "hi";
String s = t; // ✗ diag.type-mismatch (static): t is a str value;
// String::from_str(t) makes the String
}import std;
fn main()
{
String s = "hi"; // ✓ a literal where a String is declared is String::from_str of it (D-0149)
str t = "there";
String u = String::from_str(t); // ✓ the explicit, allocating bridge for a str value
assert(String::len(&s) == 2 && String::len(&u) == 5);
}Reading input
In std::io.
read_line() reads the next line of standard input. It returns a
Result<Option<String>, FileError>:
| Result | Meaning |
|---|---|
Ok(Some(line)) | the next line, without its \n (a final \r\n is removed whole); a last line with no \n counts too |
Ok(None) | the input has ended (and every later call says so again) |
Err(Utf8(e)) | the line is not valid UTF-8; e.offset is the bad byte's position in the line, and the next call reads the line after it |
Err(Io) | the input could not be read |
A line ends at \n, and a \r just before it is removed with it, so a line typed
on Windows reads the same as one typed on Linux. The line is a
String that has already been checked, so a program that reads never writes unsafe, and ?
passes a FileError up to the caller: the same error a file gives (D-0134), whose NotFound and
Denied never come from standard input, so a match that names Io and Utf8 ends with _.
Underneath is the stdin_read extern, the counterpart of stdout_write, which you can call
yourself inside unsafe to read raw bytes.
Both coby and cobc read standard input, from the terminal or from a pipe, for example
printf 'ada\nlin\n' | coby greet.cb. Standard output is flushed before each read, so a prompt
printed with printf appears before the program waits.
import std;
// Greets every line of input; blank lines are skipped.
fn greet_all() : Result<usize, FileError>
{
usize count = 0;
while (Some(name) = read_line()?) // None: the end of input; an error goes back to main
{
if (String::len(&name) > 0)
{
printf("hello, %v\n", &name);
count += 1;
}
}
Ok(count)
}
fn main()
{
match (greet_all())
{
Ok(n) :
{
printf("%v greeted\n", n);
},
Err(e) :
{
match (e)
{
Io :
{
printf("could not read input\n");
},
Utf8(u) :
{
printf("not UTF-8 at byte %v\n", u.offset);
},
}
},
}
}A program that needs all of its input before it can start, to sort it or parse it as one
document, takes it at once with read_all() (a String, Err(Utf8(e)) when it is not text) or
read_all_bytes() (a Vec<u8>, any bytes). Each gives what is left after any read_line calls,
line ends exactly as they came, and an empty result at the end of input.
import std;
fn before(ref<StringView, shared> a, ref<StringView, shared> b) : bool
{
StringView::less(*a, *b)
}
// Sorts the lines of its input: `sort` in a few lines.
fn main() : u8
{
String all = match (read_all())
{
Ok(s) : s,
Err(e) :
{
eprintf("sort: %s\n", FileError::text(&e));
return 1;
},
};
Vec<StringView> lines = String::split(&all, "\n");
if (Vec::len(&lines) > 0 && StringView::len(lines[Vec::len(&lines) - 1]) == 0)
{
Vec::pop(&mut lines); // the empty piece after the last line end
}
Vec::sort_by(&mut lines, before);
foreach (l in &lines)
{
printf("%s\n", *l);
}
0
}Program arguments
In std::env.
arg_count() is the number of arguments the program was started with, and arg(i)
is argument i as a Result<String, Utf8Error>. Argument 0 is the first one after the program:
coby prog.cb a b and cobc --run prog.cb a b both give a and b, and the program's own name
is not an argument. Each argument is checked on its own, so one that is not UTF-8 is an Err
without affecting the others. arg(i) with i ≥ arg_count() faults diag.index-out-of-bounds,
as indexing a Vec does. Both tools pass arguments. Underneath is the arg_bytes extern, which
you can call inside unsafe for an argument's raw bytes.
coby args.cb one tworunsimport std;
fn main()
{
foreach (i in 0..arg_count())
{
match (arg(i))
{
Ok(a) : printf("%v: %v\n", i, &a),
Err(e) : printf("%v: not UTF-8 at byte %v\n", i, e.offset),
}
}
}0: one 1: two
Command-line options: Args
In std::env.
A tool declares its options as it reads them (D-0184): Args::flag(&mut a, "loud", "l", help)
says whether --loud or -l was given and takes every such argument; Args::option(&mut a, "times",
"n", "N", help) gives the value of --times=3, --times 3 or -n 3 (the last one, or None), and
Args::options every value of a repeated option; each call also adds a line to Args::help(&a).
Args::finish(&mut a) then gives what is left — the positional arguments, in order, with everything
after -- among them — or an Err naming the first option nobody declared, or an option missing its
value, for the program to print. A lone - is an argument. Args::new(usage) reads the program's own
arguments; Args::from(items, usage) takes any Vec<String>.
import std;
fn main() : u8
{
Args a = Result::unwrap(Args::new("greet [options] NAME..."));
bool loud = Args::flag(&mut a, "loud", "l", "shout");
bool help = Args::flag(&mut a, "help", "h", "show this help");
usize times = Result::unwrap_or(StringView::parse<usize>(String::as_view(&Option::unwrap_or(Args::option(&mut a, "times", "n", "N", "how many times"), String::from_str("1")))), 1);
if (help)
{
printf("%v", &Args::help(&a));
return 0;
}
Vec<String> names = match (Args::finish(&mut a))
{
Ok(rest) : rest,
Err(m) :
{
eprintf("greet: %v\n", &m);
return 2;
},
};
foreach (i in 0..times)
{
foreach (n in &names)
{
printf("%v, %v!\n", if (loud) { "HELLO" } else { "hello" }, n);
}
}
0
}HELLO, world! HELLO, world!
Run with --help the same program prints:
Usage: greet [options] NAME...
Options:
-l, --loud shout
-h, --help show this help
-n, --times N how many timesNumbers and text
In std::text.
String::append(&mut s, x) adds to s exactly what printf("%v", x) would write, for the same
types: a str, any integer, f32/f64, bool, a StringView, or another String through &. So a line can be
built piece by piece and printed, or kept, once. String::parse<T>(&s) goes the other way: it reads the
whole String as a number of type T, any integer type, f32 or f64, and returns a
Result<T, ParseError>.
| Result | Meaning |
|---|---|
Ok(v) | the whole text is a number, and it fits in T |
Err(Empty) | the text is empty |
Err(Invalid(k)) | byte k cannot continue a number ("12x4" → 2), or the text ends too early ("-" → 1) |
Err(OutOfRange) | a number, but not one T can hold ("300" as u8) |
Parsing is strict: an optional + or -, then digits, and for a float an optional fraction and
exponent (2.5e-3), or inf or NaN. No spaces anywhere; read_line removes a Windows line’s
\r\n whole, so its lines parse, but a \r from any other source is Invalid. Floats are rounded correctly, and every float %v writes parses back to
the same value. To take a line apart first, split, trim or search it as views (String::split,
trim, find, the StringView functions above) and parse a piece with StringView::parse<T>.
coby sum.cb 1.5 2 0.25runsimport std;
// Adds up its arguments: `coby sum.cb 1.5 2 x` reports the `x`.
fn main() : u8
{
f64 total = 0.0;
foreach (i in 0..arg_count())
{
String a = Result::unwrap_or(arg(i), String::new());
match (String::parse<f64>(&a))
{
Ok(v) : total += v,
Err(_) :
{
String msg = String::new();
String::append(&mut msg, "argument ");
String::append(&mut msg, i);
String::append(&mut msg, " is not a number: ");
String::append(&mut msg, &a);
String::append(&mut msg, "\n");
printf("%v", &msg);
return 1;
},
}
}
String line = String::new();
String::append(&mut line, "total ");
String::append(&mut line, total);
String::append(&mut line, "\n");
printf("%v", &line);
0
}total 3.75
Numeric helpers in std::math (D-0181): clamp(x, lo, hi) holds a value within
bounds; gcd(a, b) is the greatest common divisor, never negative; is_nan(x) and is_finite(x)
ask about a float (an integer is always finite); and asin, acos and atan join the
floating-point functions. Vec::sort orders f32 and f64 too, with every NaN last.
Files
In std::fs.
read_file(path) reads a whole file as a String; write_file(path, text) makes the
file hold exactly text, creating it or replacing what it held. Both take the path, and the text,
as a StringView (D-0134): a literal, read_file("notes.txt"), or a String's whole-string view,
read_file(&path[0..$]) for a path that came from arg. They return a Result whose error is a
FileError: NotFound,
Denied, Utf8(e) for a file that is not valid UTF-8 (with the offset of the first bad byte), or
Io for anything else. A relative path is relative to the directory the program runs in. Both
tools read and write files. FileError::text(&e) is the error as a sentence for a person
("not found", "permission denied", …), as ParseError::text and Utf8Error::text are for
theirs (D-0117); %v of an error prints its variant's name instead.
| Function | Signature |
|---|---|
read_file | (StringView path) : Result<String, FileError> |
write_file | (StringView path, StringView text) : Result<void, FileError> |
These two handle the whole file at once; File, below, reads and writes one in pieces.
import std;
fn main() : u8
{
String text = "first line\nsecond line\n";
if (Err(_) = write_file("notes.txt", &text[0..$]))
{
printf("cannot write notes.txt\n");
return 1;
}
String back = Result::unwrap_or(read_file("notes.txt"), String::new());
printf("%v", &back);
match (read_file("no-such-file.txt"))
{
Ok(_) : printf("?\n"),
Err(NotFound) : printf("no-such-file.txt: not found\n"), // a pattern inside a pattern
Err(_) : printf("no-such-file.txt: cannot read it\n"),
}
0
}first line second line no-such-file.txt: not found
Open files: File
In std::fs.
A File is an open file, for what read_file cannot do: bytes that are not text, files
too large to hold, appending to a log. It is a resource, like Vec: it has one owner, can move to
another thread, and its destructor closes it. close closes it too, and reports whether that
worked (for a written file, whether the bytes were stored); after close the File is gone,
so it cannot be used again. Every operation returns a Result whose error is a FileError.
| Function | Does |
|---|---|
File::open(path) | open for reading; the file must exist (path a StringView, as for read_file) |
File::create(path) | open for writing, empty: created, or its contents discarded |
File::append(path) | open for writing at the end, created if missing |
File::open_rw(path) | open for reading and writing, created if missing, contents kept |
File::read_line(&mut f) | the next line without its \n (nor a \r before it), None at the end (as read_line) |
File::read(&mut f, &mut buf, max) | append up to max bytes to a Vec<u8>; how many, 0 at the end |
File::read_to_end(&mut f, &mut buf) | append the rest of the file to a Vec<u8> |
File::write(&mut f, &bytes[i..j]), File::write_text(&mut f, text) | write all of a slice of bytes, or text (a StringView: a literal, or &s[0..$]) |
File::printf(&mut f, "format", …) | write formatted text, as printf formats it |
File::seek(&mut f, pos), File::len(&f) | move to byte pos (a u64); the length in bytes |
File::create_new(path) | Ok(Some(file)) on a new file, Ok(None) when something is already there: a lock or pid file that two programs race for (D-0181) |
File::flush(&mut f), File::position(&f) | make everything written durable (fsync) — flush, then rename, for a file that must survive a crash; the byte the next read or write starts at (D-0181) |
File::close(f) | close it and say whether that worked |
read_bytes(path), write_bytes(path, &bytes[i..j]) | read_file and write_file for bytes: a Vec<u8> |
Reading is buffered, so read_line is quick, and File::read fills up to what was asked for.
Writing is buffered too (D-0201), so writing a file line by line is fast. The program itself always
sees what it wrote: the bytes reach the file before the same file's next operation, before any
other file operation of the program (opening, read_file, file_info, removing, renaming), before a
child process starts, and when the program ends, however it ends. Another program watching the file
while yours runs sees them at those points; File::flush writes them out at once.
import std;
fn main()
{
File log = Result::expect(File::create("app.log"), "cannot create the log");
foreach (i in 1..4)
{
_ = File::printf(&mut log, "event %v\n", i); // `_ =`: the result is not needed
}
_ = File::close(log);
File input = Result::expect(File::open("app.log"), "cannot open the log");
printf("%v bytes\n", Result::unwrap_or(File::len(&input), 0));
while (Ok(Some(line)) = File::read_line(&mut input)) // stops at the end, or an error
{
printf("read: %s\n", &line);
}
_ = File::seek(&mut input, 6);
Vec<u8> bytes = Vec::new();
_ = File::read(&mut input, &mut bytes, 1);
printf("byte 6 is %v\n", bytes[0]);
}24 bytes read: event 1 read: event 2 read: event 3 byte 6 is 49
Directories, the environment and the clocks
Directories, paths and file metadata in std::fs; the environment in std::env; the clocks in std::time.
Files live in directories, which a program can make, list and clean up; it can also read its
environment and measure time. Paths are StringViews, as for files, and failures are FileErrors.
| Function | Does |
|---|---|
make_dir(path) | make a directory: Ok(true) made, Ok(false) one was already there |
remove_file(path), remove_dir(path) | remove a file, or an empty directory |
rename(from, to) | move a file or directory, replacing a file at to |
list_dir(path) | the names in a directory, sorted: a Vec<String> |
path_kind(path) | File(len), Dir or Other; Err(NotFound) when nothing is there |
env_var(name) | Ok(Some(value)), or Ok(None) when the variable is not set |
current_dir() | the working directory, which relative paths start from |
monotonic_ns(), unix_seconds() | a clock for measuring time (never goes back), and the date |
sleep_ms(ms) | the calling thread does nothing for at least ms milliseconds; other threads run meanwhile (D-0124) |
make_dir_all(path), remove_dir_all(path) | make a directory and any missing ones above it; remove a directory and everything in it |
copy_file(from, to) | copy a file, replacing one at to; the number of bytes copied |
file_info(path) | a FileInfo: kind (as path_kind's), len, modified_unix (seconds since 1970) and readonly |
set_current_dir(path) | change the working directory, for the whole program |
temp_dir(), home_dir() | the system's directory for temporary files; the user's home directory, if known |
current_exe(), hostname() | the running program's own executable, for the files installed beside it; the machine's name, for a log line (D-0181) |
Paths have their own functions, because a path's grammar is the operating system's: / on
Unix, \ (and /) on Windows, where C:\ starts an absolute path. They read the text alone and
never touch the file system, except path_canonical, which asks the system where a path really
leads (following links; the path must exist). Use path_join rather than gluing with sprintf:
it puts in the right separator, only one, and an absolute tail replaces the base.
| Function | Gives |
|---|---|
path_join(base, tail) | base and tail with one separator between, as a String |
path_parent(p), path_file_name(p) | Some part, or None: /a/b and c.txt of /a/b/c.txt |
path_stem(p), path_extension(p) | archive.tar and gz of archive.tar.gz; .bashrc has a stem and no extension |
path_is_absolute(p), path_normalize(p) | whether p starts from a root; p with . and x/.. removed, by the text alone |
path_canonical(p) | the absolute path the system resolves p to |
import std;
fn main() : u8
{
DateTime day = DateTime::from_unix(1791022620);
String name = sprintf("app-%d-%02d-%02d.log", day.year, day.month, day.day);
String dir = path_join(&temp_dir()[0..$], "cobaltc-guide-logs/2026");
if (Err(_) = make_dir_all(&dir[0..$]))
{
return 1;
}
String log = path_join(&dir[0..$], &name[0..$]);
if (Err(_) = write_file(&log[0..$], "started\n"))
{
return 1;
}
String stem = Option::unwrap_or(path_stem(&log[0..$]), String::new());
String ext = Option::unwrap_or(path_extension(&log[0..$]), String::new());
printf("%s: stem %s, extension %s, absolute %v\n", &name, &stem, &ext, path_is_absolute(&log[0..$]));
match (file_info(&log[0..$]))
{
Ok(i) : printf("%u bytes, written in the last minute: %v\n", i.len, unix_seconds() - i.modified_unix < 60),
Err(e) : printf("file_info: %s\n", FileError::text(&e)),
}
String top = path_join(&temp_dir()[0..$], "cobaltc-guide-logs");
_ = remove_dir_all(&top[0..$]);
String up = path_normalize("logs/2026/../2025/./app.log");
printf("normalized: %s\n", &up);
0
}app-2026-10-03.log: stem app-2026-10-03, extension log, absolute true 8 bytes, written in the last minute: true normalized: logs/2025/app.log
import std;
fn kind(StringView p) : str
{
match (path_kind(p))
{
Ok(File(_)) : "file",
Ok(Dir) : "directory",
Ok(Other) : "other",
Err(NotFound) : "missing",
Err(_) : "error",
}
}
fn main() : u8
{
if (Err(_) = make_dir("work"))
{
return 1;
}
_ = write_file("work/notes.txt", "remember\n");
_ = rename("work/notes.txt", "work/old.txt");
match (list_dir("work"))
{
Ok(names) :
{
foreach (n in &names)
{
printf("%s\n", n);
}
},
Err(_) : return 1,
}
printf("notes.txt: %s, old.txt: %s\n", kind("work/notes.txt"), kind("work/old.txt"));
_ = remove_file("work/old.txt");
_ = remove_dir("work");
printf("work: %s\n", kind("work"));
u64 t0 = monotonic_ns();
printf("time goes forward: %v\n", monotonic_ns() >= t0);
0
}old.txt notes.txt: missing, old.txt: file work: missing time goes forward: true
Dates and times
In std::time.
A DateTime is a moment in UTC to the second: year (an i64), month, day, hour,
minute and second, all fields a program can read. It converts to and from the seconds of
unix_seconds() and to and from ISO 8601 text, the form logs, file names and protocols use. The
one time zone std knows is the machine's own: DateTime::to_local shifts a moment into it for
showing to a person; there are no named zones and no other zone's rules.
| Function | Does |
|---|---|
DateTime::now(), DateTime::from_unix(s) | the moment now, or s seconds after 1970-01-01T00:00:00Z (before it when negative); every i64 is a moment |
DateTime::new(y, mo, d, h, mi, s) | Some moment, or None when the fields name none (a 30 February, an hour 24, a second 60) |
DateTime::to_unix(&t) | the seconds since 1970; a DateTime written by hand with impossible fields is the fault diag.invalid-datetime |
DateTime::to_iso(&t), DateTime::from_iso(text) | 2026-10-03T10:17:00Z as a String, and back; from_iso also takes a date alone, as its midnight, and gives a ParseError for anything else |
DateTime::weekday(&t), Weekday::text(&w) | the day of the week, Monday … Sunday, and its name |
DateTime::eq(&a, &b), DateTime::less(&a, &b) | the same moment; earlier (fits Vec::sort_by) |
unix_ms() | the wall clock in milliseconds since 1970 |
DateTime::to_local(&t) | the same moment written in the machine's local time, daylight saving included, by the operating system's rule |
local_offset_seconds(unix) | how many seconds local time is ahead of UTC at that moment (negative when behind) |
Measure elapsed time with monotonic_ns(), never with the wall clock. The wall
clock is whatever the computer's owner or its time service last set it to, and it can jump backwards
while a program runs; unix_seconds(), unix_ms() and DateTime::now() say what time it is,
monotonic_ns() how long something took.
import std;
fn main() : u8
{
DateTime launch = DateTime::from_unix(1791022620);
String text = DateTime::to_iso(&launch);
Weekday w = DateTime::weekday(&launch);
printf("%s, a %s\n", &text, Weekday::text(&w));
DateTime due = match (DateTime::from_iso("2026-12-24"))
{
Ok(t) : t,
Err(e) :
{
printf("bad date: %s\n", ParseError::text(&e));
return 1;
},
};
i64 days = (DateTime::to_unix(&due) - DateTime::to_unix(&launch)) / 86400;
printf("%d whole days to %d-%02d-%02d\n", days, due.year, due.month, due.day);
match (DateTime::from_iso("2026-02-29"))
{
Ok(_) : printf("a leap day?\n"),
Err(e) : printf("2026-02-29: %s\n", ParseError::text(&e)),
}
0
}2026-10-03T10:17:00Z, a Saturday 81 whole days to 2026-12-24 2026-02-29: out of range
Local time is for showing, not for keeping. Store and log UTC, convert with
DateTime::to_local as the last step before printing, and print a local value by its fields: it is a
plain DateTime that knows nothing of its zone, so to_iso of it would end in Z and to_unix of it
is not the moment. The offset depends on the machine and on the moment (clocks change twice a year
in many zones), so this program's output differs from place to place:
import std;
fn main() : u8
{
DateTime now = DateTime::now();
DateTime here = DateTime::to_local(&now);
i64 offset = local_offset_seconds(DateTime::to_unix(&now));
str sign = if (offset < 0) { "-" } else { "+" };
i64 magnitude = if (offset < 0) { 0 - offset } else { offset };
printf(
"%s UTC is %04d-%02d-%02d %02d:%02d:%02d here (UTC%s%02d:%02d)\n",
&DateTime::to_iso(&now),
here.year,
here.month,
here.day,
here.hour,
here.minute,
here.second,
sign,
magnitude / 3600,
magnitude % 3600 / 60,
);
0
}Milliseconds and HTTP dates (D-0181). A log line or a JSON document wants the
moment to the millisecond: iso_from_unix_ms(unix_ms()) writes 2026-10-07T12:34:56.789Z, and
unix_ms_from_iso(text) reads it back (from_iso reads the same text and drops the fraction).
DateTime::to_http_date(&t) writes the form HTTP headers carry, Sun, 06 Nov 1994 08:49:37 GMT, and
DateTime::from_http_date(text) reads it and the two older forms a server may still send.
Running other programs
In std::process.
A Command names a program and its arguments; running it starts a child process. There is
no shell in between: each arg reaches the program exactly as written, spaces,
quotes, $ and ; included, so text from a user can be passed as an argument without any way to
run something else. A program that wants a shell's features runs one by name, /bin/sh with -c,
and then quoting is its own business.
| Function | Does |
|---|---|
Command::new(program), Command::arg(&mut c, a) | a command; one more argument |
Command::current_dir(&mut c, dir), Command::env(&mut c, name, value), Command::clear_env(&mut c) | where the child starts; an environment variable for it; start it with no inherited variables |
Command::output(&c), Command::output_with_input(&c, input) | run to the end; an Output with status, stdout and stderr (bytes) |
Command::status(&c) | run to the end with this program's own input and output; the status |
Command::spawn(&c) | start it and return a Child at once, its three streams piped to this program |
Child::write_input, close_input, read_output, read_output_line, read_error | talk to a running child, as to a File |
Child::wait(&mut ch), try_wait, kill | wait for the end (its status), ask without waiting, or end it at once |
A status is the program's exit code, or minus the signal that ended it (-9 after kill
on Unix). Failing to start is a FileError, as every input and output failure is: NotFound
when there is no such program. Dropping a
Child closes its pipes but leaves it running, like a job sent to the background; wait for it if
it should finish first.
import std;
fn main() : u8
{
Command c = Command::new("/bin/sh");
Command::arg(&mut c, "-c");
Command::arg(&mut c, "echo \"$1 world\"; echo oops >&2; exit 3");
Command::arg(&mut c, "sh");
Command::arg(&mut c, "hello; echo injected"); // one argument, never run as a command
match (Command::output(&c))
{
Ok(o) :
{
String out = Result::unwrap_or(String::from_utf8(Vec::clone(&o.stdout)), String::new());
printf("status %d, said: %s", o.status, &out);
printf("and %u bytes on standard error\n", Vec::len(&o.stderr));
},
Err(e) :
{
printf("could not run it: %s\n", FileError::text(&e));
return 1;
},
}
Command gone = Command::new("no-such-program-anywhere");
if (Err(NotFound) = Command::output(&gone))
{
printf("no-such-program-anywhere: not found\n");
}
0
}status 3, said: hello; echo injected world and 5 bytes on standard error no-such-program-anywhere: not found
Stopping cleanly
Pressing Ctrl-C, or the system asking a service to stop (SIGTERM), ends a program on the
spot. A server that should finish the request in hand and close its files first calls
watch_interrupts(): from then on an interrupt only sets a flag, and interrupt_requested() reads
it. There is no callback; the program's own loop looks at the flag, often enough. A blocking call
an interrupt arrives in may return early (a listener's accept and a socket's read give
Err(TimedOut)), and TcpListener::set_accept_timeout_ms bounds how long a quiet server waits
before looking again.
To end the program from anywhere, exit(status) (D-0150): the calling thread's scopes and
frames are unwound and every destructor runs, as they do when a fault ends the program, standard
output is flushed, and the process ends with status, with no diagnostic. Other threads take no
further steps. exit has type never, so it can stand in for any value: None : exit(2) in a
match arm, or exit(1) as a statement after an error message. Returning from main is still the
ordinary way to end; exit is for the place that knows the program is over and is not main.
import std;
resource struct Lock
{
i32 id;
}
fn Lock::drop(ref<Lock, exclusive> self)
{
printf("lock %d released\n", self.id);
}
fn check(i32 value) : i32
{
Lock inner = Lock { .id = 2 };
if (value < 0)
{
eprintf("negative input\n");
exit(3); // inner and outer are released first
}
value * 2
}
fn main() : u8
{
Lock outer = Lock { .id = 1 };
i32 r = check(-5);
printf("never printed %d\n", r);
0
}lock 2 released lock 1 released
import std;
fn main()
{
watch_interrupts();
u64 handled = 0;
while (!interrupt_requested() && handled < 3)
{
handled += 1; // one unit of work: a request, a job, a file
}
printf("handled %u, stopping cleanly\n", handled);
}handled 3, stopping cleanly
Also (D-0181): Child::read_error_line(&mut ch) reads the child's standard error a
line at a time, as read_output_line does its output; Child::terminate(&mut ch) asks it to stop
(SIGTERM on Unix, which a program may catch to finish cleanly) where kill ends it at once; and
process_id() is this program's own number, for a pid file. kill and terminate signal the child
alone: a sh -c "…" killed leaves the commands it started running. Child::kill_tree(&mut ch) (D-0205)
ends the child and every process it started, and theirs (on Linux, found from /proc; on Windows with
taskkill /T).
Streaming between processes (D-0195). A Child's pipes all belong to it, and
every call borrows it exclusively, so one thread cannot write a child's input while another reads
its output. Child::take_input(&mut ch) and Child::take_output(&mut ch) move a pipe out into a
ChildInput or ChildOutput of its own (None if it was already taken): give one to a thread
(spawn(pump, out_a, in_b)) and keep reading the other child here. ChildInput::write,
ChildOutput::read and read_line work as the Child's own calls do; dropping an end closes it, and
an end outlives its Child.
Cryptography, and bytes as text
In std::crypto, with hexadecimal and base64 in std::encoding::hex and std::encoding::base64.
spec/21 §0 names, tested against their published vectors and against OpenSSL and Python's cryptography on loopback and in sweeps, fuzzed continuously (impl/fuzz); not independently audited.sha256(data) is the SHA-256 digest of some bytes, 32 of them as an array<u8, 32>;
hmac_sha256(key, data) is a message authentication code under a key, the way a webhook body,
a session cookie or a signed request is protected. Both come in a streaming form for a message that
arrives in pieces: Sha256::new(), Sha256::update(&mut h, piece) as often as needed,
Sha256::finish(h); HmacSha256 the same. A Sha256 is a plain value, so a copy of a half-done
computation continues on its own. Everything is written in CobaltC (the spec lists it), so the two
tools compute exactly the same bytes; for a message of a few kilobytes the interpreter's speed does
not matter.
Check a MAC with digest_eq, never with ==. digest_eq(a, b) compares every byte
whatever the first difference, so how long the check takes says nothing about how many leading bytes
a sender guessed right. A digest is shown with to_hex and read back with from_hex; bytes that
must travel as text go through base64_encode and base64_decode (RFC 4648, padded). Random keys
come from os_random_bytes. A secret is rarely used as it arrives: hkdf_sha256(salt, secret, info, len)
derives len bytes of key material from it, bound to what they are for by info, so one secret
never keys two purposes (RFC 5869; TLS 1.3's key schedule is this function). When a protocol or
a file format names its hash, hash(kind, data), Hasher, hmac(kind, key, data), Hmac and the
hkdf_*_with(kind, …) forms take a HashKind (Sha2_256, Sha2_384 or Sha2_512) at run time;
the SHA-256 forms are the same functions with Sha2_256.
Encrypting. chacha20_poly1305_seal(key, nonce, aad, plaintext) encrypts and
authenticates (RFC 8439): a 32-byte key, a 12-byte nonce that must never repeat under one key, and
aad, data sent in the clear but protected from change; the result is the ciphertext and a 16-byte
tag. chacha20_poly1305_open gives the plaintext back, or None if a single bit of the message, the
tag or the aad was changed, and never any part of a refused message. Agreeing on a
key. Each side makes x25519_private_key(), sends x25519_public_key(&private[0..$]), and
computes x25519(&private[0..$], &their_public[0..$]): both get the same 32 bytes, which nobody who
saw only the public keys can (RFC 7748); None means the other side sent a key that must be refused.
Feed the shared secret to hkdf_sha256, never use it as a key directly. The NIST
alternatives. aes_gcm_seal and aes_gcm_open are the same pair with AES-GCM (NIST SP
800-38D), for formats and protocols that require it: the key is 16, 24 or 32 bytes (AES-128, AES-192
or AES-256), the nonce 12. p256_private_key, p256_public_key and p256_ecdh agree on a key over
the P-256 curve; the public key is 65 bytes, and None means a key that must be refused. Where you
choose, X25519 agrees on a key in about half the time P-256 takes; the two AEADs run at about the
same speed (AES-128-GCM and ChaCha20-Poly1305 each about 50 ms a MiB here). All of these take the same steps
whatever the secret bytes are, so their timing reveals nothing but lengths; a key or nonce of the
wrong length is the fault diag.crypto-length.
import std;
fn main()
{
array<u8, 32> alice = x25519_private_key();
array<u8, 32> bob = x25519_private_key();
array<u8, 32> alice_public = x25519_public_key(&alice[0..$]);
array<u8, 32> bob_public = x25519_public_key(&bob[0..$]);
array<u8, 32> shared = Option::unwrap(x25519(&alice[0..$], &bob_public[0..$]));
Vec<u8> nothing = Vec::new();
Vec<u8> key = hkdf_sha256(¬hing[0..$], &shared[0..$], &b"example key"[0..$], 32);
Vec<u8> nonce = Vec::filled(12, 0: u8);
Vec<u8> sealed = chacha20_poly1305_seal(&key[0..$], &nonce[0..$], &b"header"[0..$], &b"attack at dawn"[0..$]);
// Bob derives the same key from his side of the exchange.
array<u8, 32> bobs = Option::unwrap(x25519(&bob[0..$], &alice_public[0..$]));
Vec<u8> bob_key = hkdf_sha256(¬hing[0..$], &bobs[0..$], &b"example key"[0..$], 32);
match (chacha20_poly1305_open(&bob_key[0..$], &nonce[0..$], &b"header"[0..$], &sealed[0..$]))
{
Some(p) : printf("bob reads: %s\n", &Result::unwrap_or(String::from_utf8(p), String::new())),
None : printf("refused\n"),
}
sealed[0] = sealed[0] ^ 1;
printf(
"changed in transit: %s\n",
if (Option::is_none(&chacha20_poly1305_open(
&bob_key[0..$],
&nonce[0..$],
&b"header"[0..$],
&sealed[0..$],
))) { "refused" } else { "accepted" },
);
}bob reads: attack at dawn changed in transit: refused
| Function | Does |
|---|---|
sha256(data), Sha256::new(), Sha256::update(&mut h, data), Sha256::finish(h) | the SHA-256 digest, in one call or in pieces: an array<u8, 32> |
hmac_sha256(key, data), HmacSha256::new(key), update, finish | HMAC-SHA-256 under key (any length; a long key is hashed first) |
digest_eq(a, b) | the same bytes, compared in a time that depends only on the lengths |
hkdf_sha256(salt, ikm, info, len), hkdf_extract(salt, ikm), hkdf_expand(prk, info, len) | HKDF (RFC 5869): len bytes of key material derived from a secret, bound to info; at most 8160 bytes from one extracted key |
chacha20_poly1305_seal(key, nonce, aad, plaintext), chacha20_poly1305_open(key, nonce, aad, sealed) | authenticated encryption (RFC 8439): ciphertext and tag; the plaintext back, or None for anything changed |
chacha20(key, nonce, counter, data), poly1305(key, message) | the stream cipher and the one-time MAC the AEAD is built from |
aes_gcm_seal(key, nonce, aad, plaintext), aes_gcm_open(key, nonce, aad, sealed), aes_encrypt_block(key, block) | AES-GCM (SP 800-38D) with a 16-, 24- or 32-byte key, as the ChaCha20-Poly1305 pair; the bare AES block cipher (FIPS 197) |
x25519_private_key(), x25519_public_key(private), x25519(private, their_public) | key agreement (RFC 7748): a key pair, and the shared secret, None for a public key that must be refused |
p256_private_key(), p256_public_key(private), p256_ecdh(private, their_public) | key agreement over P-256: a 65-byte public key, and the 32-byte shared secret, None for a key that must be refused |
sha512(data), sha384(data), Sha512, Sha384 | the 64- and 48-byte SHA-2 digests, one-shot or streaming |
hash(kind, data) | the digest by a HashKind: Sha2_256, Sha2_384 or Sha2_512 |
rsa_verify_pkcs1v15(&key, kind, digest, sig), rsa_verify_pss(&key, kind, digest, sig) | whether an RSA signature (PKCS #1 v1.5, or PSS with a digest-length salt) is the key's over that digest |
ecdsa_verify(curve, public_key, digest, sig) | whether a DER ECDSA signature on P256 or P384 is the key's over that digest |
hkdf_expand_label(secret, label, context, len) | TLS 1.3's key-schedule step (RFC 8446 §7.1) |
Hasher::new(kind), hmac(kind, key, data), Hmac::new(kind, key), hkdf_with(kind, salt, ikm, info, len), hkdf_extract_with, hkdf_expand_with, hkdf_expand_label_with | the same over a HashKind chosen at run time (SHA-256, SHA-384 or SHA-512); results are Vec<u8> |
ed25519_private_key(), ed25519_public_key(private), ed25519_sign(private, message), ed25519_verify(public, message, sig) | Ed25519 (RFC 8032): 32-byte keys, 64-byte deterministic signatures over the whole message |
ec_private_key(curve), ec_public_key(curve, private), ecdh(curve, private, their_public), ecdsa_sign(curve, private, digest) | P-256 or P-384 keys, key agreement, and a DER ECDSA signature over a digest with an RFC 6979 nonce |
rsa_generate_key(bits), RsaPrivateKey::new(n, e, d, p, q), RsaPrivateKey::public_key(&key), rsa_sign_pkcs1v15(&key, kind, digest), rsa_sign_pss(&key, kind, digest) | an RSA key (1024 to 8192 bits), and signatures in the two encodings the verify functions check |
hash_password(password), verify_password(stored, password) | Argon2id password storage as a PHC string, and its check |
argon2id(password, salt, passes, memory_kib, lanes, len), pbkdf2(kind, password, salt, iterations, len) | the raw key-derivation functions (RFC 9106, RFC 8018) |
blake2b(data, len), Blake2b::new(len), Blake2b::new_keyed(key, len), update, finish | BLAKE2b (RFC 7693): a digest of 1 to 64 bytes, optionally keyed |
to_hex(bytes), from_hex(text) | lowercase hexadecimal and back (ParseError for a non-digit or an odd count) |
base64_encode(bytes), base64_decode(text) | base64 with = padding and back (ParseError for a bad character or length) |
import std;
fn main()
{
array<u8, 32> d = sha256(&b"abc"[0..$]);
printf("%s\n", &to_hex(&d[0..$]));
array<u8, 32> mac = hmac_sha256(&b"Jefe"[0..$], &b"what do ya want for nothing?"[0..$]);
printf("%s\n", &to_hex(&mac[0..$]));
array<u8, 32> again = hmac_sha256(&b"Jefe"[0..$], &b"what do ya want for nothing?"[0..$]);
printf("verified: %v\n", digest_eq(&mac[0..$], &again[0..$]));
printf("%s\n", &base64_encode(&b"foobar"[0..$]));
}ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 verified: true Zm9vYmFy
Signatures. Three families, each with a key pair, a signing function and a
verifying one. Ed25519 (RFC 8032) is the simplest and the one to choose where you can:
ed25519_private_key(), ed25519_public_key(&private[0..$]), ed25519_sign(&private[0..$],
message) gives 64 bytes, ed25519_verify(&public[0..$], message, &sig[0..$]) says whether they are
that key's signature of that message. Signing is deterministic: the same key and message give the
same bytes, and no random number is involved that could repeat. ECDSA on P256 or P384
(what certificates and TLS mostly carry): ec_private_key(curve), ec_public_key(curve, &k[0..$])
(the uncompressed point, 65 or 97 bytes), ecdsa_sign(curve, &k[0..$], &digest[0..$]) (a DER
signature over the digest made with hash; the nonce is derived from the key and the digest as
RFC 6979 says, so it too is deterministic) and ecdsa_verify(curve, public, digest, sig); the same
keys do key agreement with ecdh(curve, &k[0..$], &their_public[0..$]), of which the p256_*
functions are the P-256 case. RSA: rsa_generate_key(2048) makes an RsaPrivateKey
(minutes under the interpreter, seconds compiled), or RsaPrivateKey::new(n, e, d, p, q) builds one
from numbers read elsewhere (None if they do not fit together); rsa_sign_pkcs1v15(&key, kind,
&digest[0..$]) and rsa_sign_pss(&key, kind, &digest[0..$]) sign the digest in the two standard
encodings, RsaPrivateKey::public_key(&key) gives the RsaPublicKey that rsa_verify_pkcs1v15 and
rsa_verify_pss check with. Signing with any of them takes the same steps whatever the key; the
RSA operation is blinded and checked against the public key before its result leaves. Verification
uses only public values and answers false for anything malformed.
Keys in files. Keys travel in the encodings every tool writes, in
std::x509: a public key as a SubjectPublicKeyInfo (CertKey::from_der, from_pem, to_der,
to_pem), a private key as PKCS #8 (PrivateKey::from_der, from_pem, to_der, to_pem), where
from_pem also reads the older RSA PRIVATE KEY and EC PRIVATE KEY blocks. A PrivateKey is
RsaPrivate, EcPrivate or Ed25519Private; PrivateKey::public_key(&key) is its CertKey, and
sign(&key, &scheme, message) signs a whole message under a SignatureScheme the way
verify_signature checks it, hashing first where the scheme says so. A certificate's key is a
CertKey too, so a program can check a signature against a certificate it parsed.
import std;
fn main()
{
array<u8, 32> private = ed25519_private_key();
array<u8, 32> public = ed25519_public_key(&private[0..$]);
array<u8, 64> sig = ed25519_sign(&private[0..$], &b"release 1.4.0"[0..$]);
printf("genuine: %v\n", ed25519_verify(&public[0..$], &b"release 1.4.0"[0..$], &sig[0..$]));
printf("altered: %v\n", ed25519_verify(&public[0..$], &b"release 1.4.1"[0..$], &sig[0..$]));
// The same key, written the way openssl would.
PrivateKey key = Ed25519Private(private);
String pem = PrivateKey::to_pem(&key);
printf("%s", StringView::sub(String::as_view(&pem), 0, 27));
printf("\nread back: %v\n", Result::is_ok(&PrivateKey::from_pem(String::as_view(&pem))));
}genuine: true altered: false -----BEGIN PRIVATE KEY----- read back: true
Passwords. Store hash_password(&password[0..$]), a text like
$argon2id$v=19$m=19456,t=2,p=1$…$… that records the Argon2id parameters and the random salt beside
the result; check a login with verify_password(stored, &attempt[0..$]). Argon2id (RFC 9106) costs
memory as well as time (19 MiB and two passes here, the OWASP recommendation), so a stolen table
cannot be searched at hardware speed; the parameters can be raised later and old entries still
verify, since each carries its own. Never store a plain sha256 of a password, salted or not. For
the formats that predate Argon2, pbkdf2(kind, password, salt, iterations, len) is PBKDF2 over
HMAC, and argon2id(password, salt, passes, memory_kib, lanes, len) is the raw function.
blake2b(data, len) (RFC 7693, up to 64 bytes, Blake2b::new_keyed for a keyed MAC) is the fast
hash Argon2 is built on. Under the interpreter hash_password takes many minutes (19 MiB of
memory touched twice, word by word); compiled, well under a second.
SHA-1 is there too (Sha1, sha1, and Sha1 as a HashKind for hmac,
pbkdf2 and Hasher, D-0181), because older protocols still require it: RFC 6238's one-time
passwords default to HMAC-SHA-1, and git names its objects by it. It is legacy — collisions have been
made — so it is for checking what others produce, never for a new signature.
Big integers
In std::math.
A BigUint is an unsigned integer of any size: add, sub, mul, div_rem (a BigDivision
of quotient and remainder), rem, mod_pow, mod_inverse, clone, shifts, eq and less, and conversions to and from
big-endian bytes and decimal text. It owns its digits, so it is a resource: reassign one with
overwrite, and take a division apart by destructuring. Its operations take time that depends on the
numbers, so it is for public values, never for a secret key.
import std;
fn main()
{
BigUint f = BigUint::from_u64(1);
for (u64 i = 2; i <= 30; i += 1)
{
overwrite(&mut f, BigUint::mul(&f, &BigUint::from_u64(i)));
}
printf("30! = %s\n", &BigUint::text(&f));
BigDivision { quotient, remainder } = BigUint::div_rem(&f, &BigUint::from_u64(1000000007));
printf("mod 1000000007 = %s\n", &BigUint::text(&remainder));
}30! = 265252859812191058636308480000000 mod 1000000007 = 109361473
Certificates
In std::x509.
spec/21 §0 names, tested against certificates and keys OpenSSL made and against the system's root bundle, fuzzed continuously (impl/fuzz); not independently audited.Certificate::parse(der) reads an X.509 certificate; certificates_from_pem(text) reads every
certificate in PEM text. A TrustStore holds the roots a chain may end at: TrustStore::system() reads
the operating system's bundle on Unix (on Windows, pass a bundle to TrustStore::from_pem_file).
verify_chain(&leaf, &intermediates[0..$], &roots, host, &now) decides whether a server's certificates
prove it is host: the leaf names the host (a DNS name, a one-label wildcard, or an IP address; never
the common name), every certificate is in date and signed by the next, every issuer is a CA, and the
chain ends at a root. The error says why not: HostMismatch, Expired, UnknownAuthority and so on.
Revocation (CRL, OCSP) is not checked.
TLS
In std::tls.
s_client and s_server on loopback, fuzzed continuously (impl/fuzz); not independently audited.TlsStream::connect(addr, server_name, &roots) opens a TLS 1.3 connection: it checks the
server's certificate chain against roots and server_name, and then read, read_exact, write
and close carry data encrypted and authenticated. TlsStream::client(tcp, …) does the same over a
TcpStream the program connected and configured itself (its timeouts apply). The client speaks only
TLS 1.3, with ChaCha20-Poly1305, AES-128-GCM or AES-256-GCM, X25519, P-256 or P-384, and ECDSA,
RSA-PSS or Ed25519 signatures: the set every TLS 1.3 server must support and the common additions; a
failure is a TlsError saying whether it was the network, the certificate, the handshake or an
alert. The example is shown, not run: it needs the network.
TrustStore::system()
alone parses every root certificate). Many servers close a connection whose handshake stalls for
about ten seconds, and the interpreter's client then fails with the network error closed; under
cobc the same program completes in well under a second.import std;
fn main() : u8
{
TrustStore roots = match (TrustStore::system())
{
Ok(t) : t,
Err(_) : return 1,
};
IpAddr ip = Result::unwrap(IpAddr::parse("1.1.1.1"));
TlsStream s = match (TlsStream::connect(SocketAddr::new(ip, 853), "cloudflare-dns.com", &roots))
{
Ok(s) : s,
Err(e) :
{
eprintf("tls: %s\n", TlsError::text(&e));
return 1;
},
};
// … a DNS-over-TLS query: a 2-byte length, then the DNS message …
_ = TlsStream::close(&mut s);
0
}Serving TLS (D-0186). TlsStream::server(tcp, &key, &chain[0..$]) performs the
handshake as the server, with a PrivateKey (RSA, P-256, P-384 or Ed25519, read with
PrivateKey::from_pem) and the server's certificate chain (certificates_from_pem, the server's
own certificate first); the client's first suite, key share and the key's signature scheme are
chosen, and a client that cannot agree is answered with an alert. Afterwards read_line, read,
write and close work as on the client side, and peer_certificate (None on a server, the
server's certificate on a client) is there for pinning. TcpStream::try_clone has no counterpart
on a TlsStream: its record keys are one state. The example is shown, not run: it needs a key and
a certificate (openssl req -x509 -newkey ed25519 -nodes -subj /CN=localhost -addext
subjectAltName=DNS:localhost makes a self-signed pair for development).
import std;
fn main() : u8
{
PrivateKey key = Result::unwrap(PrivateKey::from_pem(String::as_view(&Result::unwrap(read_file("server.key")))));
Vec<Certificate> chain = certificates_from_pem(String::as_view(&Result::unwrap(read_file("server.crt"))));
TcpListener l = Result::unwrap(TcpListener::bind(Result::unwrap(SocketAddr::parse("0.0.0.0:8443"))));
while (true)
{
TcpStream tcp = Result::unwrap(TcpListener::accept(&mut l));
TlsStream s = match (TlsStream::server(tcp, &key, &chain[0..$]))
{
Ok(s) : s,
Err(e) :
{
eprintf("handshake: %s\n", TlsError::text(&e));
continue;
},
};
while (Some(line) = Result::unwrap_or(TlsStream::read_line(&mut s), None))
{
_ = TlsStream::write_text(&mut s, "echo: ");
_ = TlsStream::write_text(&mut s, String::as_view(&line));
_ = TlsStream::write_text(&mut s, "\n");
}
_ = TlsStream::close(&mut s);
}
0
}HTTP
In std::http.
impl/fuzz); not independently audited.http_get(url) fetches a URL and http_post(url, content_type, body) posts to one; each gives
a Response with its status, reason, headers and whole body, or an HttpError saying what
went wrong (the network, TLS, a malformed URL or message, a limit). Both follow redirects, decode
chunked bodies and speak https through std::tls with the system roots. A program that makes more
than one request, or wants a timeout, a header on every request or a smaller body limit, keeps an
HttpClient and calls HttpClient::send(&mut client, &request): the roots are loaded once, at the
first https request. Url::parse takes a URL apart (scheme, host, port, path, query),
percent_encode and percent_decode handle the escaping, find_header (or
Response::header) finds a header whatever its case, and Response::text gives the body as a String
(an error when it is not UTF-8). HTTP/1.1 only and one connection per request, unless the
client's keep_alive is set (D-0195): then the connection stays open while the server allows, and
the next request to the same host and port goes on it (a server for such a client must not count
on one connection per request). If that connection turns out closed before any answer, a GET, HEAD,
PUT, DELETE, OPTIONS or TRACE is sent again on a fresh one; a POST is not -- the server may have acted
on it -- and is ConnectionClosed for the program to decide. No compression and no cookies; what a server sends compressed it sends only when asked, so plain
requests get plain bodies.
Serving. HttpServer::bind(addr) listens; HttpServer::accept(&mut server)
gives each client's HttpConnection, on which HttpConnection::request(&mut c) parses the next
request (Ok(None) once the client is done) and HttpConnection::respond(&mut c, status, &headers,
body) writes the answer with the right Content-Length, reason phrase and Connection header,
keeping the connection open when the client asked for that. A malformed request is an Err the
program answers with 400 (or 413 for one over max_body); the connection then closes. Handle each
connection in its own thread with spawn, as the example does; the server speaks plain HTTP (a TLS
server is a later decision). The example is hermetic: its client and server are the same program.
import std;
fn serve(HttpConnection c) : u8
{
while (true)
{
Vec<Header> headers = Vec::new();
Vec::push(&mut headers, Header::new("Content-Type", "text/plain"));
match (HttpConnection::request(&mut c))
{
Ok(Some(req)) :
{
if (req.url == "/health")
{
_ = HttpConnection::respond(&mut c, 200, &headers[0..$], &b"ok\n"[0..$]);
}
else if (req.method == "POST" && req.url == "/echo")
{
_ = HttpConnection::respond(&mut c, 200, &headers[0..$], &req.body[0..$]);
}
else
{
_ = HttpConnection::respond(&mut c, 404, &headers[0..$], &b"no such page\n"[0..$]);
}
},
Ok(None) : return 0,
Err(_) :
{
_ = HttpConnection::respond(&mut c, 400, &headers[0..$], &b"bad request\n"[0..$]);
return 1;
},
}
}
0
}
fn accept_two(HttpServer s) : u8
{
for (usize i = 0; i < 2; i += 1)
{
match (HttpServer::accept(&mut s))
{
Ok(c) : { _ = join(spawn(serve, c)); },
Err(_) : return 1,
}
}
0
}
fn main()
{
HttpServer s = Result::unwrap(HttpServer::bind(SocketAddr::new(IpAddr::localhost_v4(), 0)));
String base = sprintf("http://127.0.0.1:%u", HttpServer::local_addr(&s).port);
auto server = spawn(accept_two, s);
match (http_get(String::as_view(&sprintf("%s/health", &base))))
{
Ok(r) : printf("%u %s: %s", r.status, &r.reason, &Result::unwrap_or(Response::text(&r), String::new())),
Err(e) : printf("error: %s\n", HttpError::text(&e)),
}
match (http_post(String::as_view(&sprintf("%s/echo", &base)), "text/plain", &b"hello there"[0..$]))
{
Ok(r) : printf("%u, %s echoed %v bytes: %s\n", r.status, Option::unwrap_or(Response::header(&r, "content-type"), StringView::of("?")), Vec::len(&r.body), &Result::unwrap_or(Response::text(&r), String::new())),
Err(e) : printf("error: %s\n", HttpError::text(&e)),
}
printf("server thread: %u\n", join(server));
}200 OK: ok 200, text/plain echoed 11 bytes: hello there server thread: 0
https, forms and compressed bodies. HttpServer::bind_tls(addr, key, chain)
makes the same server speak https (D-0186): accept completes each client's TLS handshake first,
and gives Err(HttpTlsFailure(_)) for a client that fails it, so accept's error is an HttpError.
A request's query string and a form body are read with Url::query_pairs(&url) and
form_decode(text), each a Vec<Param> (name, value, compared exactly) searched with
find_param; form_encode writes one (D-0181). The client offers Accept-Encoding: gzip, deflate
and decodes a body a server sends that way, dropping the header, so r.body is always the plain
bytes (D-0183).
import std;
fn main()
{
Vec<Param> form = form_decode("name=Ada+Lovelace¬e=100%25&tag=x&tag=y");
foreach (p in &form)
{
printf("%v = %v\n", &p.name, &p.value);
}
printf("%v\n", Option::unwrap_or(find_param(&form[0..$], "note"), StringView::of("-")));
printf("%v\n", &form_encode(&form[0..2]));
Url u = Result::unwrap(Url::parse("https://example.org/search?q=cobalt%20c&page=2"));
printf("%v\n", Option::unwrap_or(find_param(&Url::query_pairs(&u)[0..$], "q"), StringView::of("-")));
}name = Ada Lovelace note = 100% tag = x tag = y 100% name=Ada+Lovelace¬e=100%25 cobalt c
JSON
In std::encoding::json.
Json::parse(text) reads a JSON document (RFC 8259) into a tree of Json values — JsonNull,
JsonBool, JsonNumber (an f64), JsonText, JsonArray and JsonObject (its members in order) —
and Json::text(&j) writes one back compact, Json::pretty(&j) indented. Reading a tree:
Json::get(&j, "key") and Json::at(&j, i) give a reference to a member or an element, or None,
and as_text, as_i64, as_f64, as_bool, as_array, as_object and is_null ask what a value is.
Building one: Json::set(&mut obj, "key", value) and Json::push(&mut arr, value). A document that is
not JSON is Err(Invalid(byte)), nothing at all Err(Empty), a number beyond f64 or nesting past
512 levels Err(OutOfRange). Every escape is handled, surrogate pairs included; a whole number is
written without a fraction; a NaN or an infinity, which JSON cannot carry, is written null
(D-0182).
import std;
fn main()
{
Json j = Result::unwrap(Json::parse("{\"name\": \"Ada\", \"born\": 1815, \"langs\": [\"Analytical\", \"Engine\"]}"));
StringView name = Option::unwrap_or(Json::as_text(Option::unwrap(Json::get(&j, "name"))), StringView::of("?"));
i64 born = Option::unwrap_or(Json::as_i64(Option::unwrap(Json::get(&j, "born"))), 0);
printf("%v, born %v, knew", name, born);
foreach (l in Option::unwrap(Json::as_array(Option::unwrap(Json::get(&j, "langs")))))
{
printf(" %v", Option::unwrap_or(Json::as_text(l), StringView::of("?")));
}
printf("\n");
Json out = JsonObject(Vec::new());
Json::set(&mut out, "name", JsonText(String::from_str("Ada")));
Json langs = JsonArray(Vec::new());
Json::push(&mut langs, JsonText(String::from_str("Analytical")));
Json::set(&mut out, "langs", langs);
Json::set(&mut out, "ok", JsonBool(true));
printf("%v\n%v\n", &Json::text(&out), &Json::pretty(&out));
match (Json::parse("{\"a\": [1, 2,]}"))
{
Err(Invalid(at)) : printf("not JSON at byte %v\n", at),
_ : {},
}
}Ada, born 1815, knew Analytical Engine
{"name":"Ada","langs":["Analytical"],"ok":true}
{
"name": "Ada",
"langs": [
"Analytical"
],
"ok": true
}
not JSON at byte 12
Compression
In std::compress.
deflate(data) and inflate(data, max) are DEFLATE (RFC 1951), the format under zip, PNG and
git; zlib_deflate/zlib_inflate wrap it with zlib's header and Adler-32 (RFC 1950), and
gzip/gunzip with gzip's header and CRC-32 (RFC 1952), so a .gz file or an HTTP body is one call
away. Decompression takes a max, the most it may produce, and answers Err(Oversize) beyond it — a
small stream can expand without bound, so a limit is always there; Truncated is a stream cut short
and Corrupt(byte) one that is not what it claims, a wrong checksum included. Compression uses one
fixed strategy, so every implementation writes the same bytes, and reads everything other tools
write; adler32 is there beside crc32 (D-0183).
import std;
fn main()
{
String text = String::new();
foreach (i in 0..20)
{
String::append(&mut text, "the same line again and again\n");
}
Vec<u8> packed = gzip(StringView::as_bytes(String::as_view(&text)));
printf("%v bytes became %v\n", String::len(&text), Vec::len(&packed));
Vec<u8> back = Result::unwrap(gunzip(&packed[0..$], 1 << 20));
printf("round trip: %v\n", Vec::len(&back) == String::len(&text) && digest_eq(&back[0..$], StringView::as_bytes(String::as_view(&text))));
match (gunzip(&packed[0..10], 1 << 20))
{
Err(e) : printf("cut short: %s\n", CompressError::text(&e)),
Ok(_) : {},
}
}600 bytes became 52 round trip: true cut short: compressed data ends early
Databases
In std::database::postgres, re-exported by std::database.
impl/fuzz); not independently audited.A PostgreSQL client, speaking the server's own wire protocol over std::net and std::tls, with
SCRAM-SHA-256 from std::crypto; nothing is linked, so a program that talks to a database still
needs only a C compiler. PgConnection::connect(url) opens a connection from a
postgres://user:password@host:port/database URL (the port 5432 and the database the user's name
when left out), encrypting with TLS and verifying the server's certificate against the system roots
unless the URL says ?sslmode=disable; connect_with(url, &roots, timeout_ms) takes its own
TrustStore (a self-signed server's issuer, say) and timeout. There is no unverified TLS.
PgConnection::query(&mut c, sql, ¶ms[0..$]) runs one statement with $1, $2, … bound to
the PgValues given (Null, Text, Int, Float, Bool, Bytes): a parameter is sent beside
the statement, never spliced into it, so no quoting is ever needed. The PgResult has the
columns (name and type OID), the rows (each cell Some(text) as the server prints the value,
or None for NULL; read it with PgRow::get(&row, i) and parse it with StringView::parse<T>),
the affected count and the command tag. execute is query for a statement without rows,
giving the count; run_script runs a file of ;-separated statements without parameters, for
schemas and migrations. Transactions are statements: execute(&mut c, "BEGIN", &none[0..$]) and
"COMMIT" or "ROLLBACK".
An error the server reports is Err(Server(e)) with its severity, SQLSTATE code, message,
detail, hint and position; the connection stays usable after it, as it does after a result
over max_result (16 MiB by default, ResultTooLarge). A network or protocol failure closes the
connection, and every later call gives Disconnected. Notices the server sends collect in
c.notices for the program to read and clear. PgError::text(&e) is a String saying what
went wrong, the server's message included. One connection serves one thread; a program that
queries from several keeps one per thread.
| Function | Does |
|---|---|
PgConnection::connect(url), connect_with(url, &roots, timeout_ms) | open a connection (30 s timeout; the system roots), or with the roots and timeout given |
PgConnection::query(&mut c, sql, ¶ms[0..$]) | one statement with $n bound to the PgValues: its PgResult (columns, rows, affected, tag) |
PgConnection::execute(&mut c, sql, ¶ms[0..$]) | the same, giving only the affected count |
PgConnection::run_script(&mut c, sql) | several ;-separated statements, no parameters, results dropped, the first error given |
PgConnection::parameter(&c, name), c.notices, c.max_result | a setting the server reported at startup (server_version, …); the notices received; the most a result may take |
PgRow::get(&row, i), PgResult::column(&res, name) | a cell's text (None for NULL or past the end); a column's index by name |
PgConnection::close(&mut c) | tell the server goodbye and close (a dropped connection is just closed) |
PgUrl::parse(text), PgValue::text(&v) | the URL taken apart; the text a parameter is sent as |
import std;
fn main() : u8
{
PgConnection c = match (PgConnection::connect("postgres://app:secret@db.internal/shop"))
{
Ok(c) : c,
Err(e) :
{
eprintf("connect: %s\n", &PgError::text(&e));
return 1;
},
};
Vec<PgValue> none = Vec::new();
if (Err(e) = PgConnection::run_script(&mut c, "CREATE TABLE IF NOT EXISTS orders (id serial PRIMARY KEY, item text, qty int)"))
{
eprintf("schema: %s\n", &PgError::text(&e));
return 1;
}
Vec<PgValue> order = Vec::new();
Vec::push(&mut order, Text(String::from_str("widget")));
Vec::push(&mut order, Int(3));
match (PgConnection::execute(&mut c, "INSERT INTO orders (item, qty) VALUES ($1, $2)", &order[0..$]))
{
Ok(n) : printf("inserted %v\n", n),
Err(e) : printf("insert: %s\n", &PgError::text(&e)),
}
Vec<PgValue> at_least = Vec::new();
Vec::push(&mut at_least, Int(2));
match (PgConnection::query(&mut c, "SELECT id, item, qty FROM orders WHERE qty >= $1 ORDER BY id", &at_least[0..$]))
{
Ok(res) :
{
foreach (row in &res.rows)
{
StringView item = Option::unwrap_or(PgRow::get(row, 1), StringView::of("?"));
i64 qty = Result::unwrap_or(StringView::parse<i64>(Option::unwrap_or(PgRow::get(row, 2), StringView::of("0"))), 0);
printf("%s x %v\n", item, qty);
}
},
Err(Server(e)) : printf("%s: %s\n", &e.code, &e.message),
Err(e) : printf("query: %s\n", &PgError::text(&e)),
}
PgConnection::close(&mut c);
0
}Without a server the pieces can still be seen: the URL's defaults and the text each parameter is sent as.
import std;
fn main()
{
PgUrl u = Result::unwrap(PgUrl::parse("postgresql://app@db.internal"));
printf("%s@%s:%u/%s tls=%v\n", &u.user, &u.host, u.port, &u.database, match (u.sslmode) { Disable : false, VerifyFull : true });
Vec<PgValue> params = Vec::new();
Vec::push(&mut params, Int(-7));
Vec::push(&mut params, Float(0.5));
Vec::push(&mut params, Bool(true));
Vec::push(&mut params, Bytes(Vec::from_slice(&b"\x00\xff"[0..$])));
Vec::push(&mut params, Null);
foreach (v in ¶ms)
{
printf("%s\n", &Option::unwrap_or(PgValue::text(v), String::from_str("NULL")));
}
}app@db.internal:5432/app tls=true -7 0.5 t \x00ff NULL
A cell read as a value: PgRow::get_i64(&row, i), get_f64, get_bool and get_bytes (D-0181)
give None for NULL, for a column past the end, and for text of another form; get still gives the
text.
Networking
In std::net.
TCP connections, UDP datagrams and name resolution, over IPv4 and IPv6. Addresses are values:
an IpAddr (V4 or V6) and a port make a SocketAddr, read from text with SocketAddr::parse
("127.0.0.1:8080", "[::1]:8080") or built with SocketAddr::new(IpAddr::localhost_v4(), 8080);
resolve(host, port) asks the system's resolver.
Sockets block. accept waits for a connection, read for bytes, connect
for the other side. A server handles many connections at once by giving each its own thread:
spawn a function with the accepted TcpStream moved into it, and use a Channel (§19) where
threads must talk. Every socket can have timeouts; a call that waits longer gives
Err(TimedOut), and the socket is still usable. There is no TLS, and nothing is added to what you
send: the bytes that arrive are exactly the bytes written, in order, in pieces of any size, so a
protocol needs its own framing — lines, read with read_line, are the simplest.
| Function | Does |
|---|---|
TcpListener::bind(addr), TcpListener::local_addr(&l) | listen at addr (port 0: any free port); where it listens |
TcpListener::accept(&mut l), set_accept_timeout_ms(&mut l, ms) | the next connection, a TcpStream; give up after ms with Err(TimedOut) |
TcpStream::connect(addr), connect_timeout(addr, ms) | connect to a listener |
TcpStream::read_line(&mut s), read(&mut s, &mut buf, max), read_exact(&mut s, &mut buf, n) | as File's: a line, what has arrived (0: the peer has finished), exactly n bytes |
TcpStream::write(&mut s, data), write_text(&mut s, text), printf(&mut s, fmt, …) | send all of it |
TcpStream::shutdown_write(&mut s) | tell the peer nothing more is coming (it reads 0), and keep reading |
set_read_timeout_ms, set_write_timeout_ms, set_nodelay | per-stream timeouts (0: none); send small writes at once |
UdpSocket::bind(addr), send_to(&mut u, data, to), recv_from(&mut u, max) | datagrams: one message each, with its sender in the Datagram |
UdpSocket::try_clone(&u) | a second handle on the same socket: one thread waits in recv_from while another sends on the clone |
UdpSocket::connect(&mut u, to), send(&mut u, data), recv(&mut u, max) | a socket talking to one peer: recv drops datagrams from anyone else (D-0181) |
TcpStream::try_clone(&s) | a second handle on the same connection, for one thread to read while another writes; the connection closes when the last handle is dropped (D-0185) |
TcpStream::set_keepalive(&mut s, on) | have the system probe an idle connection, so a vanished peer is noticed (D-0181) |
import std;
// One connection: each line back in capitals, until the client finishes.
fn serve(TcpStream s) : u32
{
u32 lines = 0;
while (Ok(Some(line)) = TcpStream::read_line(&mut s))
{
String up = String::to_ascii_upper(&line);
if (Err(_) = TcpStream::printf(&mut s, "%s\n", &up))
{
break;
}
lines += 1;
}
lines
}
// Accepts `n` connections, each served by its own thread.
fn listen(TcpListener l, usize n) : u32
{
Vec<handle<u32>> served = Vec::new();
for (usize i = 0; i < n; i += 1)
{
if (Ok(s) = TcpListener::accept(&mut l))
{
Vec::push(&mut served, spawn(serve, s));
}
}
u32 total = 0;
while (Some(h) = Vec::pop(&mut served))
{
total += join(h);
}
total
}
fn ask(SocketAddr at, str word) : String
{
match (TcpStream::connect(at))
{
Ok(c) :
{
_ = TcpStream::printf(&mut c, "%s\n", word);
match (TcpStream::read_line(&mut c))
{
Ok(Some(answer)) : answer,
_ : String::from_str("(no answer)"),
}
},
Err(e) : String::from_str(NetError::text(&e)),
}
}
fn main() : u8
{
TcpListener l = match (TcpListener::bind(SocketAddr::new(IpAddr::localhost_v4(), 0)))
{
Ok(l) : l,
Err(e) :
{
printf("bind: %s\n", NetError::text(&e));
return 1;
},
};
SocketAddr at = TcpListener::local_addr(&l);
auto server = spawn(listen, l, 3);
foreach (word in ["alpha", "beta", "gamma"])
{
String answer = ask(at, word);
printf("%s -> %s\n", word, &answer);
}
printf("the server answered %u lines\n", join(server));
0
}alpha -> ALPHA beta -> BETA gamma -> GAMMA the server answered 3 lines
A server that runs until it is stopped combines this with watch_interrupts (above): give
the listener an accept timeout, and the accept loop looks at the flag between waits.
import std;
fn main() : u8
{
watch_interrupts();
TcpListener l = match (TcpListener::bind(SocketAddr::new(IpAddr::localhost_v4(), 0)))
{
Ok(l) : l,
Err(_) : return 1,
};
TcpListener::set_accept_timeout_ms(&mut l, 50);
u32 waits = 0;
while (!interrupt_requested() && waits < 3) // a real server: while (!interrupt_requested())
{
match (TcpListener::accept(&mut l))
{
Ok(s) : printf("a client\n"), // spawn a thread for it here
Err(TimedOut) : waits += 1, // nobody came: look at the flag again
Err(e) :
{
printf("accept: %s\n", NetError::text(&e));
return 1;
},
}
}
printf("closing the listener and stopping\n");
0
}closing the listener and stopping
Sorting and searching
In std::collections.
Vec::sort(&mut v) puts a Vec in order: integers by value, false before true, text by
its bytes, and a struct or enum of these field by field in declaration order (an enum by the order
its variants are declared in, then by payload). For any other order, or any other element type, Vec::sort_by(&mut v, less) takes a
function that says whether its first argument goes before its second. Both are stable: elements
that are equal keep the order they had. In a sorted Vec, Vec::binary_search(&v, &key) gives
Ok(i), the first position holding the key, or Err(i), where it would go
(binary_search_by takes the same less).
import std;
struct Player
{
String name;
u32 score;
}
fn higher(ref<Player, shared> a, ref<Player, shared> b) : bool
{
a.score > b.score // higher scores first
}
fn main()
{
Vec<String> names = Vec::new();
Vec::push(&mut names, String::from_str("mira"));
Vec::push(&mut names, String::from_str("ada"));
Vec::push(&mut names, String::from_str("lin"));
Vec::sort(&mut names);
assert(&names[0][0..$] == "ada" && &names[2][0..$] == "mira");
String who = "lin";
assert(Result::unwrap_or(Vec::binary_search(&names, &who), 99) == 1);
String bob = "bob";
assert(match (Vec::binary_search(&names, &bob))
{
Ok(_) : false,
Err(i) : i == 1, // between "ada" and "lin"
});
Vec<Player> ps = Vec::new();
Vec::push(&mut ps, Player { .name = "a", .score = 3 });
Vec::push(&mut ps, Player { .name = "b", .score = 9 });
Vec::push(&mut ps, Player { .name = "c", .score = 3 });
Vec::sort_by(&mut ps, higher);
assert(ps[0].score == 9);
assert(&ps[1].name[0..$] == "a" && &ps[2].name[0..$] == "c"); // equal scores keep their order
}HashMap<K, V> and HashSet<K>
In std::collections.
HashMap<K, V> maps keys to values; HashSet<K> holds a set of keys. A key is an integer,
a bool, a str, a String, or a struct whose fields (an enum whose payloads) are all keys:
struct Pos { i32 x; i32 y; } keys a grid. Any other key type (a float, an array, a struct with a
float field) is a static type error, and the message names the field that is not a key. The map
owns its keys and values. Lookups
take the key by reference, so they copy nothing.
| Function | Does |
|---|---|
HashMap::new(), HashMap::len(&m) | an empty map; the number of entries |
HashMap::insert(&mut m, k, v) | adds k; if it was present, replaces its value and returns the old one as Some |
HashMap::entry(&mut m, k, fresh) | the value for k as an exclusive reference, adding k with fresh first if absent |
HashMap::get(&m, &k), HashMap::get_mut(&mut m, &k) | Some reference to the value, or None |
HashMap::contains(&m, &k) | whether k is present |
HashMap::remove(&mut m, &k) | removes k and returns its value; every other entry keeps its place; takes time in proportion to the map's size |
HashMap::swap_remove(&mut m, &k) | the same, fast: the last entry moves into the removed one's place (D-0134) |
HashMap::key_at(&m, i), HashMap::value_at(&m, i) | entry i, for i < len, in insertion order |
HashMap::clear(&mut m) | removes every entry, destroying keys and values; the map stays usable |
HashMap::clone(&m), HashSet::clone(&s), clone(&m) | a copy of a map or set whose contents are clone (§12); Rc::clone shares instead |
HashMap::get_str(&m, "k"), get_mut_str, contains_str, remove_str | for a map keyed by String: the same lookups with a str, so no String is built to ask (D-0113); HashSet::contains_str, remove_str likewise |
HashSet::new, len, insert, contains, remove, swap_remove, key_at, clear | the same for a set; insert and the removals say whether anything changed |
HashSet::union(&a, &b), HashSet::intersection(&a, &b), HashSet::difference(&a, &b) | a new set of the keys in either, in both, or in a but not b, copied with clone; a and b are unchanged. The result keeps a's order, and union adds b's new keys after it (D-0138) |
Entries are kept in the order they were added, so foreach (k, v in &m) (§14) visits
them in that order, the same in the interpreter and the compiler, whatever the keys hash to.
key_at and value_at reach entry i directly. remove keeps that order, so it moves every
later entry down one; swap_remove is quick because it fills the gap with the last entry instead,
for a map whose order does not matter.
import std;
fn main()
{
array<str, 9> words = ["the", "cat", "and", "the", "hat", "and", "the", "bat", "sat"];
HashMap<String, u32> counts = HashMap::new();
foreach (w in words)
{
*HashMap::entry(&mut counts, String::from_str(w), 0) += 1; // 0 if new, then + 1
}
foreach (word, n in &counts) // insertion order
{
printf("%-4s %d\n", word, n);
}
String cat = "cat";
match (HashMap::get(&counts, &cat))
{
Some(n) : printf("cat: %d\n", *n),
None : printf("no cat\n"),
}
HashSet<u32> seen = HashSet::new();
u32 x = 7;
while (HashSet::insert(&mut seen, x)) // false once x comes round again
{
x = x * x % 10;
}
printf("%d distinct, back to %d\n", HashSet::len(&seen), x);
}the 3 cat 1 and 2 hat 1 bat 1 sat 1 cat: 1 3 distinct, back to 1
import std;
struct Pos
{
i32 x;
i32 y;
}
enum Tile
{
Wall,
Door(u8),
}
fn main()
{
HashMap<Pos, Tile> grid = HashMap::new();
HashMap::insert(&mut grid, Pos { .x = 0, .y = -1 }, Wall);
HashMap::insert(&mut grid, Pos { .x = 2, .y = 3 }, Door(7));
Pos at = Pos { .x = 2, .y = 3 };
match (HashMap::get(&grid, &at))
{
Some(Door(k)) : assert(*k == 7),
_ : assert(false),
}
HashSet<Tile> seen = HashSet::new(); // an enum of keys is a key too
HashSet::insert(&mut seen, Door(7));
assert(HashSet::contains(&seen, &Door(7)) && !HashSet::contains(&seen, &Wall));
}import std;
fn main()
{
HashMap<f64, str> m = HashMap::new(); // ✗ diag.type-mismatch (static): f64 is not a key type
}import std;
fn show(str name, ref<HashSet<str>, shared> s)
{
printf("%-13s", name);
foreach (i in 0..HashSet::len(s))
{
printf(" %s", *HashSet::key_at(s, i));
}
printf("\n");
}
fn main()
{
HashSet<str> monday = HashSet::new();
HashSet<str> tuesday = HashSet::new();
foreach (n in ["Ann", "Bo", "Cy", "Di"])
{
HashSet::insert(&mut monday, n);
}
foreach (n in ["Cy", "Ed", "Ann"])
{
HashSet::insert(&mut tuesday, n);
}
show("either day:", &HashSet::union(&monday, &tuesday));
show("both days:", &HashSet::intersection(&monday, &tuesday));
show("monday only:", &HashSet::difference(&monday, &tuesday));
}either day: Ann Bo Cy Di Ed both days: Ann Cy monday only: Bo Di
Queue<T> and PriorityQueue<T>
In std::collections.
A Queue<T> hands elements back in the order they arrived: push_back adds at the back,
pop_front takes from the front, each in constant time. It works at the other ends too
(push_front, pop_back), and front and back look without taking. A Vec with a growing
head index does the same job, but its storage never shrinks, and Vec::remove(&mut v, 0) moves
every other element on each call.
A PriorityQueue<T> hands back the least element first, whatever order they arrived in.
PriorityQueue::new() orders by the elements' own order, the one Vec::sort uses (numbers,
bool, text, and structs of them field by field), so a struct whose first field is the
priority needs no comparison function. PriorityQueue::new_by(less) takes the order as a
function, as Vec::sort_by does; for the largest first, pass one that answers "greater".
Elements the order calls equal come out in the order they went in, so equally urgent work is
first come, first served.
| Function | Does |
|---|---|
Queue::new(), Queue::len(&q) | an empty queue; the number of elements |
Queue::push_back(&mut q, x), Queue::push_front(&mut q, x) | adds x at the back, or before the front |
Queue::pop_front(&mut q), Queue::pop_back(&mut q) | takes the front (or back) element as Some, or None when empty |
Queue::front(&q), Queue::back(&q) | Some reference to the front (or back) element, or None; the queue stays as it is |
Queue::clear(&mut q) | destroys every element, front to back; the queue stays usable |
PriorityQueue::new(), PriorityQueue::new_by(less) | an empty queue in the elements' own order, or in less's |
PriorityQueue::push(&mut q, x), PriorityQueue::len(&q) | adds x; the number of elements |
PriorityQueue::pop(&mut q) | takes the least element as Some (of equals, the earliest pushed), or None when empty |
PriorityQueue::peek(&q), PriorityQueue::clear(&mut q) | Some reference to the element pop would take; destroys every element |
Both own their elements and destroy what is left when they end. A reference from front,
back or peek borrows the queue, so the queue cannot change while it is in use. Neither has
foreach or q[i]: a queue is read at its ends.
import std;
fn link(ref<Vec<Vec<usize>>, exclusive> adj, usize a, usize b)
{
Vec::push(&mut (*adj)[a], b);
Vec::push(&mut (*adj)[b], a);
}
fn main()
{
Vec<Vec<usize>> adj = Vec::new();
foreach (_ in 0..7)
{
Vec::push(&mut adj, Vec::new());
}
link(&mut adj, 0, 1);
link(&mut adj, 0, 2);
link(&mut adj, 1, 3);
link(&mut adj, 2, 3);
link(&mut adj, 3, 4);
link(&mut adj, 4, 5);
// Breadth first: everyone one step from 0, then two steps, ...
Vec<u32> steps = Vec::filled(7, max_value<u32>());
Queue<usize> todo = Queue::new();
steps[0] = 0;
Queue::push_back(&mut todo, 0);
while (Some(u) = Queue::pop_front(&mut todo))
{
foreach (v in &adj[u])
{
if (steps[*v] == max_value<u32>())
{
steps[*v] = steps[u] + 1;
Queue::push_back(&mut todo, *v);
}
}
}
foreach (i, s in &steps)
{
if (*s == max_value<u32>())
{
printf("%d: unreachable\n", i);
}
else
{
printf("%d: %d steps\n", i, *s);
}
}
}0: 0 steps 1: 1 steps 2: 1 steps 3: 2 steps 4: 3 steps 5: 4 steps 6: unreachable
import std;
struct Road
{
usize to;
u32 km;
}
struct Visit // ordered by dist, then node: a key
{
u32 dist;
usize node;
}
fn road(ref<Vec<Vec<Road>>, exclusive> map, usize a, usize b, u32 km)
{
Vec::push(&mut (*map)[a], Road { .to = b, .km = km });
Vec::push(&mut (*map)[b], Road { .to = a, .km = km });
}
fn main()
{
Vec<Vec<Road>> map = Vec::new();
foreach (_ in 0..5)
{
Vec::push(&mut map, Vec::new());
}
road(&mut map, 0, 1, 7);
road(&mut map, 0, 2, 2);
road(&mut map, 2, 1, 3);
road(&mut map, 1, 3, 4);
road(&mut map, 2, 3, 9);
road(&mut map, 3, 4, 1);
// Dijkstra: always settle the nearest place not yet settled.
Vec<u32> best = Vec::filled(5, max_value<u32>());
PriorityQueue<Visit> frontier = PriorityQueue::new(); // least first
PriorityQueue::push(&mut frontier, Visit { .dist = 0, .node = 0 });
while (Some(at) = PriorityQueue::pop(&mut frontier))
{
if (at.dist >= best[at.node])
{
continue; // reached sooner already
}
best[at.node] = at.dist;
foreach (r in &map[at.node])
{
PriorityQueue::push(&mut frontier, Visit { .dist = at.dist + r.km, .node = r.to });
}
}
foreach (i, d in &best)
{
printf("to %d: %d km\n", i, *d);
}
}to 0: 0 km to 1: 5 km to 2: 2 km to 3: 9 km to 4: 10 km
import std;
struct Task
{
u8 urgency;
str what;
}
fn more_urgent(ref<Task, shared> a, ref<Task, shared> b) : bool
{
a.urgency > b.urgency
}
fn main()
{
PriorityQueue<Task> inbox = PriorityQueue::new_by(more_urgent); // largest first
PriorityQueue::push(&mut inbox, Task { .urgency = 1, .what = "tidy desk" });
PriorityQueue::push(&mut inbox, Task { .urgency = 5, .what = "server down" });
PriorityQueue::push(&mut inbox, Task { .urgency = 3, .what = "review" });
PriorityQueue::push(&mut inbox, Task { .urgency = 5, .what = "disk full" });
while (Some(t) = PriorityQueue::pop(&mut inbox))
{
printf("%d %s\n", t.urgency, t.what); // equal urgency: first come, first served
}
}5 server down 5 disk full 3 review 1 tidy desk
Rc<T>
In std::memory.
Shared ownership as a library: a reference-counted box. Rc::clone makes another handle
to the same box; the value is destroyed when the last handle is dropped. Access is read-only (a
shared reference); put a mutex inside if you need mutation. Rc is not thread-safe.
A Weak<T>, from Rc::downgrade, keeps the box but not the value: Weak::upgrade is
Some(Rc) while a strong handle exists, else None. It is for a parent pointer, a cache or an
observer list, where an Rc would make a cycle (D-0125).
| Function | Signature |
|---|---|
Rc::new | (T) : Rc<T> |
Rc::clone | (ref<Rc<T>, shared>) : Rc<T> |
Rc::get | (ref<Rc<T>, shared>) : ref<T, shared> |
Rc::downgrade | (ref<Rc<T>, shared>) : Weak<T> |
Weak::upgrade | (ref<Weak<T>, shared>) : Option<Rc<T>> |
Weak::clone | (ref<Weak<T>, shared>) : Weak<T> |
import std;
fn main()
{
Vec<i32> payload = Vec::new();
Vec::push(&mut payload, 42);
auto a = Rc::new(payload); // payload moves into the box; count = 1
auto b = Rc::clone(&a); // count = 2
assert(Vec::len(Rc::get(&b)) == 1);
drop(a); // count = 1; the box survives
assert(Rc::get(&b)[0] == 42);
drop(b); // count = 0: the Vec is destroyed, then the box is freed
}Box<T>
In std::memory.
A value on the heap with exactly one owner: Rc without the count. Box::new(v) moves v
in; Box::get(&b) and Box::get_mut(&mut b) borrow it; Box::into_inner(b) moves it back out; Box::clone(&b) copies a box whose value is clone;
destroying the Box destroys the value. Its main use is a type that contains itself, which needs
something holding a pointer between it and itself: a struct that contains itself directly has no
size, and is rejected with diag.recursive-type.
import std;
struct Node
{
i32 value;
Option<Box<Node>> next; // Option<Node> here would be diag.recursive-type
}
fn main()
{
Option<Box<Node>> list = None;
list = Some(Box::new(Node { .value = 1, .next = list })); // each node owns the next
list = Some(Box::new(Node { .value = 2, .next = list }));
Box<i32> counter = Box::new(41);
*Box::get_mut(&mut counter) += 1;
assert(*Box::get(&counter) == 42);
i32 n = Box::into_inner(counter); // the value back; the Box's memory is freed
assert(n == 42);
} // list: both nodes destroyed, the outer one firstDestroying a chain of Boxes does not use a stack frame per node when each Box is the
last thing its node destroys (its first-declared resource field, as next above): the next node is
destroyed by a loop, in the same order. So a list of a million nodes goes out of scope like any
other value.
import std;
struct Node
{
i32 value;
Option<Node> next; // ✗ diag.recursive-type (static): Node would contain itself
}
fn main()
{
}swap and replace
In std::core.
A value cannot be moved out of something you hold only by reference, since that would leave
a hole. So two values behind references are exchanged by swap(&mut a, &mut b), and one is
exchanged for a new value by replace(&mut r, v), which returns the old one. Two elements of the
same Vec or slice are exchanged by Vec::swap(&mut v, i, j) or slice_swap(s, i, j), which also
handle i == j. Nothing is copied or destroyed: each value just changes place.
overwrite(&mut r, v) is replace for when the old value is not wanted: it stores v and
destroys what was there.
import std;
// Sorts any run of Strings by length: the elements are resources, so
// they are exchanged, never copied out.
fn sort_by_len(slice<String, exclusive> s)
{
for (usize i = 1; i < slice_len(s); i += 1)
{
usize j = i;
while (j > 0 && String::len(&s[j - 1]) > String::len(&s[j]))
{
slice_swap(s, j - 1, j);
j -= 1;
}
}
}
struct Pair
{
String left;
String right;
}
fn main()
{
Vec<String> words = Vec::new();
foreach (w in ["pear", "fig", "banana", "kiwi"])
{
Vec::push(&mut words, String::from_str(w));
}
sort_by_len(&mut words[0..$]);
assert(String::len(&words[0]) == 3 && String::len(&words[$ - 1]) == 6);
Pair p = Pair { .left = "L", .right = "RR" };
swap(&mut p.left, &mut p.right);
String old = replace(&mut p.left, String::from_str("new")); // old is "RR"
assert(String::len(&old) == 2 && String::len(&p.left) == 3);
}An empty slot is filled by plain assignment: a None owns nothing, so *slot = Some(...)
writes over it (§07). To put a value where one may already be, use replace, which hands the old
one back to you.
import std;
struct Node
{
i64 val;
Option<Box<Node>> next;
}
// Appends at the tail: walk to the empty link, then fill it.
fn push_back(ref<Option<Box<Node>>, exclusive> head, i64 v)
{
ref<Option<Box<Node>>, exclusive> slot = head;
while (true)
{
match (&mut *slot)
{
Some(b) : { slot = &mut Box::get_mut(b).next; },
None : { break; },
}
}
*slot = Some(Box::new(Node { .val = v, .next = None })); // slot holds None: nothing to lose
}
fn main()
{
Option<Box<Node>> list = None;
push_back(&mut list, 1);
push_back(&mut list, 2);
i64 sum = 0;
ref<Option<Box<Node>>, shared> cur = &list;
while (true)
{
match (cur)
{
Some(b) : { sum += Box::get(b).val; cur = &Box::get(b).next; },
None : { break; },
}
}
assert(sum == 3);
}Vec<T>for sequences,Option<T>for maybe-absent values,Result<T, E>for fallible calls,Box<T>for one value on the heap (a recursive type's link),Rc<T>when two owners are genuinely needed.- Library functions take
&vor&mut v; match the mode to what you need. - Hold element references briefly; any
pushorpopmay invalidate them (§10).
§22Surface syntax reference
spec/22-surface-syntax.mdTokens, declarations, statements and expressions on one page, with the tie-break rules a reader needs.
This section is the syntax in one place: the tokens, the declarations, the statements, the expressions, and the handful of places where the grammar needs a tie-break rule. It is meant for looking things up; the semantics are in the earlier sections.
Lexical structure
- Comments:
// to end of lineand/* block */(block comments do not nest). - Identifiers: ASCII letters, digits and
_, not starting with a digit. - Integer literals: decimal digits, or hexadecimal, octal or binary after
0x,0o,0b, optionally: type(255: u8,0xFF: u8). No sign:-is an operator. Float literals:1.5,0.25: f32,1.0e-7. Digit separators: a_between two digits is ignored (1_000_000,0xFFFF_0000). Booleans:true,false. Unit value:(). - String literals:
"…", typestr. Escapes\n \r \t \0 \\ \"and\u{hex}; any other source character stands for itself. A literal must close on its own line. Byte literals:b"…", typearray<u8, N>; the same escapes except\xNNin place of\u{…}, ASCII characters only, at least one byte. Byte character literals:b'a', one ASCII character or escape: an unsigned integer literal,u8unless its context gives another unsigned type. No character literals (achartype). - Keywords:
fn struct bitstruct enum resource match if else while for foreach return auto const module import export unsafe extern break continue move mut true false as void(asis reserved and unused). - Reserved type names (cannot be identifiers):
i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 isize usize f32 f64 bool str ref rawptr array slice handle mutex guard. The wordssharedandexclusiveare ordinary identifiers that only have meaning insideref<…, …>. - Whitespace and newlines are insignificant between tokens; brace placement is a matter of style.
Types
| Type syntax | Meaning |
|---|---|
i32, u8, f64, bool, … | built-in scalars |
str | text value (§21) |
void | the unit type (its value is written ()) |
ref<T, shared>, ref<T, exclusive> | references |
rawptr<T> | raw pointer |
array<T, N> | fixed array, N a constant expression (§16) |
slice<T, shared>, slice<T, exclusive> | a borrowed run of elements (§16) |
fn(T1, T2) : R, fn(T1) | function type (result void if omitted) |
handle<T>, mutex<T>, guard<T> | concurrency types |
Name, Name<T1, T2>, mod::Name | a declared struct or enum, optionally instantiated |
Declarations
// Functions. `export` makes an item visible outside its module.
fn name(T1 p1, T2 p2) : R // return type after a colon
{
body
}
fn name(T1 p1) // no return type: returns void
{
body
}
export fn name<T, U>(T x) : U // generic, exported
{
body
}
fn Type::name(ref<Type, shared> self) // associated function, called as Type::name(...)
{
body
}
fn Type::drop(ref<Type, exclusive> self) // the destructor of Type
{
body
}
// Foreign functions: parameters and result must be scalars, void or rawptr.
unsafe extern fn name(rawptr<u8> buf, usize len) : isize;
// Structs and enums. `resource` marks a type that manages something.
struct Name<T>
{
T field; // fields end with ;
export i32 visible; // field visibility is per field
}
resource struct Name
{
i32 field;
}
enum Name<T>
{
Variant(T), // with a payload
Other // without
}
// Modules and imports.
module name
{
items
}
export module name
{
items
}
module name "./path/to/file.cb"; // the same, with that file's items as the body (§17);
// `export module name "…";` as for the inline form
import path::to::item;Statements and blocks
T x = e; // typed local
auto x = e; // inferred local
T x; // uninitialised local (must be assigned before use)
Name { f1, f2 } = e; // destructuring: every field
e; // expression statement
{ // a block; no ; needed after it
statements
}
if (c)
{
statements
}
else if (c2)
{
statements
}
else
{
statements
}
while (c)
{
statements
}
match (e) // a comma after every arm
{
A(x) : e1,
B : e2,
_ : e3,
}
unsafe
{
statements
}A block is { statement* expr? }: zero or more statements, then an optional trailing
expression that is the block's value. if, while, match, unsafe and plain blocks may stand
as statements without a semicolon, and their value is discarded. When one appears as the initialiser
of a declaration or the right side of an assignment, the enclosing statement still ends with ;.
Expressions
| Form | Example |
|---|---|
| literals and names | 42, 3.5, true, (), x, mod::item |
| string and byte literals | "hello\n" (a str), b"\x00\xff" (an array<u8, 2>) |
| struct literal | Point { .x = 1, .y = 2 }, Box<i32> { .value = 5 } |
| array literal | [1, 2, 3], [0; 256] (256 copies of 0) |
| enum variant | Some(5), None, Shape::Dot |
| field, index, call | p.x, a[i], f(x, y), Vec::len(&v), identity<i64>(5) |
| borrow, deref | &x, &mut x, *r |
| operators | see the precedence table in §13 |
| propagate | e? |
| block, if, match, unsafe | as values: i32 x = if (c) { 1 } else { 2 }; |
| closure | [a, b](i32 x) { a + b + x }, move [v]() { Vec::len(&v) } |
| control | return e, return, break, continue |
Reading the grammar in tricky spots
- Declaration or expression? A statement that begins with a type name,
fn,auto, or the name of a struct or enum is a declaration; otherwise it is an expression.Point p = …declares;p.x = 1;assigns. [starts a closure or an array. It is a closure only if the closing]is immediately followed by( … ) {. Otherwise it is an array literal.- Generic call or comparison?
name<T>(…)with a type inside the angle brackets is always a generic instantiation. Comparisons are non-associative, soa < b > cis never a comparison. - Conditions are always parenthesised, which is why
if (Point { .x = 1 } == p) { … }needs no special rule. - A pattern as a condition.
if (Some(x) = e)andwhile (Some(x) = e)are read as a pattern followed by one=when the condition beginsName(,m::Nameor a literal;Some(x) == eis a comparison, andx = e(a bare name) the assignment it is everywhere, which is not abool. Name { f1, f2 } = e;(bare identifiers in braces) is destructuring;Name { .f = e }is a literal.bitstruct Name : u32 { f : 6; … }declares a struct of bit fields (§16): the widths fill the backing type exactly, low bit first.&mutis two tokens, one operator.x = y = zassigns right to left.
Restrictions and deliberate absences
Things the grammar does not provide, each a considered decision recorded in the specification:
- no character literals (
b'x'is a byte: au8, or the unsigned type its context asks for), no indexing or slicing of astr; noswitch(amatchon an integer, §16, is the checked form); nogoto - no
import … asrenaming; no method-call syntax (v.push(x)— final,CHG-0132); no operator overloading - patterns in
matchare_, a name (a binder or a constant), a literal, a variant, and a variant with a pattern for its payload, nothing else (no ranges, alternatives, struct patterns or guards); destructuring takes a struct apart whole: every field named, or..for the rest (destroyed as usual), andfield : namerenames one - no partial initialisation of aggregates
- no lifetime annotations: a function returning a reference must have exactly one reference parameter
- no user-declared bounds on generic type parameters: only the five built-in ones (
eq,ordered,number,integer,clone), on a function's parameters; a struct or enum satisfiesclonealone - no
mutqualifier on bindings (the keyword is reserved for&mut) - no moving a resource out of a field of a live struct; move the whole struct, or take it apart (a field of a temporary may be moved out; the rest ends with the statement)
- no spawning a closure that borrows; use
move
What an implementation must document
The width of isize/usize; byte order; enum discriminant width; struct padding; the
value of dangling<T>(); the encoding of function values; the address str_ptr gives for a
str's bytes and whether equal values share one; and the form in which faults and diagnostics are
reported. Everything else is fixed by the language.
PPerformance
Measured advice for fast compiled programs: what costs time in cobc's output, and how to write code the compiler can see through.
This chapter is about making compiled programs fast. Every piece of advice in it was
measured: a small program written two or more ways, each way built by cobc at its default -O2
under GCC and under Clang, and timed. The ratios quoted are from those runs, on the machine described
in bench/RESULTS.md. They describe cobc as it is today. The compiler proves more with each
release, so a gap measured here may close; where that is likely, the text says so.
None of it applies to coby. The interpreter tracks every object, reference and access
path and makes every check the specification leaves to run time, so it is many times slower than
compiled code whatever you write, and its relative costs are not cobc's. Use it to run a program
at once and to check what it does, never to time it.
A workflow by stage
| Stage | Command | What it costs and gives |
|---|---|---|
| Writing | coby --check prog.cb or cobc --check src/ | Every check made before a program starts: syntax, names, types, ownership, exhaustive matches. Nothing runs and no C is compiled, so it takes seconds even for a large program, and a program that reads input, writes files or serves the network is checked without doing so. |
| Trying it | coby prog.cb, or cobc --run -O0 prog.cb | -O0 makes the C compiler's work short, at the price of a slow program: right while you only care whether it behaves correctly. |
| Measuring and shipping | cobc -o prog prog.cb (-O2, the default) | What users run. Measure only this: -O0 keeps exactly the work -O2 removes, so a comparison made at -O0 says nothing about the real build. |
Beyond -O2: --cc clang and --cc gcc choose the C compiler, and neither wins everywhere
(bench/RESULTS.md has the comparison); -march=native lets the C compiler use every instruction
your processor has, at the cost of an executable that may not run on an older one. To see where the
time goes, perf stat ./prog counts instructions and perf record ./prog then perf report names
the functions; cobc --keep-c keeps the generated C, in which every run-time check is a visible
line ("Reading the C that cobc generates" in the Introduction).
Where the time goes
A compiled CobaltC program makes every check the specification requires, except those the compiler proves can never fail. Two kinds of check cost very differently:
- Cheap checks, a compare and a branch the processor predicts: an index against a length, an addition for overflow. In a tight loop they cost a few per cent to a few tens of per cent.
- Tracking, which the runtime library does for a value the compiler cannot reason about: it records each reference and access path as it is made, checks each access against the others (§08–§09), and retires them at the end of their statement or scope. This costs hundreds of nanoseconds per access, a hundred times a plain access or more.
So the advice that matters most is not about saving checks: it is about writing code whose values the compiler can see through, so that tracking is left out. The sections below are in order of how much they saved in the measurements.
To see where your own program still pays for tracking, run cobc --report-tracking prog.cb. It
compiles nothing: it lists the lines whose generated C still calls the runtime's tracking, with the
kinds of call on each, lines inside a loop first. A line inside a loop is the one to look at; a
line that runs once (a binding, a move into a call) costs next to nothing. The sections below say
how to rewrite what it finds.
The other direction is cobc --all-checks prog.cb: it compiles the program with every proof about a function's reference parameters switched off, so every access through a parameter is checked where it is made. It is slower, and it exists for comparison: a program must behave the same both ways, and the project's test battery runs its whole file suite under it (D-0209).
Keep hot vectors confined
cobc proves a Vec confined when nothing but its own name can ever reach it.
A confined Vec's accesses compile to plain C with bounds and overflow checks and no tracking. A
local Vec, or one a function takes by value, stays confined when it is:
- made where it is declared (
Vec::new(), or a function's result), or received as a parameterVec<T> v; - used through
Vec::push,Vec::len,Vec::pop,Vec::reserve,Vec::swap,Vec::reverse,Vec::insert,Vec::remove,Vec::truncate,Vec::clear,Vec::sort,Vec::contains,Vec::index_of,Vec::binary_search, indexing (v[i],v[i] = x,v[i].f += x) andforeach, alsoforeach (p in &mut v)writingp.f; - passed as
&vor&mut vto functions that themselves use it only in those ways (the compiler checks each such function), and in no other argument of the same call; - at most moved out at the end: as the function's result (also inside a struct or
Ok(…)built as the result), or into a call as its last use, such asoverwrite(&mut h.v, v).
A shared reference to an element, held across the loop
(ref<i64, shared> first = &v[0];), leaves the loop's reads as fast as before: a read cannot clash
with a shared reference. Writes to the vector and pushes onto it are then checked, since one of
them could. What loses confinement altogether is an exclusive reference to an
element (&mut v[i] kept in a binding), &mut v handed to a function that may keep a path to it,
a closure that names v, or moving v to another binding; then every access in the loop is
tracked, about ten times the cost in a summing loop.
A vector that is a field of a local struct (h.v) is confined the same way while the function
uses it only by indexing (h.v[i], h.v[i] = x, h.v[i].f += x) and Vec::len(&h.v), and never
uses h whole (moving it, borrowing it, capturing it); the struct's other fields are used freely.
Where the function also pushes to h.v, borrows an element of it or hands it on, indexing it there
is tracked; then hand it to a helper (sum_helper(&h.v)), loop over it with
foreach (x in &h.v), or move it out for the loop and back (replace then overwrite), each of
which is as fast as a local. Confinement is decided per function: one function's use of a vector
does not slow another function down.
A function that takes a struct by reference and loops over the struct's
vectors (the shape of most programs: fn restock(ref<Inventory, exclusive> inv, …)) runs as plain
code too, wherever the struct lives, when that reference is the function's only one, its other
parameters are plain values, and it uses each such vector only in the ways listed above (no
&inv.counts handed to another function, no element borrowed and kept). The function then
checks once, as it starts, that no element of those vectors is borrowed elsewhere, and otherwise
runs its checked code, which faults where §08 says it must. Its other fields (inv.owner,
inv.total) are used freely. Over 1,000,000 elements through such a reference, a write
v[i] = v[i] + 1 went from 546 to 1.8 ns an element, pop from 280 to 8, push from 41 to 9:
the speed of a local.
import std;
struct Inventory
{
String owner;
Vec<i64> counts;
Vec<u8> flags;
i64 total;
}
// One reference, to a struct whose vectors it only indexes, pushes,
// swaps and measures: checked once as it starts, then plain code.
fn restock(ref<Inventory, exclusive> inv, i64 amount)
{
for (usize i = 0; i < Vec::len(&inv.counts); i += 1)
{
if (inv.counts[i] < 3)
{
inv.counts[i] += amount;
inv.flags[i] = 1;
}
inv.total += inv.counts[i];
}
Vec::push(&mut inv.counts, amount);
Vec::push(&mut inv.flags, 0);
Vec::swap(&mut inv.counts, 0, Vec::len(&inv.counts) - 1);
}
fn main()
{
Inventory inv = Inventory { .owner = String::from_str("shop"), .counts = Vec::new(), .flags = Vec::new(), .total = 0 };
foreach (i in 0..8: i64)
{
Vec::push(&mut inv.counts, i % 5);
Vec::push(&mut inv.flags, 0);
}
restock(&mut inv, 10);
printf("%s: %v items, total %v, first %v\n", &inv.owner, Vec::len(&inv.counts), inv.total, inv.counts[0]);
}shop: 9 items, total 73, first 10
| The same sum of 20,000,000 elements, ten times | Time, relative |
|---|---|
A local Vec, indexed | 1 |
The same Vec, passed as &v to a helper that indexes it | 1 |
A Vec field of a local struct, indexed in the loop (h.v[i]) | 1 |
A Vec field of a struct passed to that helper (sum_helper(&h.v)), or moved out for the loop and back | 1 |
A local Vec while a shared reference to one element is held | 1 |
A local Vec that is moved to another binding after the loop | 10 |
Predicates. Vec::retain, Vec::position and Vec::binary_search_by run
as plain loops when the function they are given is a closure literal written at the call, a named
function, or a fn value holding a named function, provided it only reads the elements it is
given (Vec::sort_by does so for a closure literal): 1,000,000 elements through retain took 3 to
9 ns each. A closure stored in a
variable first (fn(…) : bool p = [lim](…) { … };) is tested when the call starts: one that only
reads what it is given is called with bare element addresses, about 150 ns an element (the cost of
calling through the variable); one that may keep what it is given is lent each element through the
runtime, about a microsecond. Writing the closure at the call is fastest.
Updating elements
Every way of updating the elements of a Vec of structs compiles to the element's address
and a few plain instructions, as long as the vector is confined: v[i].x += v[i].vx,
foreach (p in &mut v) { p.x += p.vx; }, a call that receives one element, step(&mut v[i]), and
an element named for a block, ref<P, exclusive> p = &mut v[i]; (provided the rest of that block
uses p and not v). Over 100,000 elements updated 100 times, all four took the same time. Choose
whichever reads best:
import std;
struct Particle
{
i64 x;
i64 vx;
}
fn main()
{
Vec<Particle> ps = Vec::new();
foreach (k in 0..1000: i64)
{
Vec::push(&mut ps, Particle { .x = k, .vx = k % 7 - 3 });
}
foreach (t in 0..100)
{
foreach (p in &mut ps) // plain code: the loop holds ps for its length
{
p.x += p.vx;
if (p.x < 0 || p.x > 1000)
{
p.vx = -p.vx;
}
}
}
printf("%v %v\n", ps[0].x, ps[999].x);
}294 803
Functions that take references
A function that takes a reference (ref<T, …>) or a slice runs as plain code, with no
tracking inside it, when cobc can see that nothing else can clash with that reference while the
call runs. It can see it in three cases: the reference is the function's only one; all its
references are shared; or, the usual shape of code that builds something from its input, some
are exclusive and the rest shared and every one is to plain data. In the third case the function
checks once, as it starts, that its arguments do not overlap, and then runs unchecked; where they
do overlap (count(&mut c, &c.buf[0..4])), it runs the checked code instead, which faults where
§08 says it must. Plain data is numbers, bool and char, and arrays, structs and enums of them;
not a String, a Vec, a Box or a reference. And in every case the body must use each reference
only directly:
- through it: reads and writes of the value, its fields and its elements, and
slice_len; - never handed to another function, kept in a variable or a struct, or borrowed from (an only reference may still be handed whole to a function that returns plain data or nothing);
- beside parameters that are plain values, in the third case.
| 10,000,000 bytes summed into a struct of two counters | Time, relative |
|---|---|
fn feed(ref<Acc, exclusive> a, slice<u8, shared> data) | 1 |
fn feed(Acc a, slice<u8, shared> data) : Acc, the counters passed in and returned | 0.7 |
The first, each byte read through a helper, a.sum += byte_at(data, i) | 80 |
The first, with Acc also holding a Vec | 90 |
So keep the loop over the input in the function itself, and keep the state it updates in a
struct of plain fields. Where that state belongs to something larger (a struct that also holds a
String or a Vec), pass the plain part alone, as count(&mut r.counts, …) does here:
import std;
struct Counts
{
u64 bytes;
u64 newlines;
u64 sum;
}
struct Report
{
String name;
Counts counts;
}
// One exclusive reference to plain data beside a shared slice, each used
// only directly: checked once as the call starts, then plain code.
fn count(ref<Counts, exclusive> c, slice<u8, shared> data)
{
for (usize i = 0; i < slice_len(data); i += 1)
{
c.bytes += 1;
c.sum = wrapping_add(c.sum, widen<u64>(data[i]));
if (data[i] == 10)
{
c.newlines += 1;
}
}
}
fn main()
{
Vec<u8> data = Vec::new();
foreach (k in 0..1000)
{
Vec::push(&mut data, narrow_wrapping<u8>(k * 7));
}
Report r = Report { .name = String::from_str("input"), .counts = Counts { .bytes = 0, .newlines = 0, .sum = 0 } };
count(&mut r.counts, &data[0..$]); // the struct holds a String: pass only its plain part
count(&mut r.counts, &data[0..10]);
printf("%v: %v bytes, %v newlines, sum %v\n", &r.name, r.counts.bytes, r.counts.newlines, r.counts.sum);
}input: 1010 bytes, 4 newlines, sum 126831
Text in hot code
std takes text it only reads as a StringView (D-0134), and for most code that is the
right choice. In compiled code a view is not quite free, though: making one from a String
(String::as_view) costs about 100 nanoseconds for its checks, and every piece
split returns holds its own borrow of the text. For a function of your own that a hot loop calls
millions of times:
| 1,000,000 calls of a function that reads a 60-byte text | Time, relative |
|---|---|
fn f(StringView s), one view made before the loop and passed each time | 0.1 |
fn f(ref<String, shared> s), called f(&text) | 1 |
fn f(StringView s), called f(String::as_view(&text)) | 1.8 |
fn f(String s), called f(String::clone(&text)) | 14 |
- A function that only reads its
StringView(indexes it, takes its length, hands it tostd's readers) costs nothing to call: the view is passed as its address and length. Make the view once, outside a hot loop: eachString::as_viewcosts about 100 nanoseconds. - To process input line by line, use a
read_lineloop:read_allfollowed byString::splittook about nine times as long over the same 200,000 lines, the price of keeping every line's view at once. - Calls to
stdthat take a view cost per call, not per byte (a 620-byte text cost the same as a 60-byte one).
Building and writing text
| Building one String from 2,000,000 numbers | Time, relative |
|---|---|
String::append(&mut out, i) then String::append(&mut out, ",") | 1 |
sprintf("%v,", i), then append that | 9 |
A Vec<String> of pieces (each made by sprintf), then String::join | 19 |
String::append formats a number or a bool straight into the String, with no
intermediate text, so build output with it; sprintf makes a new String each time, about
two-thirds of a microsecond a call. Standard output is buffered (§21), so printf per line is a
fast way to write many lines: 2,000,000 lines to a file took 0.35 s with one printf per line and
0.24 s building one String with String::append and printing it once. Printing as you go is
fast enough for most programs and keeps no text in memory; building one String saves a little
more where all the text fits.
Choose the collection by what you do most
| Operation | Fast | Slow | Measured |
|---|---|---|---|
| Take elements from the front, in order | Queue::pop_front, or Vec::reverse once then Vec::pop | Vec::remove(&mut v, 0), which moves every remaining element: the cost grows with the square of the length | 10,000 elements: 2.4 times; 100,000: 51 times |
| Test membership in a large set | Vec::sort once then Vec::binary_search, or HashSet::contains | Vec::contains, which compares element by element | 20,000 keys, 5,000 queries: binary_search 5 to 6 times as fast, HashSet 1.2 times, growing with the size |
Fill a Vec whose size you know | Vec::filled(n, x), then write the elements | pushing one by one (with or without Vec::reserve first, which no longer saves anything measurable) | 50,000,000 elements: 5 to 6 times |
Counting with a HashMap<String, …>: *HashMap::entry(&mut counts, String::clone(w), 0) += 1
clones the key on every call; looking the key up first,
match (HashMap::get_mut(&mut counts, w)) { Some(c) : *c += 1, None : … }, clones only for a new
key. Over 1,000,000 words the second was about 8% faster, but both are a few hundred nanoseconds a
word: write whichever reads better.
Threads
Each running thread keeps its own share of the compiled program's runtime checks, so threads doing their own work run in parallel (§19). Measured with four threads each counting the words of the same text: 0.30 s, against 0.16 s for one thread doing a quarter of the work, and 0.24 s for four separate programs. To keep a thread's work its own:
- Move what a thread works on into it (
spawn(count, chunk)withchunkaVecyou no longer need): it then belongs to the thread. A thread that reads the spawner's data through a reference shares it. Read it through a shared reference *parameter* the function never rebinds or borrows (fn band(ref<Vec<Row>, shared> rows, …)), and hand its fields and elements on only to functions that keep nothing of them:cobcthen reads through it with bounds checks alone, since nothing can change what it reaches while the call runs, and several threads read one structure at once (four threads aggregating 20,000 rows: 30 ms, against 48 ms for one). A reference kept past the call, or stored in a struct, is tracked as usual, and threads that form such references into one structure take turns on its records. - Hand results back as the thread's result, through
join, or through aChannel, rather than writing into a structure every thread locks. - Keep a lock's block to reads and writes through its guard (
{ auto g = lock(&m); *g += 1; }): such a block is a lock and an unlock and nothing more. Four threads each taking one shared mutex 200,000 times, with a little work between, took 0.6 s; giving the guard to a function, or returning from inside the block, makes each lock cost about ten times as much. - A thread per connection is a sound design for a server. Each thread is an operating-system thread with a reserved, mostly uncommitted stack, and a finished thread is reused by the next
spawn. A test server held 10,000 connections at once, each its own supervised thread answering two queries, in 250 MB, about 25 kB per idle connection, with no failures, and a round trip cost the same 0.13–0.16 ms with 1,000 connections open as with 10,000. An idle connection costs nothing while it waits: on Linux its thread sleeps in the kernel until data arrives or it is cancelled.
Arithmetic and plain values
- Every
+,-and*on integers is checked for overflow (§06). Where wrapping around is what you mean (hashes, checksums, random number generators), say so withwrapping_add,wrapping_muland their relatives: a tight sum took a third less time, and Clang executed 40% of the instructions. Never use them to avoid a check on arithmetic that should not overflow: the check is what turns a wrong result into a diagnostic. - Pass a struct by whichever reads best. A 64-byte struct of eight
i64passed to a function that reads it took the same time copied as through aref<…, shared>.
When nothing else is enough
A kernel that must run at the speed of hand-written C can be hand-written C:
extern "./kernel.c"; links it and unsafe extern fn declares it (§20). That code is outside every
guarantee CobaltC makes: its memory safety, its aliasing and its arithmetic are yours to get right,
and its declaration is an unchecked claim. Measure first.
- Measure the
-O2build, nevercobyor-O0. - Keep a hot
Veca local (or a parameter it owns) that only its own name reaches; across a loop, hold shared element references only, never an exclusive one. - Index a struct's
Vecfield (h.v[i]) where the function only indexes it; where it also pushes to it or hands it on, loop withforeachor a helper. - Give a function over a struct's vectors the struct as its only reference, and keep its uses of the vectors to indexing and the
Veccalls listed above. - In a function that takes references, use them only directly: loop over a slice in the function itself, and update plain state through one
exclusivereference (or pass it by value). - Make a
StringViewonce, outside a hot loop; passing it on is free. - Build text with
String::append;printfper line is fine. - Write a child process's input in large pieces (
ChildInput::write,Child::write_input): each write goes straight to the pipe and wakes the child, so a write per line of 100,000 short lines cost 11 s of system time, and 64 KB at a time 0.06 s. Nothing is buffered, so a program that writes a line and then waits for the child's answer works as written. - Never
Vec::remove(&mut v, 0)in a loop over a longVec; for membership in a large set, sort once andVec::binary_search, or use aHashSet. - Make a
Vecof a known size withVec::filled, then write its elements. - Say
wrapping_only where wrapping is the meaning.
AAppendix: diagnostics
Every named diagnostic, what it means, and the usual fix.
Every diagnostic has a stable name. Static means the program is rejected before it runs; dynamic means a checked fault terminates the program; both means the same rule is applied statically where the checker can see the problem and dynamically otherwise.
| Diagnostic | Phase | Meaning | Usual fix |
|---|---|---|---|
diag.syntax-error | static | the text is not a CobaltC program by the grammar | correct the text there; the message names what was expected and what was found |
diag.unbound-name | static | a name resolves to nothing | declare it, fix the spelling, or import it |
diag.ambiguous-name | static | two imports or two enums supply the same name | qualify it |
diag.name-not-visible | static | the item is private to another module | mark it export or move the use |
diag.module-file-not-found | static | module m "…"; names no readable file | fix the path (relative to the declaring file) or create the file |
diag.module-cycle | static | a file's module body loads itself again, directly or through other files | move the shared items into a third file both name |
diag.module-file-duplicate | static | one file is named by two module declarations | declare it once; reach it by qualified path or import |
diag.const-not-constant | static | a const's initializer is not a constant expression, or a constant depends on itself | use only constants in it (a local const helps), or compute the value in a function |
diag.static-assert-failed | static | a static_assert condition is false, for the program or for one instantiation | fix what the assertion guards, or the assertion |
diag.static-assert-not-constant | static | a static_assert condition (or message argument) is not a bool constant expression | check run-time values with assert; name compile-time values with const |
diag.duplicate-item | static | two declarations add the same qualified name | rename one, or move it into a module |
diag.duplicate-local | static | a name declared twice in one block (a function's parameters and a loop's or pattern's names count as declared in the block they govern) | assign to the existing variable, or choose another name; an inner block may shadow it |
diag.foreign-associated-fn | static | fn T::name in a module that does not declare T (for example fn Vec::first_or outside std) | declare a plain function, or put it in T's module |
diag.no-main | static | no fn main() at the root module (a main inside a module is just m::main) | declare fn main() at the root, or run the file that has one |
diag.div-overflow | both | min / -1 or min % -1 for a signed type | checked_div, or widen first |
diag.transfer-without-authority | both | a move from a binding that no longer holds its resource (already moved or destroyed) | move from the current holder |
diag.type-mismatch | static | an operand or argument has the wrong type | convert explicitly; fix the types |
diag.unbounded-type-parameter | static | arithmetic, comparison, field access or a call on a bare T | use a concrete type |
diag.cannot-infer-type-parameter | static | a generic argument is fixed by nothing | write name<T>(…) or declare the variable's type |
diag.break-outside-loop, diag.return-outside-fn | static | misplaced control statement | move it |
diag.capture-list-mismatch | static | the closure's […] does not equal its free variables | add or remove names |
diag.stale-binding | both | use of a moved-from, dropped, or dead binding or reference | use the current owner; re-fetch after a move or reallocation |
diag.use-of-uninitialized | static | a read on a path with no prior write | initialise on every path; initialise aggregates whole |
diag.reference-escapes-scope | static | a reference stored or returned beyond its referent's block | return an owned value |
diag.lifetime-elision-ambiguous | static | a reference returned from a function with 0 or 2+ reference parameters | one reference parameter, or return owned |
diag.aliasing-conflict | both | a read or write while a conflicting reference is live | end the earlier borrow first |
diag.borrow-exceeds-source | static | &mut taken through a shared reference | take ref<T, exclusive> |
diag.write-through-shared | static | assignment through a shared reference | as above |
diag.destroy-while-aliased | both | drop or scope exit while a reference or guard is live | let the reference's holder end first |
diag.move-while-aliased | both | a move while a reference is live | end the reference before moving |
diag.move-out-of-field | static | moving a resource out of a live struct's field or through a reference | move the whole container, or match a reference (match (&e)) |
diag.borrow-of-non-place, diag.borrow-of-temporary | static | & applied to a value or temporary | bind it first |
diag.no-destroy-authority, diag.transfer-without-authority | both | destroying or moving something this thread does not own | destroy in the owning thread, once |
diag.overwrite-of-live-resource | both | assigning over a resource that is still owned and is not quiet (its end runs a destructor or releases something) | overwrite(&mut x, v), or drop first |
diag.unwrap-failed | dynamic | unwrap or expect of a None or an Err | match, or unwrap_or with a default |
diag.assert-failed | dynamic | an assert condition is false (the formatted message, if any, is shown) | the condition names what was assumed; find why it did not hold |
diag.read-of-resource | static | a resource used where a copy would be made (==, a value read) | move it or take a reference |
diag.bad-destructor-signature, diag.direct-destructor-call | static | drop declared wrongly or called by hand | fn T::drop(ref<T, exclusive> self); use drop(x) |
diag.destructor-on-plain-type | static | T::drop declared for a type that is not a resource | declare the type resource struct T (or resource enum T) |
diag.arith-overflow | both | + - * or negation left the type's range | widen, or use wrapping_/saturating_/checked_ |
diag.div-by-zero, diag.div-overflow | both | division by zero; MIN / -1 | test first or checked_div |
diag.shift-amount-out-of-range | both | shift by the bit width or more | mask the amount |
diag.narrowing-overflow | both | narrow or to_int of a value that does not fit, or a value too wide for a bitstruct field | checked_narrow or narrow_wrapping, or widen the target |
diag.result-discarded | static | an expression statement whose value is a Result, so its error would vanish | handle it (?, match, unwrap), or write _ = e; to ignore it on purpose |
diag.literal-out-of-range | static | a literal that does not fit its type | fix the literal or its type |
diag.recursive-type | static | a struct or enum contains itself by value | put the recursive part behind Box<T>, Rc<T> or Vec<T> |
diag.index-out-of-bounds | both | array or Vec index, or str_byte position, at or past the length | check the index first |
diag.not-ascii | dynamic | String::push_ascii given a byte of 128 or more | build the bytes in a Vec<u8> and check them with String::from_utf8 |
diag.not-char-boundary | dynamic | a StringView bound inside a character's UTF-8 encoding | move the bound to a character's start, e.g. with StringView::find |
diag.non-exhaustive-match | static | a variant with no arm and no _ | add arms |
diag.unreachable-arm | static | an arm whose every value an earlier arm already takes (arms are tried in order) | remove it, or move it before the arm that shadows it |
diag.propagate-outside-fallible-context | static | ? in a function whose return type is not Result<_, E> with the same E | change the return type or map_err |
diag.format-invalid | static | a printf-family format that is not a string literal, or a malformed % specifier | fix the specifier; write %% for a literal % |
diag.spawn-borrow-closure | static | spawn given a closure that borrows | add move |
diag.mutex-reentrant-lock | dynamic | locking a mutex this thread already holds | release the guard first |
diag.channel-zero-capacity | dynamic | Channel::new given a capacity of 0 | give the number of values it may hold; 1 for a hand-over |
diag.channel-deadlock | dynamic | the program's only running thread waits on a full or empty channel that is not closed | send or receive from another thread, or close the channel first |
diag.stack-exhausted | dynamic | calls nested deeper than the stack the implementation provides | recurse less deeply: a loop, or an explicit stack in a Vec |
diag.trusted-outside-unsafe | static | a raw or extern operation outside unsafe | wrap it in unsafe after checking the precondition |
diag.extern-non-ffi-type | static | an unsafe extern fn with a non-FFI parameter or result | pass scalars or rawptr |
diag.alloc-failure | dynamic | Vec or Rc could not allocate | call allocate yourself if you need to recover |