Part I: The Language Core Version: 0.0.1343 (Draft) Date: 2026-11-03
Nikaia is a statically typed systems programming language. Its properties are predictable execution, memory safety and concise source. Memory management, concurrency constraints and representation details are enforced by the compiler. The compiler translates Nikaia source to Rust and drives the Rust toolchain, which is called the backend throughout this specification.
Four weights decide a question about the language (ADR-258). The special case carries the cost, not the common one. The compiler works for the developer, not the developer for the compiler: what can be worked out is not written. The common case has zero overhead: it costs what the same program costs written by hand in the backend. The language is consistent with itself: a rule reads the same wherever its shape appears.
There is one Nikaia. The same source compiles for every target and under every build option, and the resulting program prints the same bytes. A build option decides how a program is built, never what it computes.
A build decides how, never what.
The build options are named below. Each lives under [build] in
nikaia.toml; --target and --user-parallelism override the first two for a
single build. The specification does not state their number. Statements run in
the order they are written; a program that wants work to run beside the code
after it writes async (8.1.2).
target — the machinetarget names the machine a program is built for. The target decides what the
standard library offers on it (Part III 17.2) and what a panic does: the
stack unwinds where the machine unwinds, and the program traps where the
machine traps (Part III, Appendix A). The targets are x86_64-linux,
aarch64-linux, wasm32-unknown, aarch64-android, x86_64-android,
armv7-android, and every triple rustc --print target-list prints, written as
Rust writes it; the default is the machine the compiler runs on. A target
without an operating system (a -none or -uefi triple) is refused.
user_parallelism — whether user code may run concurrentlyuser_parallelism is a permission, not a thread count.
no (the default): no two pieces of user code are ever in flight at the
same time. A data race in user code is impossible under this option.yes: user code may run concurrently. The compiler enforces the rules of
Part II chapter 12 that keep shared data intact.The option applies to user code only. The compiler and the runtime may use
additional threads for internal work, provided that no user code executes
concurrently on them. Such work does not change a byte of what the program
prints. How many threads serve a yes is the runtime’s decision.
The compiler may be concurrent even when the program is not.
Taking a lock while a lock is held is refused with NK2203 when the program is
compiled (Part II 12.3). The re-entrancy check is a build option that decides
whether the program also carries the runtime check that notices such a nesting
if one occurs. It is on by default and may be declined.
For every program that obeys the nesting rule, both builds behave identically. The check cannot fire in a correct compiler. If it fires, the compiler has a defect; without the check, the same defect appears as a silent hang.
The key is reentrancy-check in [build], and it takes yes or no. It has
no command-line override. Declining it removes the check from the crossing
shape of std only; the single-threaded shape keeps its own borrow check.
Some names need no use. They are these, and there are no others:
Vec, String,
Bytes;println, print, eprintln,
eprint;assert and panic, and assume, written only inside unsafe (Part III, 14.2);access_all and update_all
(Part II, 12.3);Shared, SharedMut and Locked (6.2);Nothing in the prelude does I/O but printing, and nothing in it pauses. The
prelude is not cut down on a machine with no filesystem (Part III 17.2). HashMap is outside it:
a program writes collections::HashMap after use std::collections.
The compiler decides the shape of a SharedMut[T] per value. A program writes SharedMut(0) with no import
(6.2, Part II 12.2).
eprint and eprintln, like panic and assert, are diagnosis; what they do
depends on the target, and the compiler decides it. access_all and
update_all take the locks in one order however the program wrote them, so a
deadlock cycle cannot form. Bytes is one shared buffer: what fs::read
hands back, passed on by a count. panic ends the
program with the program’s own words, at the Nikaia line.
Everything else in the standard library is reached the way a package is
reached: use std::fs at the top of the file, and
fs::read_to_string(path, fs::Root::Anywhere) where it is used (9.1). A name
that lives in a std module is refused without its prefix — NK1117 for a
function, NK1135 for a type — and a prefix is refused without its use, each
with the line to add. A name on this list written with a std module in front of it, such as io::println, is refused too (NK1163).
A comment begins with // and runs to the end of the line, or begins with
/* and runs to the matching */. A block comment may span lines and may
stand anywhere whitespace may stand. Block comments nest: /* a /* b */ c */
is one comment. An unclosed /* is reported where it opened. A block comment
is a comment wherever it stands, including /** … */ and /*! … */.
A run of /// lines immediately before an item is that item’s
documentation. An item is a fn, a struct, an enum, a trait, a field or
a variant. Anywhere else /// is an ordinary comment. An ordinary comment
standing between the run and the item does not end it; a statement does. A doc
comment is prose: the compiler reads no directive, no @param and no link out
of it. The compiler does not copy it into
nikaia.contracts (Part III, 13.5; ADR-251 D2).
A variable is a named storage location that holds a value. A variable is
declared with let.
Immutability A variable is immutable unless it is declared otherwise. Once a value is assigned to an immutable name, the name cannot be assigned again.
let x = 10
// x = 20 <-- This would cause a Compiler Error
Mutability
A variable that may change is declared mutable with the keyword mut.
let mut y = 10
y = 20 // This is allowed
Deep immutability: immutable
let promises that the name is not assigned again; a SharedMut it holds can
still change through another owner. immutable promises more: neither the name
nor anything reachable from its value ever changes (ADR-389).
A value whose type reaches, through fields, elements, variants or captures, a
SharedMut, a Locked, a channel end or another handle to storage changeable
through another owner is refused (NK1331). Nothing is moved out of it and no
mut self method is called on it. A task may read it, captured or handed
over, without a copy or an owner count (8.3).
fn main() {
immutable limits = load_limits()
let a = async fn { check(limits) } // no copy, no count
}
Where an item stands, immutable X = f() is evaluated on first access, exactly
once, even when several tasks reach it together; every later access reads the
same value. Nobody can change it, so it is not global state. Its initializer may
not throw; a value that may be missing is a T?. A cycle among item-level
immutables is refused (NK1331). A value known at build time is a comptime
(Part II, 10.2).
immutable DUMP_DIR: String? = env::var("NIKAIA_DUMP_ACCEPTED")
Taking a tuple apart. let (a, b) = pair() binds each name to the part at its position. The names may nest as the tuple does, let (a, (b, c)) = (1, (2, 3)): a let takes a name, _, or a parenthesised list of these, as deep as it is written. A written type on a tuple is refused (NK1136), because one type cannot say which name it is about. A variant is refused (NK1259): it may not be the one the value holds, and taking it apart is a match’s. A parameter takes the same pattern, in a function, fn dist((x, y): (f64, f64)) -> f64, with the type of the whole after it, and in a lambda, fn((name, v)) { … }. It is still one parameter: callers, the function’s type and its count of arguments see one tuple. mut before it belongs to the whole, so the parts are changeable; without it they are views. An option, which a call names, is never a pattern.
Reserved words
A reserved word means one thing wherever it appears. A name may not be a reserved word. The reserved words are:
as async break catch comptime continue dsl else
enum extern false fn for grammar if immutable
impl in let match mut null pub ref
return select self spawn struct sync test throw
throws trait true unsafe use while with
A name that is a reserved word does not parse, and the parse error names the word and says it is reserved.
ref is reserved for the pointer an extern "C" declaration writes (Part III 15.1); anywhere else it is refused (6.5).
comptime is reserved for its construct: a binding where an item stands or a statement inside a function
body (Part II, 10.2). It promises a time of evaluation, not constancy.
_ is not a name; it is the ignore pattern. It stands where a name would be
bound and says that the value is ignored on purpose: a position of a
destructured tuple (let (name, _) = pair()), a parameter a shape dictates
(fn handle(event: Event, _: Context), fn(_, value) { … }), and a part of a
match pattern ((0, _), Shape::Circle(_)). The arm taken when nothing else
matched is else, not _ (3.4). let _ = expr is refused: a call made for its effect is written as the
call, and a resource is closed by name. _ is never a value. What _ ignores
is not moved, so a let over a place stays a view of it (6.5).
with is reserved for its construct: a copy of a value with named fields
changed, p with { x: 1 } (4.2).
extern and unsafe are reserved for their constructs: an extern
block, and the unsafe { … } a call into one is written in (Part III 15.1).
select is reserved for its construct: the block that races several
waits and takes the first to finish (Part II, 12.4).
break and continue are reserved for their constructs (3.3).
async is reserved for its construct: the prefix that starts a task (8.1.2).
spawn stays reserved until the move to async(detached) is built (8.2): spawn fn { … } is still accepted and means async(detached) fn { … }, and the commit that removes the form releases the word (ADR-async-5 D13).
test is reserved for its construct: a test "name" { … } block of the
package it stands in (Part III 14.1).
loop, const, macro and quote are ordinary names. A declared
loop or quote is a name like any other. Where nothing declares one
of them, the refusal every undeclared name meets carries a hint: loop { … } is
answered with write while true, const X = … with write comptime, and
macro and quote with Nikaia has no macros.
seq is an ordinary name.
keep and from are not reserved; each means its construct in one position
(ADR-views-3 D13). keep is the mark only before a
parameter or self in a parameter list (6.5), from the source only after a
result type (6.6); both are names everywhere else.
Three further rules about the list:
grammar block has its own vocabulary, and it is not on the list:
rule, boundary, fold, par_fold and unchecked are keywords inside
a grammar (Part II, 10.1) and ordinary names everywhere else.:: or a ., a reserved word is a name. A segment follows a ::
and a member follows a ., and no construct begins in either position, so
Self::dsl (Part II, 10.5) and h.start() on a handle named start are
what they look like.self is on the list and is also a name: the receiver of a method.
A method body uses it; user code may not declare it. Declaring self at a
let, a for binding, a lambda’s argument or a struct field is refused with
NK1119; a parameter named self does not parse, and the message says so.Nikaia provides basic types to represent simple values.
int: an integer with the values of an i64. It is the type of most
numbers. It stops the program where an i64 would, and an int and an
i64 take each other without as. How many bits hold it is the
compiler’s choice: fewer only where no value, no result and no abort
changes. An extern "C" signature and a described Rust contract take a
fixed width; an int there is refused with NK1262
(ADR-285 D36-D39).i64: a 64-bit integer, exactly that width in memory and in arithmetic,
as each fixed width below is. It is the type of a length:
xs.len() hands back an i64, and an index is one.i32: a 32-bit integer. It is written where the layout matters: a
struct that has to be small, a wire format, a C header. An i32 that
meets an i64 becomes one (below).u8: one byte. It has the same conversion and arithmetic names as the
other integer types. A file read whole is a Bytes (fs::read): one
shared buffer of them, handed on by a count and not copied, and read
as a run of bytes (2.6). text.bytes is a text’s UTF-8 as u8s.u64 and u32: unsigned, 64 and 32 bits. They are written where the
bits are the point: a hash, a mask, a wire format’s field
(ADR-285).f64: a double-precision floating-point number. A literal may carry an
exponent, always written e: 1.5e-4, 2e3,
9.54791938424326609e-04. 1E5 is refused.f32: a single-precision floating-point number, for data where the size
of a value matters more than its digits. A float literal is an f64
unless its use asks for an f32: let w: f32 = 0.5. An f32 and an
f64 do not mix without as.bool: true or false.String: text, and always valid UTF-8: bytes that are not are refused
where they enter the program, with NotText. A file name need not be
text, and is an fs::Path (Part III 17.1). Whether a value of it is a view into text that is
already there, or text of its own, is the compiler’s decision per use
(6.6). A literal is a view of the program’s own text, wherever it goes:
where only literals and other views are put into a position, it holds
the view and nothing is allocated; where text of its own is put there
too, the literal is constructed there. Wherever a program declares String -
a field, a result, a parameter, a let, the element of a list, the key
or value of a map - it is text of its own where only text of its own is
put there, a view where only views are, and either, per value, where
both are; a published field or result is never the third. What a
published field, parameter or result is - a view, and of which
parameter, or text of its own; lent or kept - is recorded in the ledger
(Part III 13.5).ref String: a view of text, written only in an extern "C"
declaration and on a pub extern fn (Part III 15.1), where it is
required and the body is held to it.scalar: one Unicode scalar value - a code point that is not a
surrogate - and not a byte. It is written between single quotes, with
the escapes a string uses: 'a', 'ä', '\n' (2.6). Walking a text’s
scalars yields it (for c in name.scalars), and a match over one
compares against it (3.4). There is no char: what one character is
depends on the unit, and 2.6 names the three. An integer becomes one
by name, scalar::from(n): n is an i64 (a narrower integer widens
into it), and the call’s precondition is that n is a scalar value -
not negative, not a surrogate (0xD800 to 0xDFFF), not above
0x10FFFF. It is held as every precondition is (Part III 14.2): shown
by the caller’s facts, a literal, an if or an assert, or refused
(NK1300). Only a u8 becomes one by as, since every byte value is
one: b as scalar.Array[T], with no length: a run of T of any length, read with
.len(), xs[i] and for. Where it does not own, it is a view of
the run, with the promise a view of text carries — this points at
elements somebody else keeps, no copy and no handle
(ADR-views-3 D11). Array[u8] is the byte buffer.xs: Array[T] is a view, and mut xs: Array[T] a
changeable one; the caller hands an Array[T, N], a Vec[T] or a piece
of either, so total(xs) for a Vec[i64] needs no word. A keep
parameter owns what it is given and is generic over its length: the call
says which, and two such arrays in one signature are two lengths.Array[T] is a view
where what flows into it is a piece of a buffer, tethered as text is
(6.6). Where it would have to own, it is NK1182, and the message names
the ways out — Vec[T] to own and grow, Array[T, N] to write the
length down. A run the program builds while it runs is a Vec[T];
NK1179 refuses a field declared as a view that is given a run the body
owns.comptime declared Array[T] is a view of data the build fixed, the
form a Vec[T] crosses as when a build hands one to the program
(10.2): a const the program reads, and nothing allocates. windows
and chunks yield Array[T] views.A number may carry a digit separator, a radix prefix or a scale, and nothing
else. 1_000_000, 0xFF, 0b1010 and 0o17 are the four forms, beside the
exponent above. A decimal whole number may end in a scale: K, M, G,
T and P are 10³ to 10¹⁵, Ki, Mi, Gi, Ti and Pi are 2¹⁰ to 2⁵⁰
(ADR-322). 1G is 1000000000 and 4Gi is 4294967296. A
radix-prefixed number and a float take no scale. An underscore stands between digits and nowhere else. A
float takes the separator (1_000.5) and no prefix. A digit the radix does not
have (0b1210, 0o19) is refused. A leading underscore is not one of the
four forms: _000 is a name.
The radix is a spelling, and a literal is a value. 0xFF is 255, and it
takes the first type that holds it exactly as 255 does. So let mask = 0xFF
is an i32 and let mask: u8 = 0xFF is a u8. The width is never read off
the digits. The separator and the scale are not part of the value: 1_000 is 1000 and
1K is 1000 to the checker, to the ledger and to a diagnostic’s text.
There is no type suffix. 1i64 is a number beside a name, and the name
beside it is refused with NK1117 because nothing declares it (Part III, C.3).
A word that stands on its own is read as a name, so assert c is refused the
same way.
A number too wide for a u64 is refused (Part III C.1), and one above
an i64 is a u64’s: let basis: u64 = 0xcbf29ce484222325.
These are the integer types a program writes: int, and the five fixed
widths i64, i32, u8, u64 and u32. A fixed width is written where the
width is the point; neither is a warning anywhere. The specification does not
offer others: a written usize,
isize, i8, i16, u16, i128 or u128 is refused with NK1135, and so is
str, outside an extern "C" declaration (Part III 15.1). A length is an i64:
for i in 0..<xs.len() gives an i64, xs[i] takes one, and neither
conversion is written, because the compiler emits both. A negative index
reports as an access out of bounds (Part III, A.2).
An integer that does not fit aborts, at every build. An i32 holds what an
i32 holds. An arithmetic result that does not fit is an inconsistent program
state, like an index past the end of a list or a division by zero (Part III,
A.2), and the program stops. The rule is the same at both values of
user_parallelism and in every build (1.2).
Three cases abort although the code looks harmless:
-x and x.abs() on the smallest value of a signed type, because there is
no matching positive one;-1, for the same reason;Where a program means to wrap or to stop at the limit, it says so by name.
let h = h.wrapping_mul(31).wrapping_add(c) // a hash, wrapping on purpose
let volume = level.saturating_add(increase) // stops at the maximum
let n = a + b // aborts if it does not fit
The names are wrapping_add, wrapping_sub, wrapping_mul, wrapping_div,
wrapping_neg, wrapping_abs, wrapping_shl, wrapping_shr, and the same
names with saturating_, except that there is no saturating_shl and no
saturating_shr. They exist for i32, i64, u8, u32 and u64; an unsigned
type has no _neg and no _abs. There is no operator for either.
The bits of an integer are reached with operators
(ADR-285): &, |, ^, << and >>, and !, which on
an integer flips every bit. Both sides are one integer type, except a shift’s
count, which may be any. A shift loses the bits it moves out of its type (a
shift of numbers alone takes the type that holds its exact value, 2.4), and a
count as large as the type is wide aborts; >> fills with zeros on an unsigned type and
with the sign on a signed one. They bind tighter than a comparison, and one
written beside a comparison is written in parentheses (NK1197), because C
reads a & mask == 0 the other way:
fn fnv(key: String) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325
for b in key.bytes() {
hash ^= b as u64
hash = hash.wrapping_mul(0x100000001b3)
}
return hash
}
fn main() {
let h = fnv("hello")
println(f"{h >> 32} {(h & 0xff) == 0x0b}")
}
On a bool they are refused (NK1198): && and || join two bools.
A rotation puts the bits it moves out back in at the other end:
x.rotate_left(n) and x.rotate_right(n) exist for u8, u32, u64, i32 and i64. The
count n may be of any integer type and is taken modulo the type’s width, so
no count aborts: w.rotate_right(-1) is w.rotate_left(1).
x.low_bits(n) is x with only its n lowest bits kept, for u8, u32 and
u64: none where n <= 0, all where n is at least the width; no n aborts.
One integer type becomes another without as where no value can be lost
(ADR-285 D32). Two integer types have a common type when
one type holds every value of both:
u8 |
u32 |
u64 |
i32 |
i64 |
|
|---|---|---|---|---|---|
u8 |
u8 |
u32 |
u64 |
i32 |
i64 |
u32 |
u32 |
u64 |
i64 |
i64 |
|
u64 |
u64 |
none | none | ||
i32 |
i32 |
i64 |
|||
i64 |
i64 |
let with
an annotation, an argument, a return or a body’s last expression, a field
in a struct literal, an assignment, an element of a list whose element type
is stated.NK1199) and so is the list (NK1154).Vec[u32] is not a
Vec[i64].fn area(x: i32, y: i32) -> i64 {
x * y // computed in i64
}
let total = count + offset // u32 + i64 is an i64
let n: u64 = xs.len() // a length is never negative
A stated type reaches through +, -, *, /, % and a negation to the
operands, each widened before the operation: x * y above is computed in
i64, and does not stop where an i32 would. It does not reach into what a
name was given, into a call’s arguments, a method’s receiver or an index. It
stops at &, |, ^, !, << and >> and at the wrapping_,
saturating_ and truncating_ names, whose result depends on the width: a
computation of them in a narrower type, put where a wider one is stated, is
refused (NK1215), and the program writes (a as i64) << 3 or
(a << 3) as i64.
A value the compiler shows is not negative goes into an unsigned type
without as: a length, and a value after if k >= 0 or assert k >= 0.
Where nothing shows it, k as u64 is refused with NK1241, and the program
writes k.checked_u64() or k.bits_as_u64().
Widening plays no part in deciding a type. A number without an annotation
(2.4) and a type parameter (4.6) take the type their uses ask for, and two uses
asking two types are refused (NK1200) though one would widen into the other.
pick(a, b) for fn pick[T](a: T, b: T) over an i32 and an i64 is
refused; pick(a as i64, b) is not.
A conversion is written as where it always fits, or where the compiler shows it does.
let average = (total as f64) / (count as f64) // widening, always fits
let small = big as i32 // only where `big` is shown to fit
A conversion that can lose a value - a wider integer to a narrower one, a
signed one to an unsigned one or back, an f64 to an integer or to an f32 -
is written as only where it is shown to fit. A constant that does not fit is
refused with NK1116. A value the compiler proves within the destination’s
range converts. Any other such as is refused with NK1241, and the program
says by name what it means: the low digits, or whether it fits. This does not
depend on a build option. An integer becomes a scalar by as only from a
u8; from any other integer type, a variable or a literal alike, as scalar
is refused with NK1241, and the program writes scalar::from(n) (2.2) or
the character, '\u{8}'.
Where a program means to keep only the low digits, it says so by name, as it does for wrapping. The name carries the type it converts to:
let small = big.truncating_i32() // keep the low digits, on purpose
let floored = measurement.truncating_i32() // a float, toward zero, clamped
let n = text.bytes.len().truncating_i32() // a count that may not fit
The names are truncating_i32, truncating_i64, truncating_u8,
truncating_u32 and truncating_u64. Each converts out of every integer type
wider than its own, signed or unsigned, and out of an f64: the low bits of
its width, read as its type. None converts out of a type of the same width.
rounding_f32 is the nearest f32 out of an f64, and an infinity beyond
the f32 range.
Where a program means to read the same bits with the other sign, it says that by name:
let word = b.u32_le(0).bits_as_i32() // 0xFFFFFFFF is -1
let key = offset.bits_as_u64() // -1 is 18446744073709551615
bits_as_i32 converts out of a u32, bits_as_u32 out of an i32,
bits_as_i64 out of a u64 and bits_as_u64 out of an i64, and out of
nothing else. Every bit stays where it is; no value is checked. A number that
fits both types keeps its value, and one that does not comes out on the other
side of zero. A same-width truncating_ name is refused (NK1210), naming
the bits_as_ one.
Where a program means to ask whether it fits, it says so by name too (ADR-315), and the answer is optional:
let sum = a.checked_add(b) ?? return null // null where it does not fit
let small = big.checked_i32() ?? 0 // null where an i32 does not hold it
The arithmetic names are checked_add, checked_sub, checked_mul,
checked_div, checked_rem, checked_neg and checked_abs, for the five
types the wrapping names have and without _neg and _abs on an unsigned one;
a division by zero is null too. The conversions are checked_i32,
checked_i64, checked_u8, checked_u32, checked_u64 and checked_f32, out of an
integer type that has values the destination lacks and out of an f64. A
value that is not a number fits no integer; an infinity and a value that is
not a number carry over into an f32.
Two conversions lose digits without anything failing:
f64 to an f32 rounds to the nearest f32 where it is within the
range. An f32 to an f64 always fits.f64 is not checked. Digits are lost at large values
without anything overflowing: 9007199254740993 through an f64 comes back
9007199254740992. This is a limit of the language, not an abort.An as names one of the types above and nothing else. n as u128 and
n as usize are refused with NK1122, naming the type.
Where the backend wants a machine-width number, the compiler writes the
conversion. A length comes back as an i64 and an index goes in as one. A
count goes in as one too, so " ".repeat(indent) is written with no
conversion. A negative count aborts with “a count cannot be negative”.
A literal that does not fit its type is a compile error, not an abort.
let x: i32 = 3000000000 is refused with NK1116, and so is a sum of literals
that cannot fit. NK1116 fires wherever a type stands beside the constant: an
annotated let, a return against a declared result, or an argument whose
parameter says what it takes.
The compile-time check reaches a sum. let b = a + 1, where a is a
constant an annotation declared an i32, is folded and refused with no
annotation on the let line: the operand’s declaration gives the arithmetic
its type. + - * / % and a negation fold, through any number of immutable
lets, in a magnitude and a sign (65 bits), each step held to the type a declared operand gives it, so that the message can name what
the expression comes to. A mut local, a parameter, a for binding and a cast
stop the fold. An expression that does not fold is never refused.
A division whose divisor is a constant zero is refused with NK1118. Every
other division by zero is unrecoverable at run time and names its line
(Part III A.2).
A literal that nothing constrains, let big = 3000000000 on its own, is not
refused: it takes the first type that holds it (2.4).
A type is non-nullable unless it says otherwise. A variable of type
String always holds a string and is never null. A type that admits the
absence of a value is written with a trailing question mark ?.
let strictly_string: String = "Hello"
// strictly_string = null // Error!
let mut maybe_string: String? = null // Valid
maybe_string = "World" // Valid (`mut`, as in 2.1)
A literal is a view of text the program was compiled with (6.6), and it
stays one wherever a String is wanted. Where the use only reads it, or
keeps it in a position - a field, an annotated let, a return, an element
of a list, an argument the callee keeps - into which only literals and other
views flow, nothing is allocated: the position holds the view, and static text
outlives every name. Where text of its own (an f"…", a .clone(), a String
handed over that no view still reads, 6.6) flows into the same position, the literal is constructed there, as
[1, 2] is a Vec where one is wanted. A literal in a String that is changed, or handed on to
what takes text of its own (a mut or owning parameter, also through a
match binding or a lambda’s last expression), is text of its own:
let mut y: String = "hi" then y.push_str("!") needs no copy written. A view of text the program has is not a literal,
and where it is kept .clone() makes the copy, written where it happens. A view
in a published String slot that text of its own also flows into is refused without it (NK1106); any other String slot takes the view (2.2). .clone() is the one
word for a copy, of text and of everything else: a copy of text is text of its
own. .to_owned() is refused naming it (NK1189); .to_string() is the text
form of a value, and the text form of text is the text itself: a view stays a
view, and nothing is copied. On a plain text literal neither .to_string() nor
.clone() is written: the literal is the text (NK1301).
T? lowers to the backend’s Option<T>, the mapping Part III 15.2 writes
the other way round, and null is a reserved word (2.1) that lowers to None.
The ? comes last, after the type’s arguments: Vec[i64]? is a nullable list
and Vec[i64?] is a list of nullables. A tuple takes it too: (String, String)? is a pair that may be missing, and p?.0 is its first part as a nullable.
A plain value stands where a nullable one is wanted. That is the one
widening the language has, and the compiler writes the constructor. There is no
Some in Nikaia. The constructor is written wherever a plain value meets a
nullable slot: an annotated let, an assignment, a return, a struct-literal
field, and a call argument. Where the compiler cannot work out the value’s type,
it writes a conversion instead, which is right whether or not the value is
already nullable.
A test that proves a T? is not null makes it a T where the proof
holds (ADR-382), for a local, a parameter, or a path of
fields from one (p.name, self.cache):
if x != null && x > 3 { use(x + 1) } // `x` is an i64 on the right and in the body
if x == null || x > 3 { … } // and on the right of `||`
if x == null { return } // and in the rest of the block
while x != null { x = next(x) } // and in the body, up to the write
The proof holds in the operand after && (what the left proves true) and
after || (what the left proves false), in the branch the condition leads
into (the else branch gets what it proves false), in a while body, and in
the rest of a block after an if whose branch leaves with return, break,
continue or throw, and in the body of a match arm whose guard proves it
((x, _) if x != null => x; no other arm sees that proof). ! swaps true
and false. Elsewhere the name is the
T? it was declared. An assignment to the name after the test ends the proof
there, and a loop inside the proven region that assigns it ends it for the
whole loop; a use that needs a T after that is NK1102, naming the
assignment.
A path of fields is proved like a name, at any depth. Its proof ends at an
assignment to the path or to a path it is part of (p.name = …, p = …), at
a mut hand-over of either (a mut self method called on it, a mut
argument), and at a lambda or a task (an async form, a par_iter element, a select arm) that sees
the root; a write to a sibling (p.age = 3) and a call that only reads keep
it. A read through an index or a call (v[i].f, f().x) is not a path and
proves nothing. A binding in a condition (if let v = x, if x is Some(v))
is refused: match binds the value an enum or a T? holds.
let m = null with nothing beside it is not refused by the compiler:
let mut m = null followed by m = "hi" is a correct program. Where nothing
ever says the type, the backend asks for the annotation.
Nikaia is statically typed: the type of every variable is known at compile time. A type is rarely written. The compiler uses type inference to deduce the type from the value.
let name = "Nikaia" // Compiler knows this is a String - a view of static text
let count = 42 // an i32, because nothing here asks for anything else
A number takes the type its use asks for. Where nothing asks, it takes the
first type that holds it — i32, and i64 where an i32 is too small:
let count = 42 // an i32: nothing asks, and an i32 holds it
let big = 3000000000 // an i64: nothing asks, and an i32 does not
let m = 3000000000 // also an i64 - here because the line below asks
println(f"{wide(m)}") // fn wide(n: i64) -> i64
let small = 42
println(f"{wide(small)}") // an i64 here: the use decides, and 42 holds in one
The use is asked first and the size second. So small may still become an
i64, and big never has to be annotated to be one. An integer comptime is
such a number whatever computed it, and each use may take it in its own type
(Part II 10.2). A number too large for an
i64 is refused unless a use asks for a u64.
A use is anything the function does with the name: the parameter it is
handed to, a typed value it meets in an operation or a comparison, an annotated
let, a field or a return it goes into, a value assigned to it or from it,
and an index, which asks for an i64 only where nothing else asks. The compiler
reads every use in the function and writes down the type it decides, among all
five integer types. Two uses that ask for two types are refused (NK1200), with
the annotation to write. Where no use asks, what the name is given decides:
let mut n = 1 followed by n = 3000000000 is an i64, and a value it is
given that the decided type does not hold is NK1116, naming the use.
A sum of numbers is a number, so the same rule decides it. So are a product and a shift: the value is computed exactly, and a shift loses bits only in a type a use asks for (2.2):
let c = 2000000000 + 2000000000 // an i64: nothing asks, and an i32 does not hold it
let small = 2 + 3 // an i32, the same as any number that fits
let k = 1 << 40 // an i64: 1099511627776
let top = 1 << 31 // an i64: 2147483648
let bit: i32 = 1 << 31 // -2147483648: the i32 is asked for
A name is where the widening stops. A name already took a type, and arithmetic happens in the type of its operands where no wider type is stated for it (2.2):
let a = 2000000000 // an i32 - the first type that holds it
let c = a + a // refused: this comes to 4000000000, which an i32 does not hold
let b: i64 = 2000000000 // say so, and the sum is an i64
let d = b + b // 4000000000
The way out is the annotation on the third line.
The question is about the value, not the digits: -2147483648 is an i32.
NK1116 answers the case where a type stands beside the literal (an
annotated let, a return against a declared result, an argument whose
parameter says what it takes) and is silent otherwise (Part III, C.4). An
operation over names whose type is pinned is asked wherever it stands - in an
f"…" hole, a condition, a list - and refused once, at the innermost operation
that overflows. A
constant is widened where an i64 holds it and refused with NK1116 where no
type does or where a name pinned a narrower one. Two positions take their type
from where they stand and are left alone: a sequence index and a repeat
count, both counted by the machine.
There are two string literals, and the difference is one character at the front.
let name = "Nikaia"
println(f"hello, {name} - {name.to_uppercase()}") // f"…" - the braces are code
println("hello, {name}") // "…" - the braces are braces
"…" is text. A { in it is a brace and nothing else, so a program that
writes JSON, CSS or a regular expression writes the braces as they are:
print("{}") // prints {}
print("\\d{3}") // a regular expression, written as one
print("{ margin: 0 }") // a rule, not a hole
f"…" has code in it. Between { and } stands an expression: a name, a
field, a call, an index. It is written as code is written anywhere else, so a
string inside a hole has plain quotes: f"{greet("Ada")}". What follows a : inside a hole says how to write
the value rather than which value. The first colon that is not inside a call or
an index separates the two, so move(by: 1) in a hole keeps its own colon.
The format after the colon is a closed set, as in Rust and Python, in this
order, each part optional: a fill character and an alignment, a 0, a width, a
precision, a base.
| written | what it does |
|---|---|
< > ^ |
left, right or centred in the width; a character before it is the fill ({s:*^9}), a space where none is written. Without one, a number stands right and text left |
0 |
a number is filled with zeros after its sign: {n:05} is -0042 for -42 |
| a width | the least number of scalars written, as decimal digits: {s:>8} |
. and digits |
a float’s digits after the point, rounded: {x:.2} is 3.14 |
x X b o |
an integer in hexadecimal (lower or upper case), binary or octal, with no prefix: {h:016x} |
The width and the precision are literal digits. A base on a value that is not an
integer, a precision on one that is not a float, a 0 on one that is not a
number, and anything outside the table are refused (NK1332), and the message
names the table.
A hole is written as its value’s text, and so is what a print call is
handed. Text, a number, a bool, a scalar and an error (whose text is its
message) have one. A list, a map, a set, a range, a tuple, a value behind a
lock, and a struct or enum of the program that is not an error have none:
f"{p}" and println(p) are refused alike (NK1201), and the help names the
parts to write instead, such as {p.name}.
Inside an f"…", two braces stand for one: {{ is a literal { and }} a
literal }. An escape is not a hole: the { in "\u{0041}" belongs to the
escape. A } on its own is an error. A plain string needs none of that: it
has no holes, so "{" is a brace and "{{" is two.
The f and the quote are one token. f"x" interpolates; f "x" is a
variable named f beside a string. Whitespace never decides what a program
means. An f"…" is not a constant: it cannot stand as a default, a match
pattern or a grammar’s literal. Inside a hole, \" is refused - a hole is
code, and a string in it has plain quotes.
A newline is an ordinary character in a literal. Nothing ends a literal but
the closing ". A literal left unterminated runs on to the next " in the file
or, if there is none, to the end of the file.
A \ introduces an escape, and the set is closed:
| written | what it is |
|---|---|
\n \r \t |
newline, carriage return, tab |
\0 |
the zero character |
\\ \' \" |
the backslash and the two quotes, standing for themselves |
\xNN |
two hexadecimal digits, naming a byte: below \x80 in text, any byte in a '…' that is a u8 |
\u{…} |
up to six hexadecimal digits, naming any scalar: \u{1F600}; a surrogate is refused |
The same set holds in a '…', in an f"…" and in a template. A \ in front of
anything else is refused (NK1184), naming the set; a literal backslash is
written \\. A character above \x7F is \u{…}. The set is the backend’s,
and a literal is written into the generated file as it stands.
The type follows the syntax and the use, never the contents. "…" is a
view of static text and f"…" builds a String, whether or not it has a hole
in it. Adding a brace to a piece of text cannot change its type. Where the use
asks for an fs::Path, an f"…" builds one, and its holes may be a path, text
or a number (Part III 17.1); a let without a type takes it from the binding’s
first use. A path in a hole of an f"…" that is text is refused (NK1201): the text is
written p.to_text() or p.display(). Handed to print or println, a path
in a hole is written as its bytes are.
A template’s holes need no f. dsl html { <p>{name}</p> } eod (Part II)
marks the construct as a place where code appears.
Text is UTF-8 (ADR-320), and a position in it is a
text::Pos (ADR-374). A text is read through a view that names what
it counts:
| view | an element | reads |
|---|---|---|
s.bytes |
u8 |
s.bytes[i], s.bytes[a..<b], s.bytes.len(), s.bytes.find(…), s.bytes.iter(), for |
s.scalars |
scalar |
take(n), skip(n), count(), for |
s.graphemes |
String, a view of one grapheme |
take(n), skip(n), count(), for |
s.bytes is a Bytes (Part I 6.6) over the text’s own buffer, borrowed: it
costs no copy unless it is kept beyond the text. There is no s.len() and no s[i]. s.is_empty() needs no view. split,
lines, trim, starts_with, ends_with, contains, == and find work
on the text itself.
let preview = title.graphemes.take(10).collect() // ten things a reader sees
let field = record.bytes[0..<8] // eight bytes of a fixed format
for c in name.scalars { if c == 'ä' { … } }
A position in a text is a text::Pos, a boundary in that one text. It
comes from the text: s.start(), s.end(), a match, a move, a grammar run
over the text. No number becomes one, and it takes no arithmetic; two
positions of one text compare with < and ==. Below the language it is a
byte offset.
A search gives a match. s.find(needle; from: text::Pos) -> text::Match?
finds the first match at or after from (default s.start()), s.rfind(needle)
-> text::Match? the last. A match has start and end, positions of s, and
m.text(), the matched text. A needle is a text or one scalar, s.find(','),
as the pattern of contains, starts_with, split and replace is (#877).
A pattern is searched with std::regex (ADR-regex-1).
The dialect is RE2’s, as Rust’s regex crate writes it: no backreference, no
lookaround, a search in time proportional to the text times the pattern. A match
is the leftmost one, among those the one the pattern prefers. Text is matched by
scalar, and \d, \w, \s are Unicode classes; (?:…) does not capture.
A regular expression is a regex::Regex[E], E its engine, a type implementing
regex::Engine: regex::Literal, regex::OnePass, regex::Dfa,
regex::TaggedDfa, regex::BitNfa, regex::FlatNfa, or regex::Dynamic for a
pattern compiled at run time. regex::compile(comptime pattern) ->
regex::Regex[E] takes a pattern the build knows - a literal, a text composed of
comptime values, asset("…") - and the build chooses E from it; a type
written where the result is received is held to, and a pattern that does not
parse stops the build (NK1244). regex::dynamic(pattern) ->
regex::Regex[regex::Dynamic] throws regex::Error takes a pattern known only at
run time. regex::Any holds a regular expression of any engine (re.any()).
re.is_match(s) -> bool, re.find(s; from: text::Pos) -> text::Match?,
re.find_all(s) -> Seq[text::Match] (not overlapping), re.captures(s; from:
text::Pos) -> regex::Captures? with c.group(i) and c.named(name), each a
text::Match? (group 0 the whole match), re.replace_all(s, with) -> String
($1, ${name} in with) and re.split(s) -> Seq[String]. Every position
is a text::Pos of s. --comptime prints for each known pattern its engine
and what the calls on it became.
A text is cut by name, at its own positions: s.before(p), s.after(p),
s.between(p, q), and s.split_at(p) for the first two at once. Each is a
view of s. A text takes no […]. between with q before p stops the
program.
let m = line.find(";") ?? return
let name = line.before(m.start)
let rest = line.after(m.end)
A position moves only in a unit it names: p.forward(scalars: n),
p.forward(graphemes: n), p.forward(bytes: n), and back with the same
options. Each gives a text::Pos? of the same text, null past either end
and, for bytes:, inside a scalar. Exactly one option is written (NK1285).
let s = "abcdefg"
let m = s.find("ab") ?? return
let q = m.end.forward(scalars: 4) ?? return
println(s.between(m.start, q)) // abcdef
A number is reached through a view: s.before(p).bytes.len() is the byte
offset of p, s.before(p).scalars.count() its scalar count.
A position cuts, moves and searches only the text it came from. The
compiler keeps which text that is as it keeps a view’s sources (6.6), through
parameters and results. A position of another text, or of a text chosen at
run time, is refused (NK1214); so is a number, and the help names
forward and the view. A position of row = t.between(k, e) is one of
row, not of t. A cut at positions is not checked when the program runs.
A position is kept in a text::Cursor, never in a field (NK1284). A
cursor is a text and a position in it: text::Cursor(t) stands at
t.start(); c.at() is the position, c.go(p) sets it to one of the same
text; c.until(needle) -> String? is the text from the position to the
next needle and moves past it, or null without moving where none follows;
c.rest() is the text from the position, c.text() the whole text.
let mut c = text::Cursor(t)
while true {
let row = c.until("\n") ?? break
let (name, temp) = row.split_once(";") ?? continue
…
}
A Bytes is one run of bytes, whatever holds its memory: what it allocated, a Vec[u8] handed over with Bytes(v), which copies nothing, or a mapped file (fs::Mapped is a Bytes, Part III 17.2). It reads the same in each case and is handed on by a count. A Bytes parameter, lent, takes any of them, and a text’s s.bytes.
A Bytes is read as a run of bytes: b[i] -> u8, b[a..<b], b.len(),
b.find(byte; from: i64) -> i64?, b.iter(), the bytes one at a time as a
list’s iter gives its elements, b.starts_with(prefix) and
b.ends_with(suffix), each a Bytes compared byte for byte, and
b.text(), which checks UTF-8 and is
NotText where it fails, and b.text_lossy(), an owned copy in which each
run that is not UTF-8 is U+FFFD. An index outside stops the program, as a list’s does.
A Bytes searches for a set of bytes (ADR-375):
bytes::ByteSet([';', '\n']) is such a set, of any bytes, built once. A set
written empty is refused (NK1287); one empty at run time finds nothing.
b.find(set; from: i64) -> i64? is the first byte of the set at or after
from, b.find_all(set; from: i64) -> Seq[i64] every one, rising, and
b.split(set) -> Seq[Bytes] the pieces between them by text’s split
rules: neighbouring bytes of the set give an empty piece, one at the end an
empty last piece, an empty b one empty piece. b.find_all(set).count() counts without making a position. Each position find_all yields is at least from and below b.len(), and a proof may use it (Part III 14.2). find_all takes scan: bytes::Scan: Scan::ByBlock, the default, reads the next block when the hits of the last one are spent; Scan::AfterHit looks for each hit in the half block right after the one before it, which costs a compare a hit and is predicted where hits lie mostly within half a block of one another. Both yield the same positions.
A word is read at a position, in the order its name says: b.u32_le(i)
and b.u64_le(i) read 4 or 8 bytes from i, least significant first, as a
u32 or a u64; b.u32_be(i) and b.u64_be(i) read them most significant
first. Each reads what b[i..<i + n] reads and stops the program where that
cut would. No read goes past the end: a loop that reads whole words up to its
last row runs over a copy of the last bytes with zeros behind them,
Bytes(tail) (ADR-320 D13).
Vectors are std::simd’s (ADR-377): simd::Lanes[T]
holds as many lanes as the build’s widest vector register (Lanes[T].count, a
constant of the build), simd::Fixed[T, N] holds N lanes on every target, N
a literal power of two from 2 to 64 (NK1297); T is u8, i8, u16, i16,
u32, i32, u64 or i64; i8, u16 and i16 are lane types only. Both are
made by simd::load(b, at), which reads what b[at..<at + count] reads and
stops the program where that cut would, or by simd::splat(x), each taking its
type from what receives it (NK1245 without one); v.count() is the lane
count. They offer lane by lane wrapping_add, wrapping_sub,
wrapping_mul, saturating_add, saturating_sub, min, max, & | ^ !,
<< and >> by a constant, simd::select(mask, a, b), the comparisons eq, ne,
lt, le, gt, ge, which answer a simd::Mask[T] (a Fixed[T, N]’s is simd::Mask[T, N]), and the reductions
wrapping_sum, min_lane and max_lane. + - * / % are not lane operations
(NK1296); a lane shift’s count is a literal below the lane’s width (NK1299). Only a Fixed offers lane(i), checked as an index,
simd::Fixed.from([…]), shuffle(of) by constant indices within its lanes
(NK1298), dot_pairs(w), which multiplies lane by lane and adds each
neighbouring pair into one lane of twice the width (u8 by i8 into i16,
saturating, i16 by i16 into i32, wrapping), widen and narrow_saturating. A mask is read as
positions: first() -> i64?, positions() -> Seq[i64] rising, count(),
any(), all(), and combined with & | !; each position is at least 0 and
below the lane count, and a proof may use it.
A '…' is a u8 or a scalar, by its use. One ASCII character is either;
'\xNN' above \x7F is only a u8; one scalar that is not ASCII is only a
scalar. A literal its uses do not type is a scalar. A let of one without an
annotation is typed by its uses, as a number’s is (2.4): let c = ';' then
b.find(c) is a u8, and uses that ask both are NK1200. A surrogate is
refused (NK1184).
Several scalars that normalise (NFC) to one are that one, with the warning
NK1213; anything else is refused with NK1212, which hands over the text
literal: "👍🏽".
A grapheme is the standard library’s. s.graphemes follows Unicode’s
segmentation rules in the version of the std the program is built with. A
grapheme is a view of text, compared with a text literal.
== compares bytes, and so does a hash. A precomposed and a decomposed ä
differ. s.normalized(text::Form::Nfc) - or Nfd, Nfkc, Nfkd - makes a
text that compares as its form says.
s.quoted() is the text as source writes it. It returns the text between double quotes, written exactly as Rust’s {:?} writes a string: \" and \\, \n, \t, \r and \0, \u{…} for the other control characters and for characters that do not print, and every other character, ä among them, as it is. Read as a literal (2.6), the result is the same text.
Control flow is the order in which statements and calls are executed.
Nikaia is an expression-oriented language: almost every construct has a
value. A block is a group of statements surrounded by curly braces
{ ... }. The last line of a block is the value of that block.
let result = {
let a = 5
let b = 10
a + b // Returns 15
}
A statement begins a line. Two statements on one line are a parse error: println!("hi") is not println and then !"hi", and let a = 1 2 is not let a = 1 and then 2. The first statement of a block may stand on the line of its { ({ return 0 }), and a jump’s trailing condition (return 0 if done) is part of its statement.
A block’s last line is the block’s value; return is the function’s.
A return written at the end of a block that is itself a value (a match arm,
3.4; an if branch whose value is taken, 3.2; a catch handler, 7.1) leaves
the enclosing function rather than handing that block a value. The one block
that is its own function is a lambda, whose return leaves the lambda (5.3).
The if expression tests a condition of type bool. Where the condition is
true it evaluates the first block; otherwise it evaluates the else block.
if is an expression, so its value may be bound to a variable.
let age = 18
let status = if age >= 18 {
"Adult"
} else {
"Minor"
}
After else, an if may stand where the block would stand: else if. A
chain is one if inside another with the inner braces left out, so every rule
of if holds at every link:
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else {
"C"
}
A condition is an ordinary expression. Every expression the language has
may stand there, with the same operators, the same precedence and the same
associativity as anywhere else. &&, ||, !, ??, as, null, a tuple and
a range are all conditions.
One rule concerns the brace. The { after the condition opens the
body, so an expression that starts with a brace is parenthesised:
if p == (P { x: 1 }) { // the parentheses say which `{` is which
…
}
if (match n { 1 => 10, else => 20 }) > 15 {
…
}
That is the whole of the difference between the head of an if, a while or a
for and any other position. Every expression is reachable.
A jump may carry its condition after it (ADR-276).
return, throw, break and continue take an if on their own line, and
the line means if cond { jump }:
fn limit(speed: i64) -> i64 {
return 250 if speed > 250
return speed
}
fn first_even(numbers: Vec[i64]) -> i64 {
for n in numbers {
continue if n % 2 != 0
return n
}
return 0 - 1
}
throw TooFast(speed) if speed > 250 reads the same way. Nothing but a jump is
guarded like this, and an if that begins a line is a statement of its own.
return if c { a } else { b } is still a return of an if expression.
A loop repeats code.
The while Loop
A while loop repeats its body as long as its condition is true.
let mut count = 0
while count < 5 {
println(f"Count is {count}")
count += 1
}
The for Loop
A for loop iterates over a sequence, such as a range of numbers or a list.
// Iterates from 0 to 4: `..<` stops before its end.
for i in 0..<5 {
println(f"Index: {i}")
}
a..b includes its end and a..<b excludes it. The spelling is the same
in a for, in a slice and in a pattern. a..=b is refused, naming the form
that replaces it. A range is an ordinary expression: it may be given a name,
passed, or indexed with. A range binds looser than every operator in it, so
0..<n - 1 is a range ending below n - 1, not a range with something
subtracted from it.
A range is a value, and walking it does not use it up. A range kept in a
name is walked as often as a program likes, from either end, and in steps:
after let days = 0..<n, both for d in days and
for d in days.step_by(7).rev() walk it, the second every seventh day counted
back from the last. A sequence something produces — keys(),
io::lines(), xs.iter().map(…) — is walked once, and a second walk is refused
(NK2702, Part III C.3). Whether one can be walked from its back end is known
to the compiler: a range, a list’s iter(), scalars and a map over any of
them can; io::lines() cannot, and rev() on it is refused. A range in a
list’s brackets reads a run of it, a view, whether the range is
written there or kept in a name.
A for over a list lends it: the elements are looked at, and the list is
still there when the loop is over. Changing the list inside the loop is refused with NK2305; what the loop wants to add is collected and added after it. A binding is a view of its element, so an
element kept past the turn, text in a field for instance, is kept as a copy,
.clone(); a number, a bool and a scalar are the element itself. A map is
walked as pairs, for (key, value) in scores. A for head takes the pattern a let takes, nested tuples and _ included, for (name, (line, col)) in places, and no variant (NK1259). Taking the elements away is written,
for x in xs.drain() (6.5). A ref written in front of the list is refused with
NK1137 (6.5).
A for binding changes only with mut, and mut means what it means on a
let (ADR-views-2). A change to a binding without it is
refused with NK1139. mut stands in front of the whole head,
for mut p in team, for mut (a, b) in pairs, as in front of a let’s, and
marks every name in it. In front of one name of a tuple, for (k, mut v) in m
or let (tx, mut rx) = …, it is not supported yet, NK1307: refused now,
possible later, and the help writes mut in front of the whole pattern. Over a
list or an array that the function may change, the binding is a view of the
element, and a change reaches the list:
struct P { pub n: i64 }
fn main() {
let mut v: Vec[P] = [P { n: 1 }, P { n: 2 }]
for mut p in v {
p.n = p.n + 1 // changes `v`
}
let mut counts = [1, 2, 3]
for mut n in counts { n = n * 2 } // `counts` is [2, 4, 6]
for mut i in 0..<3 {
i = i * i // a range's number is the turn's own
println(f"{i}")
}
}
Over a list the function may not change, mut is NK1139 at the list. Over
elements the loop owns - xs.drain(), a range - the binding is the turn’s own,
as let mut y = x is. A map’s key and a set’s element take no mut,
NK1306: the key decides where the entry stands, so it is taken out and
inserted again after the loop. A map’s values are not changed by a loop over
the map yet (that needs the one-name mut above); a value is changed by key,
m[k]. While the element is a view the body uses the
list neither to change nor to read it (NK2305); what it needs of the list is
read into a name before the loop. The view is given back whole at the end of
each turn (6.5).
Leaving a loop early: break and continue
break leaves the loop. continue skips the rest of this turn and starts the
next one. Both act on the innermost loop around them:
let mut first_even = 0 - 1
for n in numbers {
if n % 2 != 0 {
continue // not this one; take the next
}
first_even = n
break // found it; stop looking
}
println(f"{first_even}") // the loop is over, the program is not
A break leaves the loop and a return leaves the function. After the
break above, the println runs.
Three rules govern them:
Neither takes a value. A loop is a statement and hands back nothing.
break n is refused rather than read as a break followed by a statement
n:
error[NK1133]: Nothing after `break` in the same block can run.
There is no label. break acts on the loop it is written in, and there
is no way to name an outer one. A program that leaves two loops at once
leaves the inner one and tests outside it, or returns where the function
has nothing left to do.
A jump does not leave a function. A break whose loop is outside a
lambda, a task (an async form, 8.1.2) or a DSL fold’s step
is refused. Each of those is a function of its own, and a jump is a jump to a
place in the same function:
for x in xs {
let each = fn (n) {
break // error[NK1132]: the nearest loop is
} // outside this lambda
}
Such a lambda hands back a bool, and the loop tests it. A catch handler
is not a function of its own: a break in one leaves the loop around it:
for p in paths {
let text = fs::read_to_string(p, fs::Root::Anywhere) catch { break }
seen += text.bytes.len()
}
A loop can fail. Some sequences a for walks are read as it goes, such
as standard input’s lines. Getting the next element can fail. When it does, the
loop stops and the failure leaves the function, exactly as a failing call
would (Chapter 7). The function declares it:
use std::io
fn tally() -> i64 throws { // without `throws`: error[NK2701]
let mut n = 0
for line in io::lines() { n += 1 }
return n
}
Nothing marks the loop, as nothing marks a call that can fail. The same rule applies at the end of a block, where a resource’s cleanup can fail and the function that owns it declares it (6.4).
Getting the next element can also pause, and nothing marks that either. A
loop over standard input gives its thread up between lines. The library says
which sequences are like that; a program writes for.
Most loops cannot fail. A range, a list, a map: nothing is read.
There is no third loop form. A loop that does not end on its own is written
while true { … }, and it is left by a break:
let mut n = 0
while true {
n += 1
if n == 5 {
break
}
}
There is no loop keyword. break hands back nothing. A function whose last statement is a while true that no break leaves needs no return after it, whatever type it declares: it never reaches its end (ADR-276 D6).
match)The match expression compares a value against a series of patterns. Every
possible case is handled.
let value = 2
match value {
1 => println("One"),
2 => println("Two"),
else => println("Something else"), // `else` is the arm taken when nothing above matched
}
A pattern is one of seven things, and each is read the way it is written:
| pattern | matches |
|---|---|
else |
anything, and binds nothing: the arm taken when none above it matched. _ in this position is refused: it is the ignore pattern, which stands in a tuple position, as a parameter and as a part of a pattern, (0, _) |
1, "text", true, 'n' |
that value |
Op::Times |
that variant |
Message::Write(text) |
that variant, binding what it carries |
Message::Move { x, y } |
that variant, binding its fields by name: x is short for x: x |
other |
anything, and binds it to that name |
null |
over a T?, the value that is not set; every other arm of that match matches the T inside, so a bare name binds the T and 0 compares with it (ADR-352). In a tuple, a bare name at a T? position binds the T once the arms before it have taken null there: after (null, null) and (null, y), (x, _) binds x as the T (ADR-352 D1). Over a type that is never null it is refused with NK1259 |
The last two rows are one rule: a path with :: in it names a variant, and a
bare name binds. Quit is a variant of Message, never on its own.
An enum with exactly one variant is written by its type’s name
(ADR-347): Exit(c) is Exit::Code(c) and Pos { x, y } is
Pos::At { x, y }, as a pattern and as a value. A bare name still binds, so a
unit variant’s pattern is written long, or as else. The short form of an
enum with more than one variant is refused with NK1252.
Eight more shapes:
| pattern | matches |
|---|---|
(0, 0), (0, y) |
a tuple, position by position |
(0, y) \| (y, 0) |
either alternative; every alternative binds the same set of names, and alternatives that bind different names are refused with NK1155 |
200..299 |
a range, inclusive at both ends. An exclusive one is written by moving the end; ..< is never written in a pattern |
(x, y) if x == y |
a guard: the arm matches only where the condition holds, and the word is if |
Event::Click(Point { x, .. }) |
a pattern inside a pattern, and .. for the fields this one does not name |
Held::Precondition(..), Held::Precondition(c, ..) |
that variant, whatever values it carries, binding none (ADR-349); beside other parts, .. is the values they do not name, as in (a, ..). On a variant without values it is refused with NK1255 |
[], [x], [first, .., last], [h, ..rest] |
a Vec[T], an Array[T] or a view of either, element by element (ADR-355). .. is the elements not named and binds nothing; ..rest binds them as a view of the run. A pattern has at most one .. (NK1258), and a match with a list pattern needs else |
Point { x: 0, y }, Move { x: px, .. } |
a field against a pattern: x: 0 matches where the field is 0, x: px binds the field to the name px, and x: Op::Times or x: (0, y) reaches inside it. It names every field, or ends with ..; one that leaves a field out otherwise is refused with NK1234 |
.. means two different things: in a range pattern it is the range, and
in a struct pattern it is the rest of the fields, in a tuple’s, a variant’s
or a list’s brackets the rest of its values. The position says which.
A match reads what it binds. Over a name the function owns, an arm that
only reads a part it binds takes a view of it (6.5), and the name is whole
after the match: it may be matched again, printed, or handed on. An arm that
keeps the part — pushes it into a list, returns it, hands it back as the arm’s
value, changes it — takes it, and the name is gone afterwards, as it is after
any hand-over. A number, a bool and a scalar are copied either way.
Every case is covered. A match over an enum is complete when every
variant is named (an or-pattern names each variant in it), and needs no else. A match over anything else needs an
else, or a bare name, which catches everything; over a struct, a pattern of
that struct whose every field is a binding, _ or left to .., Point { x, .. },
catches everything too. bool is the exception: true and false are two arms and a complete
match. An incomplete match is refused with NK1151, which names what is
missing: the variants, or else. A guarded arm covers nothing, so a match
whose only catch-all carries an if is refused with NK1151. A match over a T? with a null arm is complete
when an unguarded null arm or else covers null, and the other arms cover
the T: null => … and n => … are a complete match. Some(…) and None are not patterns of a T? and are refused (NK2704 for Some(…), NK2705 for None), unless the subject’s type declares such a variant.
An arm’s body is an expression or a block. The expression may be a throw, a
return, a break, a continue or a panic(…). Their type is never, so
an arm that throws sits beside an arm that hands back a value, and the match
is that value’s type:
match step {
(Op::Times, n) => { value = value * n }
(Op::Divide, n) => { value = value / n }
}
A member of a nullable type is reached through an operator that handles the
null case.
?.): reaches a member only where the receiver is not
null. Where the receiver is null, the expression is null. A field
and a method are both members, and a method call takes its arguments
there as it does anywhere.??): supplies a fallback value where an expression is
null. A chain may be written: a ?? b ?? c takes the first that has a
value. The fallback is of the type the left side holds: a ?? b on an
a: T? is a T, and a b of another type is refused. Where the last
fallback may be null too, so may the answer: a ?? b
with a b: T? is a T?. The fallback is one value - a literal, a name, a
call, a field, a jump, or an expression in brackets - so a ?? 0 > 3 is
refused and (a ?? 0) > 3 is written. There is no postfix ??: a?? is
refused, naming a ?? b, a jump and a ?? panic("…")
(ADR-279 D1, D3).// If find_user returns null, 'name' becomes null.
let name = repo.find_user(id)?.full_name
// A method is a member, so it is reached the same way.
let greeting = repo.find_user(id)?.greet("Hallo")
// If 'name' is null, "Guest" is assigned.
let display_name = name ?? "Guest"
// And a fallback may have a fallback.
let shown = nickname ?? name ?? "Guest"
The result of a ?. is a T?. Where the member is itself a T?, the
result is flattened: a?.b?.c never reaches through a nullable of a nullable.
A read of a map whose values are T? is flattened the same way (4.5).
The compiler decides which case applies from the declared type.
?. or ?? after a type that cannot be absent is a warning. A type that
is not T? always has a value: s?.len() is s.len(), with the member’s type
and the warning NK1217; s ?? "b" is s, its fallback never runs, and the
warning is NK1216.
After a list’s index they are refused. A list’s index is a T (4.5), so
xs[i] ?? 0 over a Vec[i64] is NK1211 and users[i]?.name over a
Vec[User] is NK1121: one past the end stops the program, and neither
operator guards an index. Over a Vec[i64?] the index is an i64?, and ??
replaces a null element.
?. takes nothing. It reaches through a view of its receiver, so user is
usable on the line after user?.name. What comes out is a copy where the
member copies and a view of the receiver otherwise, as a field read is (6.6).
A receiver that is a call’s result is a temporary: a field reached through it
is taken out of it, and a method that hands back a view of it keeps it, to the
end of the block, in a binding the compiler writes on the line before. Where
that would change what runs (on the lazy side of ??, && or ||, in a
match arm, after another call in the same statement) or where the view is
the block’s value or the result, the line is refused, naming the let to
write (ADR-278 D22).
?? takes its left side where the answer is kept, and lends it where the
answer is only read (ADR-279 D5), as an argument is lent or
handed over (6.6). println(user ?? "Guest") leaves user usable on the next
line; let shown = user ?? "Guest" takes user, as let b = a takes a.
The positions that only read are an argument the function only reads, an
f"…" hole, a comparison of text, and the receiver of a method that only
reads it. A value that is a word by its kind - a number, a bool, a scalar, an
enum whose variants hold nothing - is copied at a read rather than lent; every
other type is lent, at every size (ADR-279 D8).
A left side that is itself only borrowed, beside a fallback that jumps, is
a view wherever it stands (ADR-279 D10): over a lent
c: Contract, let s = c.signature ?? return false binds a view of the field.
Nothing can take a part of a loan, and a return, throw, continue or
break has no value of its own, so the answer can only be the left side,
lent. Kept past the loan - returned, stored, pushed - the view is refused, and
.clone() is the copy.
?. reaches a method. find(1)?.greet("Hallo") calls the method only
where there is something to call it on; the arguments reach it, and the result
is a T? like any other reach. It flattens where the method’s own result is
already a T?. A method may pause and may fail, and a ?. on one
carries both.
A ?. guards its own member and no more. Where the receiver is absent, the
whole expression is null and what follows is never reached. A . written
after the reach is reaching into a T?, and is refused where it is written
with NK1125: T? is a type of its own (2.3), and a member of T is not a
member of it. So a?.b.c is refused and a?.b?.c is the program.
A struct groups related values under a single name.
Visibility and Encapsulation
Everything is private unless it says pub. That includes a struct and its
fields.
pub.pub.// file: users.nika
// The Struct is public, but fields are private
pub struct User {
username: String,
email: String,
is_active: bool,
}
A field may have a default, a build-time value written after its type (ADR-331 D5, ADR-318):
pub struct Page {
pub size: i64 = 50,
pub cursor: String?,
}
A struct literal may leave such a field out, and the field takes its default:
Page { cursor: null } has a size of 50. A literal that leaves out a field
without a default is refused with NK1234. The default is evaluated once,
while the package that declares the struct is built.
A struct states its rule with an assert after its fields
(ADR-369):
pub struct Piece {
start: i64,
end: i64,
assert(0 <= start && start <= end; message: "a piece ends after it starts")
}
The claim reads the fields by name, and it is the type’s invariant. It
holds after every construction - a literal, a with, the constructor’s
result - at the end of every method that changes the value, and after every
write to a field from outside such a method; inside a method it may be broken
for a while. Each of these points is proved, or where the compiler cannot,
refused or checked as the build says, as any assert is (Part III 14.2). Wherever a value of the
type is read, the rule is a fact the compiler proves from. Where a struct with
private fields writes no rule, the compiler may find one its package’s code
always keeps, and uses it only as a fact.
A struct literal Type { field: value } names fields, and a private field
cannot be named from another package. A type another package constructs
provides a public constructor.
The Anonymous Constructor (pub fn)
An impl block may declare one function with no name. That function is called
when the type name is invoked like a function: User(...).
// file: users.nika
impl User {
// The Constructor
// It accepts positional arguments (Subject Zone)
pub fn(username: String, email: String) -> User {
// We can access private fields here because we are inside the module
return User {
username: username,
email: email,
is_active: true, // Default logic handled internally
}
}
}
A struct literal is written with braces, and only with braces.
User { username: name, email: address } is the literal. User(a, b) is a
call, of the anonymous constructor or of anything else. Where the name is a
type, Type(field: value) is refused with NK1146, naming the braces.
A type is constructed by its anonymous constructor, in std as in a
.nika file: Vec(), String(), HashMap(), Stats(first). A written
Type::new is refused with NK1149, in a call and as a value alike; the
constructor handed over as a value is the same spelling,
par_fold(M, Summary, …). The ledger writes Type::new, the name the lowering
uses.
A copy with fields changed: with. with writes a copy of a value with
named fields changed, without naming the rest:
let moved = p with { x: p.x + 1 }
let stats = old with { count: old.count + 1, sum: old.sum + t }
The braces are the struct literal’s, with its field list and its shorthand.
with names top-level fields only: a field of a field is
p with { pos: p.pos with { x: 1 } }. The fields not named are moved from
p, never copied unseen. Where one of them is a text or a list and p is used
afterwards, the refusal names the copy to write. Across a package, with names
pub fields only, as a literal does.
A field the type does not have is NK1107 and a private one NK1110, both the
literal’s own refusals. A field named twice is NK1172, here and in a plain
literal. A with that names none is NK1174. NK1173 refuses the operand: an
enum, a view (there is nothing to move out of one), a type with no
fields, or a value whose type the compiler did not work out.
Usage Example
Another package reaches User through its constructor.
// file: main.nika
use users
fn main() {
// ERROR: Private Fields
// Direct struct initialization is forbidden because fields are private.
// let u = users::User { username: "A", email: "a@b.com", is_active: true }
// OK: Public Factory Constructor
// Calls the 'pub fn' defined in 'impl User'.
// Note: Uses positional arguments as per Function Syntax.
let u = users::User("Alice", "alice@example.com")
}
In Java, C# or Python a class bundles three things: the data a value holds, the methods that work on it, and a place in a hierarchy it inherits from. Nikaia has no classes. Each of the three has its own construct:
| A class gives | Nikaia writes |
|---|---|
| fields | a struct (4.1): what a value is |
| methods | an impl block: what a value does |
| a base class, shared behaviour | a trait (4.7): what several types can do, each in its own impl |
| a fixed family of subclasses | an enum (4.4): one of a fixed set of variants |
struct Position {
x: int,
y: int,
}
struct Player {
name: String,
at: Position, // a part, not a parent
health: int,
}
impl Player {
fn hit(mut self, damage: int) {
self.health = self.health - damage
}
}
What goes wrong with classes, and what Nikaia does instead:
Player holds a Position, and calls its methods by name.Reader
and a Closable has to fit one tree, or the language needs multiple
inheritance and its diamond. A Nikaia type implements any number of
traits, each in an impl of its own, and no trait knows about another.shape.area() through a base
reference reaches whatever subclass the value has, including ones written
later; neither a reader nor the compiler can say which body runs. In
Nikaia every call knows its body when the program is built. A closed set
of cases is an enum, and match must handle every variant (3.4); adding
a variant makes each match that misses it an error. An open set is a
type parameter bound by a trait (4.7), and each call is resolved for the
type it is given. So the prover reads the body or the contract of the
function a call reaches (Part III 14.2), and no call goes through a table.Shared[T] or
SharedMut[T], where the reader sees it. That is what lets the compiler
decide which data needs protecting when work runs in parallel.enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
}
fn area(s: Shape) -> f64 {
return match s {
Shape::Circle { r } => 3.14159 * r * r,
Shape::Rect { w, h } => w * h,
}
}
A trait method has no default body and a trait is not a type (4.7): a list
of values of different types is a list of an enum, and code shared by
several impls is a function they call.
An enum is a type whose value is one of several distinct variants.
enum Message {
Quit, // carries nothing
Move { x: i32, y: i32 }, // named fields, read by name
Write(String), // positional, read by position
}
An enum is the type for a value that is one of a fixed set of things: two
operators, four directions, the three states a connection can be in. An enum is
read back with match (3.4): an arm per variant, and no arm for a case that
cannot happen.
A type may hold itself. A syntax tree is an enum whose variants hold more of
it, and a list is a node whose next is another node or nothing:
enum Expr {
Num(i64),
Add(Expr, Expr),
}
struct Node {
value: i64,
next: Node?,
}
The program writes the tree as the tree. A value that held a whole value of its
own type inline would be larger than itself, so the compiler keeps such a field
somewhere else and the program never names where: it is built, read, matched
and assigned as the type it declares (ADR-246). What that costs is one
allocation per such field built. A pattern cannot yet look inside such a part
(Expr::Add(Expr::Num(n), b)); a name binds it, and a match in the arm asks
the question.
An enum with one variant is built by its type’s name (ADR-347): the variant adds nothing the type does not say.
enum Meters { Value(f64) }
enum Pos { At { x: i32, y: i32 } }
enum Done { Only }
let m = Meters(2.5) // Meters::Value(2.5)
let p = Pos { x: 1, y: 2 } // Pos::At { x: 1, y: 2 }
let d = Done // Done::Only
The long form stays valid. An enum with more than one variant takes the long
form (NK1252).
The standard library provides types for groups of values.
let numbers = [1, 2, 3, 4]
[v; n] is n copies of v (ADR-362):
[0; 181] is 181 zeros. v is worked out once and must be a value that
copies (8.3); one that does not, a String or a Vec, is refused with
NK1280, and (0..<n).map(fn(_i) { v.clone() }).collect() writes each
copy. For a list n is any whole number, and a negative one aborts as an
index out of range does. A list literal whose only element is a range,
[0..181], is a list of one range and warned (NK1281): the numbers are
(0..181).collect(); a written type, Vec[Range[i64]], says it is meant.
A list keeps the order it was given, and that order can be changed:
xs.sort() puts the elements in their natural order, and
xs.sort_by_key fn(x) { … } in the order of what the closure returns. Both
are stable: elements the key does not separate keep the order they had.
Two passes therefore express a compound order without a comparator:
names.sort() // by name
names.sort_by_key fn (name) { -(report[name]?.hits ?? 0) } // then by hits, descending
xs.is_sorted() says whether every element is at most the next in their
natural order, and it holds after xs.sort(). xs.binary_search(x) looks
for x by halving and requires xs.is_sorted() (Part III 14.2, a
property): a call where the order is not shown is refused (NK1300), so a
list whose order cannot be shown is checked once where it arrives
(return … if !xs.is_sorted()) or vouched for with
unsafe { assume(xs.is_sorted()) } (Part III 14.2).
A chain runs element by element (ADR-342). A list’s
map hands back a list, but where its result goes straight into the next
step - another map, iter(), a for, len() - no list is built between
them, as none is between the steps of xs.iter().map(…).filter(…). This
holds where every lambda of the chain touches nothing (Part III 13.5) and
changes no name outside it; a chain with a lambda that prints, reads or
takes a lock runs step after step, as written. In a chain that qualifies,
the order in which the lambdas reach the elements is not fixed. Where more
than one step would panic, which one stops the task is not fixed either,
and an overflow may stop the chain after its last element instead of at its
own; it stops only for a reason a step has on a value the chain computes as
written, and the message names the line that overflowed.
A map has no order, so a program that prints one says which.
let pair = ("*", 3) // (String, i64), the text a view
let op = pair.0
A tuple is the type for values that belong together for one step of a
computation: the element of a grammar rule that yields an operator and its
operand, or the two halves of a map entry in for (name, value) in map.
A group whose meaning outlives the step is a struct (4.1) with named
fields.
use std::collections
let mut scores = collections::HashMap()
scores["Player1"] = 100
Reading a map through the brackets gives a T?, because a key is data and
may be absent: scores["Player1"] ?? 0, or scores[name]?.rank ?? 0.
get gives the same. A list’s xs[i] stays a T, and a wrong index
aborts (Part III, Appendix A). A += on a map slot is refused with the
written-out form (NK1162).
A map made without an annotation takes its key and value types from the
first write that says them: after scores.insert("Player1", 100) it is a
map from String to one number without a type, decided by its uses as a
list’s numbers are (2.4, ADR-285 D43). A map nothing
writes into or types is refused with NK1153.
The value reached is a view of the map where it does not copy, and
m[k] ?? 0 on a map of numbers is the number. Kept where a T? of its
own is wanted - let k: Kind? = m[1], or x = m[k] ?? return into a
name that owns its value - a value that copies is copied out, and one
that does not is written m[k]?.clone().
A map of T? values reads one T?, as ?. does (2.3): a key that
is absent and a key that holds null both answer null, and
m.contains_key(k) tells them apart. m[k] = null stores null - the
key is there afterwards - and m.remove(k) removes it
(ADR-293). A map whose keys it
owns — a HashMap[String, V], a HashMap[i64, V] — takes a key
written into it as its own and is lent one it is read with; m[k] ?? "-"
over a map of text is a view of text.
A key is hashed from what it holds. A number, text, a bool, a
scalar, and a tuple, list or option of such is a key; a struct or an
enum the program declares is one where every field is, hashed from its
fields as == compares them. A float is not, nor a type with an
impl Eq of its own unless it also has an impl Hash (NK1260).
A map’s hash function follows where its keys came from. Keys derived from
data a remote peer supplied are hashed with a random per-run key. Keys from
data the program supplied are hashed with the fast function. Maps that one
value passes between are hashed the same way, decided for the whole
package, and a map in a pub signature or type is hashed as each use’s
map is. User code does not configure this; Part III, 17.1 names the cases
where a program decides it. The iteration order of a map is not guaranteed and may differ
between runs.
A list is written [1, 2, 3]. The elements are expressions, a trailing
comma is allowed, and the type is Vec[T] where T is what the elements agree
on. Elements that do not agree are refused with NK1154. Elements written only
in number literals are one number without a type, decided as a let’s is
(2.4): by the uses, and where none asks, i32, or i64 where an element does
not fit an i32. An item a walk of the list hands a lambda is that number too, and a use of it inside the lambda is a use of the list’s number: xs.iter().filter(fn(v) { byte(v) }) makes xs a Vec[u8]. [] is the empty
list and takes its element type from the first use that says one:
let xs: Vec[i64] = [], or a push. Where nothing ever says the type, [] is
refused with NK1153, asking for the type. A [ at the start of a line
begins a literal and never an index of the line above it, so an index is always
written where its subject is. The list type is written Vec[T]; there is no
second spelling [User].
A list has no operators, and neither has a map or a set: a + b on two
lists is refused with NK1191. One list’s elements join another with
a.extend(b), which grows a in place (ADR-253).
A [] whose only uses cannot give it an element type (xs.len() and nothing
else) is not refused by the compiler, and the backend reports it
(Part III C.4).
A fixed-size array is Array[T, N], in the bracket generic every other
parameterised type is written in; N is a number rather than a type, and it is
the one place a type argument is one. It is N elements inline: in a
struct it is part of the struct, as an argument it is passed as a value, and
nothing is allocated. It is indexed as a list is, an index out of range aborts
(Appendix A), and len() is the N it was declared with, known while the
program is built.
struct Vector3 { parts: Array[f64, 3] }
let origin: Array[f64, 3] = [0.0, 0.0, 0.0]
The literal is the list literal: it takes the array type where the use asks
for one, and a literal with no use to constrain it is a Vec. A use is an
annotated let, an argument, a declared result or a struct literal’s field,
and the answer descends with the literal, so Vec[Array[f64, 2]] and
Array[Array[i64, 2], 2] take their shape too. The length is part of the type,
so Array[f64, 3] and Array[f64, 4] are different types and a literal whose
length does not match N is refused with NK1157 naming both numbers.
[v; n] (4.5) is such a literal too: let buf: Array[u8, 4096] = [0; 4096],
where n is a number the build knows and equal to N.
An array is walked as a list is (ADR-collections-2):
for x in a lends it (3.3), and a.iter(), on an array and on a run of one,
is the sequence a list’s iter() is, so a.iter().zip(b.iter()), map,
filter and enumerate follow. Nothing is copied.
A generic declaration is written once for several types. A type parameter is
declared inside square brackets [...].
struct Box[T] {
item: T,
}
impl Box[T] {
fn get(self) -> T {
return self.item
}
}
fn hand[T](x: T) -> T {
return x
}
A type parameter is written, never inferred. Inside the body it is a type:
x is a T, and a T is not an i64, because the caller picks what T is.
At the call it is filled in from the arguments: hand(n) where n is an
i64 hands back an i64, and Box { item: n } is a Box[i64]. A parameter
list may hold the type itself, fn schema[T](T); the call then writes a type
there, schema(User), and the function needs no value of it (Part II 10.3).
A call may also name a function’s type arguments in brackets, parse[Config](text);
the types fill the type parameters in order. The call is warned with NK1233
(ADR-330 D9) where a value argument’s type could have said
the type, and not where the brackets are the only place that says it,
json::read[Contact](bytes) (ADR-grammar-2 D11).
A type may take values as well as types in its brackets (ADR-333), as
Array[f64, 3] takes its length. A value parameter is marked comptime and written with its
type; those after a ; are named at the use and have a default, as a function’s options do (5.1):
pub struct Decimal[comptime precision: i64, comptime scale: i64; comptime rounding: Rounding = Rounding::HalfEven] { … }
let price: Decimal[10, 2] = …
An argument is a build-time value (Part II 10.2), refused with the codes a comptime is refused
with where it is not one. Two uses name the same type when their arguments are equal. Inside the
declaration a value parameter is a constant: precision is an i64 known while the program is
built.
A function’s parameter may be comptime as well. The call writes its argument as any other,
and the argument must be a build-time value; inside, the parameter is a constant, and the function
is emitted once per value:
pub fn get[F: Fn](self, comptime path: String, handler: F) -> Server { … }
server.get("/orders/{id}") fn(id: i64) { … } // "/orders/{id}" is read while building
A caller that passes its own parameter on declares it comptime too.
A parameter marked comptime? may be a build-time value (ADR-specialization-1):
a call whose argument is one calls a copy of the function made for that value,
two calls with equal values share it, and any other call calls the function as
written. self may be marked. A value is a build-time value by rule: a
comptime binding, a literal, a comptime or comptime? parameter inside its
copy, a field, element or part of one, a call the build may run over them
(Part II 10.2); a let is not one. Inside the function, the parameter may not
stand where only a build-time value is allowed (NK1305), since the function
as written receives run-time values. A result type may be computed from
the function’s comptime parameters by a call the build may run:
fn compile(comptime pattern: String) -> Regex[engine_for(pattern)].
A comptime parameter in a function’s brackets is read off its arguments’ types, as T is:
fn reversed[comptime N: i64](xs: Array[i64, N]) -> Array[i64, N] { … }
fn dot[comptime N: i64](a: Array[f64, N], b: Array[f64, N]) -> f64 { … }
dot([1.0, 2.0], [3.0, 4.0]) // N is 2
dot([1.0, 2.0], [3.0, 4.0, 5.0]) // refused: N cannot be 2 and 3
The call writes nothing for it; inside it is a constant, and a result type may name it.
Array[T] as a keep parameter (2.2) is such a length that nobody names.
A T on its own has no members:
fn shout[T](x: T) -> String {
return x.to_uppercase() // error[NK1126]: `T` stands for a type the caller
} // picks, and nothing says it has a method
// `to_uppercase`
A bound, [T: Summarize], is 4.7’s subject.
A trait declares a set of methods that different types can share.
trait Summarize {
fn summary(self) -> String
}
impl Summarize for User {
fn summary(self) -> String {
return f"User: {self.username}"
}
}
A trait’s methods are signatures without a body. The declaration says what
a type must have, and the impl says what it does.
A type’s own methods are its declarer’s. An impl without a trait works
only on a struct or an enum the program declares. One on a built-in type,
a std type or any type written with a module in front is refused with
NK1209. To give such a type a method, declare a trait and implement it for
the type:
use std::time
trait Doubled { fn doubled(self) -> time::Duration }
impl Doubled for time::Duration { // not `impl time::Duration`
fn doubled(self) -> time::Duration { return self * 2 }
}
A type parameter may be bound by a trait. The bound is what gives a generic body something it may do (4.6):
trait Summarize {
fn summary(self) -> String
}
fn shout[T: Summarize](x: T) -> String {
return x.summary() // `Summarize` says there is one
}
The bound answers the whole call: how many arguments summary takes, what
they have to be, what it hands back, and whether it can fail or pause all come
from the declaration. Several bounds are written [T: Named + Aged]. A bound
may take a path, [H: http::Handler], and the ledger records a trait and each
impl where they were written. Whether a type implements a trait is the union
over every ledger the program reads plus its own. A call that passes a type with no impl of the bound’s trait is refused at the call, with NK1164.
A trait method reads like any signature. Without sync it may pause;
without throws it cannot fail. An implementation is checked against the
declaration: a body that pauses under a sync declaration is refused, and a
body that does less than the declaration allows is accepted. A trait can
therefore describe I/O, fn load(self) -> String throws, and a call
through its bound pauses where the declaration says it may.
An impl owes its trait the declared methods and no others: a method the trait
does not declare, or one it declares that the impl leaves out, is refused
with NK1130. A method whose implementation pauses where the declaration
says sync is refused with NK1129. A trait method has no default body,
and a trait is not a type: there is no dyn and no fn f(x: Summarize).
Eq and Ord are traits of the language, in scope everywhere (a program that declares a trait or an enum of the same name keeps its own): Eq has eq(self, other: Self) -> bool sync, Ord has compare(self, other: Self) -> Ordering sync, and Ordering is the enum Less | Equal | Greater; they are the standard library’s Eq, Ord and std::cmp::Ordering in the Rust a program lowers to, so a type with an impl Ord is an Ord type to Rust code. A bound [T: Eq] gives the body == and != on two T; [T: Ord] gives <, <=, >, >=, compare, and also == and !=, because an ordered type is an Eq one. An operator on a type parameter that no bound allows is refused with NK1248. A bound is answered by an impl, and where there is none by the language: the numbers except the floats, bool, scalar, String and Bytes are Eq and Ord, a float is neither, and a container is when what it holds is, and so is a view of a run: two views of u8 runs are equal where their bytes are. A struct or an enum the program declares is Eq where its parts are, and Ord only where the program writes impl Ord for it. impl Eq and impl Ord are written for a type the program declares, and impl Ord only for a type that is Eq; any other is NK1249. An impl Eq replaces the equality the type would have had, and a body of either that can pause is NK1129. The laws are the program’s: equality is reflexive, symmetric and transitive, the order is total, and compare is Equal exactly where eq is true. The compiler does not check them. Hash is a trait of the language too: hash(self, mut into: Hasher) sync, where the body hands the parts that make a value what it is to into.add(x), each of a type that hashes; it is Rust’s Hash. impl Hash is written for a type the program declares and that is Eq, any other is NK1249; it replaces the hash the type would have had, and a type with an impl Eq is a hash map’s key only through one. Its law is the program’s as well: two values eq calls equal feed add the same values.
A function signature separates the data a function operates on (its subject)
from its configuration options with a semicolon separator (;).
Zone 1: Subject (Positional) Parameters before the semicolon are the data the function operates on.
Zone 2: Configuration (Named Only) Parameters after the semicolon are options, flags, or modifiers.
// Definition
fn request(url: String; timeout: time::Duration = 30.seconds(), method: String = "GET") { ... }
// Valid calls
request("https://api.com") // both options defaulted
request("https://api.com"; timeout: 60.seconds()) // one of them named
request("https://api.com"; method: "POST", timeout: 5.seconds()) // in any order
// Invalid calls
// request("https://api.com", 60.seconds()) // a positional argument in the named zone
// request("https://api.com"; timout: 5.seconds()) // error[NK1109]: no option `timout`
The ; stands between the two zones, and where one zone is empty it is not
written. A function whose parameters are all options is declared
fn execute(target_age: i64 = 0) and called execute(target_age: 30).
execute(; target_age: 30) is refused. A mixed call keeps its ;, and the ;
is required there. A method call takes the same form. A driver’s deferred
parameters (Part II 10.5) write no ; either, because a receiver stands
outside the parentheses.
A call’s ( is on the line of what it calls (ADR-317),
as an index’s [ is: a ( at the start of a line begins what is written
there - a tuple, a parenthesised expression - and never the arguments of the
line above it.
Every configuration parameter has a default. A caller may leave an option
out, and the call then takes the default. A parameter that has to be passed
belongs before the ;. An option without a default is a parse error that says
so.
A default is a build-time value (ADR-318): a literal,
or anything a comptime may hold (Part II 10.2), such as
timeout: time::Duration = 30.seconds(). It is evaluated once, while the
package that declares the function is built, and every call that leaves the
option out receives that value, in the form a comptime crosses in. A default
that cannot be evaluated or cannot cross is refused with the codes a comptime
is.
Order is the declaration’s, not the call’s: method written first above is
still passed second. The ledger records an option’s name, type and default,
the default as its value (Part III, 13.5).
Optional Parentheses A function declared without parameters may omit the parentheses, matching the block lambda style.
fn init {
// No args, no parentheses required
}
A lambda is written fn { … }, and 5.3 describes it. There is no second,
shorter form for single-line bodies.
Naming the arguments, fn(user) { … } (5.3), is the same form spelled out: the
body is a block either way, and both spellings stand in all the same places.
The arguments are the ones the lambda names. A lambda that names none takes
none, so fn { … } is a lambda of no arguments. A body that uses a, b or
c without declaring it is refused with NK1117.
fn: expression is refused with a message that names the block form.
fn { ... })A block lambda holds any number of statements. Its arguments are the ones it names.
fn(name) { ... }, and fn(first, second) { ... } for more than one;
a parameter in brackets takes the one tuple it is handed apart,
fn((name, v)) { ... } (2.1, Taking a tuple apart). Two names are two
arguments, never the parts of one tuple (NK1101)fn { ... } is the zero-argument
form, which is what .or_insert_with fn { Stats(0) } takeslet complex = users.map fn(user) {
let bonus = calculate_bonus(user)
// Implicit return of the last line
user.score + bonus
}
let ids = users.map fn(user) { user.id }
There are no automatic argument names. A local inside a lambda may be
called anything. A body that uses a, b or c without declaring it names
something nothing declares:
error[NK1117]: `a` isn't declared anywhere.
Trailing Syntax A lambda that is the last argument may stand outside the parentheses. Where there are no other arguments, the parentheses are omitted with it:
let ids = users.map fn(user) { user.id }
let sum = numbers.reduce(0) fn(acc, n) { acc + n }
A block ends at its }, so a chain continues after it and means what it reads as:
Routes()
.get("/x") fn { first() }
.get("/y") fn { second() }
// The count comes from the list, so a local may be called anything
users.map fn(user) {
if user.is_guest() {
return "Guest"
}
return user.name
}
A trailing lambda may follow a method call with or without other arguments, a
plain call, or a path: users.map fn(user) { … },
numbers.reduce(0) fn(acc, n) { … }, access_all(a, b) fn(x, y) { … }
(Part II, 12.3), and the chain above.
A lambda directly after async is run, not bound. let x = async fn { … },
async fn { … } and the handle forms async(handle) fn { … },
async(deferred) fn { … } and async(detached) fn { … } run the lambda’s body
as a task (8.1.2); return in it ends the task with its value. A task whose
value is a function is written let f = async fn { fn { … } }.
A lambda carries no effect marker. A parameter list is followed by the body
and by nothing else; fn(info) sync { … } is not one construct (7.2).
Whether a lambda borrows or moves the variables it uses is inferred from the
context the lambda is used in. The rule is the same at both values of
user_parallelism.
@immediate)A function that runs the callback to completion before it returns is an immediate context.
map, filter, for_each, sort_by.let prefix = "User: "
let names = ["Alice", "Bob"]
// 'map' is @immediate. It executes completely within this stack frame.
// 'prefix' is implicitly borrowed.
let formatted = names.map fn (name) { prefix + name }
// 'prefix' is still valid here because it was only borrowed.
println(prefix)
@detached)A function that stores the callback, runs it later, or hands it to another thread or task is a detached context.
async(detached), defer, set_timeout, channel.on_receive.let prefix = "Log: "
// 'async(detached)' is @detached. The lambda might outlive the current function.
// 'prefix' is implicitly moved into the background task to ensure safety.
async(detached) fn { println(prefix + "System started") }
// Compiler Error: 'prefix' has been moved!
// println(prefix)
The context belongs to the function that takes the lambda, not to the call.
map is immediate for every caller and async(detached) is detached wherever it is written,
so the capture is decided where the lambda is written.
A function in user code declares a code parameter with a function type. The
type is spelled the way a signature is, handler: fn(Request) -> Response,
with sync or throws after the result where a declaration puts them. A
parameter the code changes carries mut, and one it keeps keep, as in a
signature: f: fn(mut S)
lends the argument at each call as a mut parameter of a named function is
lent, and both fn bump(mut s: S) and the lambda fn(mut s) { … } fit it. Without
sync the code may pause; without throws it cannot fail. A lambda that does
less fits a type that allows more. A pausing lambda handed to a fn() sync is
refused.
The context of such a parameter is inferred, not written. A parameter the
body only calls is immediate and borrows. A parameter the body keeps (stores,
hands back, gives to a task) says keep, as every kept parameter does (6.5),
and is detached and moves; there is no @detached to write. For an immediate
parameter the lambda’s touches count for the callee, while
whether the call may pause, and the shape it is lowered to, follow the
parameter’s type. std’s own lambda-taking entries, such as map, are written
by hand and follow the lambda in everything. For a kept parameter the
callee’s promises about pausing follow the type, so a listen that calls a stored
fn(Request) -> Response may pause. The capture at an async(detached) is reported with
NK2101 (8.3).
A function type stands wherever a type does: a struct field, a result, a
let, an element of a list. A function value in any of them is kept: the
lambda moves what it captures, and a copy of the value shares the one closure.
A field that holds one is called like a method, button.on_click(4), where the
struct has no method of that name. A named function stands where a function
value is wanted. A kept value handed to a parameter that only runs it is lent.
Two values of a type that holds a function are not compared (NK1188).
struct Button { label: String, on_click: fn(i64) -> i64 sync }
fn adder(n: i64) -> fn(i64) -> i64 sync {
return fn(x) { x + n }
}
fn main() {
let b = Button { label: "ok", on_click: fn(x) { x * 2 } }
let add = adder(3)
println(f"{b.on_click(4)} {add(1)}")
}
A lambda handed to a parameter that runs it at most once may give away what
it captures (ADR-lambdas-1). Inside any other lambda,
handing a captured value to something that keeps it is refused (NK2105,
6.5), because a second run would find it gone. A lambda that runs at most once
has no second run: it may put a captured value into a field, a container or a
kept argument, and after the call the value is gone, as a kept argument is,
whether or not the lambda ran.
let t: Vec[i64] = [1]
m.entry("k").or_insert_with fn { H { a: "x", t: t } } // `t` moves into the new `H`
// println(t.len()) // NK2105: `t` was handed over in the lambda above
In std these parameters are Entry::or_insert_with’s and
Entry::and_modify’s lambda and the block of
Locked::access and SharedMut::access; the contract says so with once
(Part III). update is not one: its block may run again (Part II 12.2). For a
code parameter of user code the answer is inferred, as the context is (C):
it is once when the body calls it at most once on every path and hands it on
only to a parameter that is once. A call in a loop, a second call, or keeping
it makes it many. Nothing is written in the source. Where NK2105 refuses a
hand-over inside a lambda, it names the parameter that may run it again, or
the second call in the callee that made it many.
main’s Parameters Are the Command LineA program starts at main. main may take parameters; they are the words the
program is started with (ADR-360).
/// Multiplies a positive number.
fn main(a: int; times: int = 4, verbose: bool = false) {
assert(a > 0)
let b = times * a
if verbose {
println(f"{times} * {a} = {b}")
} else {
println(f"{b}")
}
}
prog 5, prog 5 --times 3, prog --verbose --times=3 5.
T?: such a subject may be left out at the end and is then null.
Options
are named: --name value or --name=value, the default where one is left
out, an _ in the name written - (max_depth is --max-depth). An option
of type bool is a bare --name, which sets it to true; --name=false
sets it to false. Named words may stand anywhere among positional ones;
-- ends them, and every word after it is positional. A word that is -
followed by a digit is a value, never a name.main is int, a fixed-width integer, f64, bool,
String, or one of these behind ?; the last subject parameter may be a
Vec[T] of one of them, which takes every positional word left, none
included. A number is read in the shape std reads one from text (Part III
17.1) and must fit its type. A subject of type T? stands after every
required subject and before a last Vec[T]. Another type, a required subject after a T? one, and a parameter of main on
a target with no command line (wasm32-unknown) are refused (NK1273); so
is an option named help (NK1274).main’s preconditions are derived as every function’s are (Part III
14.2): a claim carried back to main’s entry that reads only its parameters,
subjects or options, at least one of them; one that reads none the prover
decides.
They are checked after the words are read and before the body runs, and in
the body each is a fact.--help and -h print the program’s name, the /// lines above
main, and each parameter with its type, default and preconditions, and end
the program with status 0.main inside the program is an ordinary call, and
cli::args() still returns every word the program was started with.@inline asks that a function’s body be copied to where it is called
(ADR-370). It stands before a function or a method:
@inline is a hint, and lets another package inline a pub fn;
@inline(always) asks it of every call, and @inline(never) of none, for a
cold path; it stands once, and not before a trait’s method, which has no body. None of them changes what a program means. A function that calls
itself is never inlined into itself. Where the compiler sees a hot call the
backend would leave out of line - what a fold’s or par_fold’s step calls
(Part II 10.7) - it adds the hint itself; a written @inline(never) wins.
@inline
fn digit(b: Bytes, i: i64) -> i64 {
return (b[i] as i64) - 48
}
Memory is managed by ownership and borrowing, which the compiler enforces. There is no garbage collector, and user code frees no memory.
When a variable goes out of scope, at the end of the block {} where it
was created, its memory is released. User code frees no memory.
Three shared types carry a value that has more than one owner.
Shared[T] is written in the source; it is not inferred. The compiler
decides which owner count each value gets: one a second thread may safely
touch where the value may reach one, and a cheaper one where it may not.
Whether a Shared may be handed to a task (Part II, 11.2) is decided from the
type and from where it is going, at both values of user_parallelism. The
count follows that answer.
Where the compiler proves that a value never leaves the thread that made it, it uses the cheaper count. This never changes meaning. Where nothing proves it, the value gets the safe count, and there is no way to ask for the other one.
At user_parallelism = no every count is the cheap one. User code runs on
one thread, the runtime’s own threads run no user code, and a Shared may not
be handed to code nothing written down describes. What a program passes out of
itself is what is inside the Shared, a view or a copy; a foreign library
that keeps a value puts it in a hull of its own.
nikaia lower x.nika --sharing prints which count each value got, why, and
what would have changed it. It changes no decision.
Shared[T]: a value several parts of the program own at once and nobody
changes. The memory is released when the last owner is finished.SharedMut[T]: a value several parts own at once and any of them may
change. The lock that keeps the changes apart is part of the type; there is
no second wrapper around it. The value is changed through the four doors of
6.3.Locked[T]: an individually locked field inside a shared structure, one
lock per field rather than one lock around the whole. It is the same lock as
the one inside SharedMut[T], opened by the same four doors.The second owner arises without a written step. Where a handle on a
Shared[T] or a SharedMut[T] is handed on by value, passed to a function
that keeps it or used by a task (Part II, 11.2), the handle is duplicated.
Each handle is cleaned up at the end of its own block (6.1). No method is
called. A duplicated handle copies no data: one value, one more owner. The rule
is the same for both shared types; a SharedMut[T] may go into a task of the program’s own, and not into code nothing written down describes
(Part II, 11.2).
Lending the inner value out duplicates nothing. A lent parameter handed a shared value is a view of the value inside it, so a function that only uses the value takes an ordinary view and never mentions sharing:
fn serve(db: Connection) { … }
let db = Shared(postgres::connect("…"))
serve(db) // a view; no handle is made, and the count is untouched
A signature names the shared type only where the function keeps the value
past the call, keep db: Shared[Connection]: puts it in a structure, gives it to a task, hangs it on
something that outlives the call.
A SharedMut[T] is opened, and the opened value is an ordinary view. There
is no view straight through a lock. The caller opens the lock with one of the
four doors of 6.3 and passes the borrowed value in. The called function sees a
plain value and obeys the rule for an open lock: it may not pause, and it may
not touch a lock of its own (Part II, 12.2).
let db = SharedMut(postgres::connect("…"))
db.access fn(open) { serve(open) } // `serve` must be `sync` and lock-free
The first handle is written in the source. Each of the three shared types makes one by being called with the value that goes in it:
let db = Shared(postgres::connect("…"))
let counter = SharedMut(0)
let frei = Locked(0) // a field's lock, one per field
One rule decides every hull in the language:
A hull you cannot see, the compiler writes. A hull you can see, you write.
The compiler puts a plain value into a T? (2.3). A shared type is written
where the value is put into it.
A constructor stands wherever an expression may:
keep(Shared(connect(url))) // an argument
let p = Pool { db: Shared(connect(url)) } // a field of a literal
fn shared(url: String) -> Shared[Connection] {
return Shared(connect(url)) // a result
}
A plain value where a shared one is wanted is refused with NK1115:
error[NK1115]: `serve` takes a shared value, but `db` isn't one.
--> main.nika:11:5
|
11 | serve(db)
| ^^^^^^^^^
|
= note: It's declared as `serve(db: Shared[Db])`.
= help: Wrap it: `Shared(db)`.
A shared value may be returned, and a number may be shared: SharedMut(0) is a
call, and the call gives the number its type as any other argument does.
Only the handle is duplicated. Ordinary data (a string, a number, a struct
of those) is moved where it is handed on to something that keeps it (6.5,
8.3). For data, the .clone() is written in the source.
Each handle lives to the end of its own block, whether or not it is used
again. The duplication is not conditional on a later use, so the value is
cleaned up where the caller’s handle ends, which may be later than the task
that holds the other one. A program that wants the cleanup earlier ends the
block earlier (6.1). --sharing names each duplication site beside the count
it printed for that value.
SharedMut[T] is one name and two hulls. Which hull a value gets is
decided per value, as the owner count is. A message, a printed type and a
ledger entry say the name the program wrote. Shared[Locked[T]] is
refused with NK1123, and the message names SharedMut[T].
A value behind a lock is not changed by assignment. The lock is opened first, and the shape of the change decides which door opens it:
let kasse: SharedMut[i32] = ...
let stand = kasse.get() // take a copy out
kasse.set(hole_neuen_stand()) // replace it; the new value comes from outside
kasse.update fn(mut v) { v += 100 } // change it, under the lock
// Where the value is large, `update` changes it where it lies, and `access` reads it there.
let protokoll: SharedMut[Log] = ...
protokoll.update fn(mut log) { log.add("gebucht") }
let n = protokoll.access fn(log) { log.len() }
get and set take no block. get copies the value out. set’s
argument is computed before the call, including any wait for I/O, and the
lock is open for one store.update is handed the value as mut v and changes it. It returns
nothing (4.3). Whether v is a copy of a small value or the address of a
large one is the compiler’s decision. An update block may be run more than
once, so it may not do I/O or take another lock.access hands the block the value where it lies, to read. The block may
not change it. It is for a value too expensive to copy.update and access run user code while the lock is open, so that code runs
straight through: no I/O, and no second lock. The compiler checks both
(Part II, 12.2 and 12.3).
A value a crashed task left is never read (ADR-327). A task
that panics while it holds a lock poisons the value. An update or
update_all whose changed values all copy - numbers, bools, scalars - works on
a copy and puts it in place when its block ends, so a panic in it leaves the
value as it was and poisons nothing; an update_all puts both copies back
together, and where one of its two values does not copy, neither is copied and
a panic poisons both. access and access_all cannot change a value, and a
panic in them poisons nothing. A change an earlier door made stays: values that
must agree are changed in one update_all. A door to a poisoned value panics in the
task that opens it. A value made with
SharedMut::supervised(fn { … }) is rebuilt instead: the runtime calls the
function again, puts its result in place, and the holders keep their handle; how
often is bounded as a supervisor’s restarts are (Part II 12.8).
A value taken out of a lock is stamped. kasse.get() is a Seen[i64],
and so is what access computes. A Seen reads like the value it carries: a
program prints it, compares it, sends it in a response, and hands it to any
function that touches no lock. The stamp goes with it through arithmetic,
calls, struct fields declared Seen[…], and time. A stamped value may not go
back into a lock blind: kasse.set(stand + 100) is refused wherever
stand was read, and so is if stand > 100 { kasse.set(0) }. The doors for
what was seen are update, which decides inside the lock, and
set(neu; after: stand), which stores only if the lock still holds what was
seen and throws Overtaken otherwise. There is no word that removes the stamp.
Three mistakes are refused by name. Assigning to a SharedMut directly,
kasse = 0, is refused, and the message names set. A set given a stamped
value, or standing under a stamped condition, is refused, and the message names
update and after:. An update block that assigns to v without reading it
is refused as a set.
Resources are cleaned up deterministically, following the RAII principle (Resource Acquisition Is Initialization). There is no garbage collector.
Automatic Destruction
When a variable goes out of scope, at the closing brace }, its memory is
released.
Custom Cleanup (impl Drop)
A struct that manages an external resource (a file handle, a socket, a C
pointer) may implement the Drop trait. The drop method is called when the
value is destroyed. drop is synchronous: it runs straight through and
never pauses. It is for teardown that is pure memory work or a cheap native
call.
struct FileHandle {
fd: i32
}
impl Drop for FileHandle {
fn drop(mut self) {
println("Closing file descriptor...")
// Native close call would go here
}
}
Cleanup That Needs I/O (impl Cleanup)
A resource whose teardown needs I/O (a buffered file flushing, a transaction
rolling back, a TLS connection closing) implements Cleanup. I/O may pause
(Chapter 8), and drop cannot pause.
use std::fs
struct BufferedFile {
path: String,
held: String,
}
impl Cleanup for BufferedFile {
// Pausable teardown. May pause, may fail.
// Runs where the value dies - on normal exit AND while an error is
// bubbling up.
fn cleanup(mut self) throws {
fs::write(self.path.clone(), fs::Root::Anywhere, self.held.clone(); append: true)
}
}
impl Cleanup has the one method, fn cleanup(mut self), with throws
where it can fail. A type that also needs a synchronous last resort writes it
as its own impl Drop: drop runs after cleanup, or alone where cleanup
cannot run (below). Anything else written in impl Cleanup, a drop
included, is refused with NK2601, and the message points at impl Drop.
User code never calls cleanup. The value dies where ownership says it
does: at the closing brace of the block that owns it, or in the function it
was handed to when that function’s parameter is keep (6.7). The compiler runs cleanup there:
the end of that block is a place where the function may pause and where a
failure surfaces. Values that die together are cleaned up in the order they
die, the last one bound first. What holds such a value (a struct field, a list
element) has its cleanup too, because the value dies with it.
std has one of its own: fs::create(path, root) hands back a buffered
fs::Writer, whose write holds the text and whose cleanup writes what it
holds.
use std::fs
fn report(lines: Vec[String]) throws {
let mut out = fs::create("report.txt", fs::Root::Anywhere)
for line in lines {
out.write(f"{line}\n")
}
} // `out` dies here, and what it holds is written
A cleanup error is an error. The cleanup is a call the compiler writes, and
it is answered like one. If cleanup declares throws, the function where the
value dies can fail, and one that does not declare throws is refused with
NK2605, the code of every call that can fail there. The message names the
resource:
error[NK2605]: This function can fail, because cleaning up `f` can fail.
--> report.nika:2:5
|
2 | let f = fs::create("report.txt", fs::Root::Anywhere)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `f` is a `fs::Writer`. Its cleanup runs at the end of this block and can fail, so the function can fail with it.
= help: Declare the function with `throws`, or close it yourself where you want to handle the failure: `f.close() catch { … }`.
Two refinements:
close().
close() consumes the resource, runs its cleanup there and hands the error
back at the call, and no implicit cleanup runs afterwards.Where nothing may pause, nothing with a pausable cleanup may die. The
cleanup is a pause, and the rules for pauses apply to it as written: a value
that would die inside a door’s block is refused with NK2202 (Part II 12.2),
inside a par_iter lambda with NK2209, and inside a lambda handed to a
sync function type with NK2206. The message names the cleanup of the
value; the way out is binding it outside, where it dies after the lock is
released, or closing it there.
When cleanup cannot run. When a task is cancelled (it lost a select
race, or a supervisor restarts it), what died in it and was not yet cleaned up
is adopted by the runtime, which finishes it in the background before the
program exits (“parked cleanup”). The runtime configuration cleanup-deadline
bounds them (Part III 13.3b). A cleanup the deadline cut off is a failure of
the program: exit status 70, with the resource named on the panic path. A
panic runs no pausable cleanup: during panic teardown only the synchronous
drop runs, and on a target that traps rather than unwinds, a panic ends the
process immediately, so no destructors run at all (Part III, Appendix A). The
panic hook (7.2) runs on every panic. A panic is for an unrecoverable bug;
a recoverable failure uses throws, where full cleanup is guaranteed.
There is no defer keyword. Cleanup happens at the end of the block through
Drop. A lock’s .access() releases the lock; there is no manual
lock/defer unlock pair. A throws unwinds the stack and runs drop for
every variable in scope.
A function that only looks at a value, and does not own it, takes a view of it. The owner keeps the value, the borrower may read it, and the loan ends on its own.
A view is never written; the compiler infers it (ADR-views-3
D4, D5). A parameter, a receiver, a field, an element, a local binding and a
result are declared with their type (String, Vec[Row], Reading), and
whether each is a view or a value of its own is read off the program. ref is
written only inside extern "C" (Part III 15.1); a ref anywhere else is
refused, with a help that removes it: at a call or in an expression with
NK1137, ref mut with NK1158, in a declaration with NK1333.
Nikaia source contains no lifetime annotations. There is no syntax for them. Everything described below happens inside the compiler.
Nikaia source code contains no lifetime annotations. Ever.
Two things are guaranteed:
Borrowing inside a function, across I/O. A function may pause at any I/O call (Chapter 8), and a borrowed value stays valid across the pause:
use std::fs
fn report(config: Config) throws {
let name = config.name // a view of `config.name`
let data = fs::read("log", fs::Root::Anywhere) // pauses here (I/O)...
println(f"{name}: {data}") // ...and the view is still valid.
}
Returning a view from a function. fn first_word(s: String) -> String
needs no annotation. Where a function takes several, the compiler reads
off its body which one the result comes from, a caller reads it off the
ledger, and the function may write it, -> String from s (6.6, 6.7).
A borrow may not outlive its owner (6.6).
The caller writes nothing; the callee’s declaration says what an argument
becomes (D1). serve(db), fs::map(path, root), into.merge(stats):
whether an argument is lent, kept or changed is read off the callee’s
parameter, and the compiler writes the reference, as it writes the pause and
the failure a call carries (7.1, 8.1).
A plain parameter is lent; a parameter that keeps its argument says keep
(D2): fn add(mut self, keep item: Item) { self.items.push(item) }. Keeps means: stores it into something that outlives the call (a field of
the subject, a struct or collection handed back), gives it to a task, or
passes it to a callee whose own parameter keeps it. Moving it into a struct, a
variant’s value, a tuple or a list the body builds is not keeping it, unless
that value is kept. Handing back a part of it is keeping it: a field
whose type moves, taken out of the parameter and returned, keeps the
parameter; a field that copies leaves it lent. A whole parameter handed
back is a view the result points into (6.6). A plain parameter is lent: the
caller’s value is usable again after the call. The compiler infers keeping
over the call graph and fails closed, so a use it cannot follow counts as
keeping. It checks the signature against that: a body that keeps a parameter
not marked keep is refused with NK2302, with a help that adds the word,
and a keep on a parameter the body never keeps is a warning, NK1334. The
ledger records each parameter’s keep (6.7).
The receiver follows the same rule: self is lent, mut self is changed
in place, keep self consumes the object.
A trait method’s signature fixes keep for every implementation (D2): an
impl that keeps a parameter its trait method does not mark keep is refused
with NK1264.
A parameter the function changes in place says mut in the declaration,
fn fill(mut out: Vec[i64]) (D3); the call, fill(xs), shows nothing, as
xs.push(1) shows nothing. A parameter the body changes without mut is
refused with NK1138. A mut parameter handed to another mut parameter is
passed straight on. A mut parameter given away whole is NK1131: it stays
the caller’s, so the way out is .clone() or taking it keep. A callee that
wants a changeable copy writes let mut v = x inside.
A part taken out of a mut parameter is given back before the function is
left, and one taken out of a let mut view of a place before the view’s block
is left (ADR-views-1). A match arm that changes a part it binds takes it (3.4), and so does
handing a field to something that keeps it; assigning the part, or the whole,
gives it back. On every way out of the function - its end, a return, a
throw - the parameter is whole again, and between the taking and the giving
back stands no throw and no call that can fail. Both are refused with
NK2108. In match e { E::Add(a, b) => { … e = E::Add(l, r) } … } the arm
takes a and b out of e, and the assignment makes it whole again.
mut self is such a parameter too. A let mut over a place of something
the function may change, let mut c = m[k] ?? continue, is a view through
which a change reaches the place, and the same rule holds for it: on every way
out of its block - its end, a continue, a break, a return, a throw - it
is whole again. let kept = pairs(c.ensures, c.from) takes both lists out with
no copy, and c.ensures = [], c.from = [] give them back. Handing the whole
view to something that keeps it is NK1131, as for a mut parameter.
A for lends its list, so the list is still there after the loop;
iteration that takes the elements away is written for x in xs.drain(). A
for mut over a list or an array the function may change is a
let mut view of each element in turn (ADR-views-2),
lowered as iter_mut(), and the rule above holds for each turn: a part taken
out of p in for mut p in v is given back before the turn ends, and handing
p whole to something that keeps it is NK1131. A
let that annotates a for binding with the element’s type, let copy: Row = r,
is refused with NK1183: the binding is a view, so the annotation comes off,
or .clone() makes the copy. A
let over a place (config.name, totals.stations[name]) is a view of it
where the value would otherwise have to move, and takes the part where
the name is kept (handed to a keeper, returned, or assigned into a place) and
the place is a part of a mut parameter, of mut self or of a let mut
view (ADR-views-1 D6): let outer = walk.at;
walk.at = P {..}; visit(walk); walk.at = outer copies nothing. In a loop,
such a part is given back before the turn ends, and a refusal that comes from
the taking names the line that keeps the name.
Fields, elements and local bindings are views where the code makes them
so (D5). let s = totals[name] and let n = config.name over a place are
views, and so is an arm of their value; taking the value out is written
(remove, drain, .clone()). let y = x over a whole variable is a
rename, and a let whose initialiser is a call or a literal owns what it is
given. A field or an element holds a view, tethered to its buffer (6.6), where
what flows into it is a piece of a buffer, and a value of its own otherwise. A
field or an element the program changes in place, or hands to a keep
parameter, holds a value of its own; where a view would flow into it, the
refusal names where to write .clone():
struct Expr { n: i64 }
fn pick(given: Vec[Expr], tail: Expr, c: bool) -> i64 {
let first = given[0] // a view of `given[0]`
let either = if c { tail } else { given[0] } // a view of either
return first.n + either.n
}
A value that is a word by its kind - a number, a bool, a scalar, an enum
whose variants hold nothing - and a tuple whose parts all are, is copied at a
read, so it is never a view. In an extern "C" declaration ref i32 and
ref mut i32 are the pointers C expects (Part III 15.1).
Nothing here inserts a copy. A value handed to a keep parameter, and used
again afterwards, is refused with .clone() named as the way out (8.3).
Four kinds of argument are not lent and are passed owned: one handed to a
keep parameter, a value that copies, an argument that is already a
view, and a method’s argument.
A view handed to a lent parameter is passed as it is: in
for p in team { greet(p) } the element p is a view of a Person, and a
greet(p: Person) is lent that view, with no copy. A keep parameter needs a
value of its own, so a view handed to it is refused (NK1102), with
.clone() as the way out.
A value used after it was handed over is refused with NK2105: an argument
handed to a keep parameter, a key or a value written into a container, a
field, an element, a let that renames it, an assignment, and, inside a loop
or a lambda, the next turn’s hand-over of what is already gone; a lambda that
runs at most once has no next turn (5.4 D). A second use in the same
statement is refused too. A part is tracked on its own: after
xs.push(p.name), p.x is still there and p.name and p are not. A part of
something only lent is refused where it is handed over, with NK2106. A
parameter whose part is handed over is kept, so it says keep and its caller
hands it over whole.
A borrowed value escapes when it is returned past the scope that owns the
buffer, captured by a @detached lambda, or put into a collection that lives
longer than the buffer. An escaping view is tethered: the buffer stays
alive as long as the view does.
Transient = borrow. Escaping = tether. Copying = yours to ask for.
Every view (of text, of a run) is in one of three states. The compiler picks the cheapest one that works, and the states are never written in the source:
| State | What it is | Cost |
|---|---|---|
| Borrowed | a plain reference into the buffer | nothing at all |
| Tethered | a plain reference; the buffer has moved into an anchor | nothing where a frame keeps it; one count per task, or per buffer where a container drops entries — no copy |
| Owned | a String of its own |
one allocation, only where the program wrote .clone() |
The buffer cannot die while anything still points into it. It is kept alive deterministically, by the frame that outlives it or, where none does, by a reference count, with no garbage collector.
Two rules keep this cheap on large data:
Nothing is written for a tether. Where the buffer lives is the compiler’s
decision, like which count a Shared gets:
struct Token {
text: String, // a view into someone else's buffer
}
fn tokenize(path: String) -> Vec[Token] throws {
let source = fs::read_to_string(path, fs::Root::Anywhere)
// The returned tokens outlive this function, so `source` lives on in the
// caller: no lifetimes, no copies of the text, no dangling references.
...
}
A buffer lives in the anchor of whatever holds its views: the anchor is the place a buffer moves to while a view of it lives, and who owns it is read off the program:
mut parameter, or kept by a list outside a loop. This costs
nothing, works across any number of calls, and the body may pause;What a body owns is handed out the same way, whole or a part: a view of a
local handed back, a piece of a text the body read, puts that local in the
caller’s anchor, and so does a struct, a variant’s value, a tuple or a list
handed back whose views point into s. A view handed back from a parameter points into the
caller’s argument, which lives as long as the result does; an argument made
in the call itself, let r = e(name.clone()), lives in the caller’s anchor. A
view of a parameter kept in a place another parameter holds, such as a part of
s pushed into mut v: Vec[String], points into the caller’s argument the same
way, and that argument lives as long as the place’s owner does.
A struct built and consumed inside the scope that owns its buffer is borrowed, and costs nothing:
struct Reading { name: String, temp: i32 } // `name` a view where views flow in
--tethers (on nikaia build and nikaia lower) prints where each buffer lives and why, and changes nothing;
a change is a ledger diff in review.
There is no struct with lifetime parameters in Nikaia.
An escape whose buffer cannot be shared is refused. Where a slice escapes
and its buffer lives on the stack or came from a foreign library, no tether is
possible. The compiler refuses the program and names the ways out, .clone()
among them. The compiler never inserts that copy. One buffer handed both to a
task and out of the function is refused with NK2304, and so is a container of
structs holding views that drops entries inside a loop where the container is
not a list, or a struct’s views are not text.
A lent parameter is not kept past its call: storing it into a task or a
field is keeping it, and the parameter says keep (6.5, NK2302). A view
inside a struct carries the buffer it points into, so a function that keeps a
view, and not a copy of its text, takes the struct that holds it keep:
struct Reading { name: String, temp: i32 }
impl Summary {
fn record(mut self, keep m: Reading) { … } // and not `keep name: String`
}
Handing a view back out of the buffer it came from is not this rule:
fn count(seq: String, k: i64) -> HashMap[String, Tally] returns views
of seq, and the result points into seq and nothing else. The same holds for
a struct literal handed back: fn make(name: String) -> Reading whose body
only moves name into the Reading returns a Reading that points into
name.
A result points where the body says, view by view. Every way out of the
body - a return, the last expression - is followed back to the parameters
whose views reach what it hands back: the receiver and every lent parameter
(6.5). A literal, a buffer the body made and
a copy reach none. Each view the result holds - the result itself, or each view
in a field, an element or a variant’s value - points into the parameters that
reach it, and only those. Where the body chooses at run time, by two ways out
or one value given either, the view points into each of them. Where no
parameter reaches it, it points into nothing a caller passed.
impl Checker {
fn label(self, world: World) -> String {
return world.names[self.at] // points into `world`; `self` is free again
}
}
struct Pair { left: String, right: String }
fn pair(a: String, b: String) -> Pair {
return Pair { left: a, right: b } // `left` points into `a`, `right` into `b`
}
fn longest(a: String, b: String) -> String {
if a.len() >= b.len() {
return a
}
return b // points into `a` or `b`
}
The source may be written on the result with from (D8):
fn first_word(s: String) -> String from s,
fn longest(a: String, b: String) -> String from a | b. Where it is written,
the body is checked against it: a way out that hands back a view of a
parameter the from does not name is refused with NK1335. It must be
written where there is no body to infer it from - a trait’s method, a function
type, a foreign or Rust function - and a result there that is a view without a
from is refused with NK1336. Elsewhere it is optional. A method
implementing a trait hands back views of what the trait’s from names.
Every argument no view of the result points into is free when the call
returns. The ledger (6.7) writes each view’s sources in the same spelling
(D10): -> String from world, -> String from a | b, and for a value holding
views each by its path, left: from a, right: from b. Each field of a struct keeps its own
sources, whether the type is pub or not: pair(x, y).left points into x
alone. A field whose type holds several views joins their sources, and an
enum joins those of all its variants, since which variant a value is gets
decided as the program runs.
While a view is used, the data it may point into stays as it was. Giving an argument up is not changing it: a caller may assign its variable a new value, leave its scope, move it into another variable or into a struct, a variant’s value, a tuple or a list it builds, or pass it to a lent parameter (6.5) while a view of it is still used, and the old buffer moves into the anchor of whatever holds the view, to be freed with the last such view. The place it moved into holds a view of that buffer, so a field, element or variant’s value is a view wherever such moves flow in, and holds either where text of its own flows in too (2.2). A variable or place it moved into is held to the same rule as the argument. Nothing is copied:
let mut y = "short".clone()
let r = longest(x, y)
y = "replaced".clone() // the old text of `y` lives on while `r` is used
println(r)
Changing that data in place while the view is still used is refused with
NK2305, at the change: a method that changes the value, or an assignment to a
part of it. So is handing the data to a new owner, at the call: a callee
whose parameter is keep or mut (push on a list too), since it may change
or discard the data. So is moving it into a place that must hold text of its
own, at the move: a field, element or variant’s value that the program,
in any function, changes in place or hands to a keep parameter. The
message names the view, the call that made it and the parameter it comes from,
speaks of data and never of how long anything lives; for such a move, also the place that makes
the field own its text. Use the view before the change, or take a copy of it
with .clone() before the change; for a hand-over, a copy may also be what is
handed over (take(y.clone())); for a move into a place, a copy may be what
moves (Cursor { input: text.clone(), pos: 0 }), or the value is built first
and the view taken from its field
(let c = Cursor { input: text, pos: 0 } then parse(c.input)):
let mut y = "short".clone()
let r = longest(x, y)
y.push_str("er") // NK2305: `r` may point into `y` (`longest`: `return b`)
println(r)
A tether keeps the whole buffer alive, not just the part pointed at.
--tethers (on nikaia build and nikaia lower) names every buffer kept this way and what keeps it;
.clone() keeps the part and lets the buffer go.
For every function the compiler infers a borrow contract: which
parameters it keeps, and what each view of its result points into, such as
“the result of checker.label(world) points into world.” The source writes
keep (6.5) and may write from (6.6), and the compiler checks them against
the inferred contract. Contracts are stored in a generated file,
nikaia.contracts, the ledger, which is committed alongside nikaia.lock
(Part III, 13.5). It writes keeps per parameter and a result’s sources in the
source’s spelling, -> String from a | b, a written from as written
(ADR-views-3 D10). A change of either on a pub function
is a contract change.
The cleanup point moves with a kept value (D6): a callee that keeps a
File runs its cleanup at the end of the callee, not of the caller’s block
(6.4). A change to a parameter’s keep on a type whose teardown has an effect
is a contract change the ledger names, with the callers whose cleanup moved.
The ledger has two jobs:
An ownership rule refuses a program that breaks it. Every such refusal explains itself in plain language and names the way out. Reading a Nikaia diagnostic requires no knowledge of Rust; a backend error that reaches user code is a defect of the compiler.
The most common case is changing a collection while looping over it:
let mut users = load_users()
for u in users {
if u.is_duplicate() {
users.remove(u) // bug: pulling the rug out from under the loop
}
}
error[NK2301]: You're changing `users` while a loop is still reading it.
--> main.nika:4:9
|
2 | for u in users {
| ----- the loop reads `users` here
3 | if u.is_duplicate() {
4 | users.remove(u)
| ^^^^^^^^^^^^^^^ changed here
|
= note: Removing items mid-loop would lose the loop's place. In most languages that's a crash or a silent bug.
= help: Use the method that does this safely: `users.retain fn (user) { !user.is_duplicate() }`.
For every known pattern of this kind, the standard library provides a safe,
named method (retain, drain, entry, swap(i, j), …), and the diagnostic
names it.
Nikaia distinguishes two kinds of errors.
throws)A recoverable error is an expected problem: a file is missing, a connection
drops, an input does not fit the format. A function that can fail says so with
throws.
use std::fs
use std::net
fn fetch_config() -> String throws {
let text = fs::read_to_string("config.txt", fs::Root::Anywhere) // can fail
let mut peer = net::connect("127.0.0.1:9000") // can fail too
peer.write(text)
return text
}
throws stands after the result type, and sync with it:
fn fetch_config() -> String throws. A function type has the same order. A
declaration with no result type writes the word after the parameters:
fn tick() sync { … }. The form before the arrow does not parse; the message
names the order.
throws names no types. What a function can fail with follows from its
body. The compiler infers it whole-program and writes it to nikaia.contracts
(Part III, 13.5).
A function value keeps its set. A named function or a lambda used as a
value carries the set inferred for its body wherever it goes: a let, a
field, an element of a list or a map, a result, a parameter, called at once or
kept. A call through it can fail with that set; writing, storing, handing on
or returning it can fail with nothing. Where values with different sets meet
(the arms of an if or a match, the elements of one list or map, the values
assigned to one variable or field, the arguments every caller hands one
kept parameter), the place carries their union. A place no failing value
reaches cannot fail (ADR-351). Code that is kept lives in a value
someone holds, reached from a parameter or a result: the set belongs to that
storage, every handle to it shares it (a Shared, the two ends of a channel),
and it holds everything put there in the value’s life. What a function puts
into such a place is in its ledger entry (Part III 13.5, code).
A code parameter the body does not keep fails with what each call hands
it. Where a function only calls its code parameter or hands it on, a call of
the function fails with the function’s own errors and with what that call
passed, never with what other callers pass, pub or not
(ADR-error-sets-1). With
fn retry(f: fn() throws) throws { f() }, a match over error around
retry(read) is complete when it names what read fails with, whatever else
retry is handed elsewhere. Inside the body, a catch around f() sees
whatever any caller may pass, and needs else.
An error type is an enum.
enum ConfigError {
NotFound(fs::Path),
Unreadable(fs::Path),
BadSyntax { line: i64, expected: String },
}
impl Error for ConfigError {
fn message(self) -> String {
match self {
ConfigError::NotFound(p) => f"no config at {p.display()}"
ConfigError::Unreadable(p) => f"cannot read {p.display()}"
ConfigError::BadSyntax { line, expected } => f"line {line}: expected {expected}"
}
}
}
An error carries what belongs to it: “not found” carries the path. A
match over an enum (4.4) is checked for completeness. What is thrown
implements Error, and the impl line says so. A number, a bool, a
character and text are not errors, and a throw of one is refused with
NK1161.
Raising: throw.
if !fs::exists(path, fs::Root::Anywhere) {
throw ConfigError::NotFound(path)
}
A throw is an expression, and so are return, break and continue.
Their type is never. A never fits every expected type without widening
it, so each may stand wherever an expression may:
let user = find(id) ?? throw NotFound(id)
A throw leaves the function and makes it throws, a break needs a loop,
and a statement after a break in the same block is refused with NK1133
(3.3). What a return written inside an expression hands back stands on its
line: let x = y ?? return at the end of one returns nothing, and the next
line is the next statement.
Propagation happens on its own, and nothing marks it. A call that can fail
stands inside a function that declares throws. There is no operator and no
sigil:
fn load() -> Config throws {
let text = fetch_config() // if it fails, `load` fails
return parse(text)
}
Four things leave the control flow of a program without the line showing it: a call may pause (8.1), a block’s end may pause and fail (6.4), a call may fail (here), and a loop’s step may fail.
The declaration is required. A call that can fail, in a function that does
not say throws, is refused with NK2605:
error[NK2605]: This function can fail, because `liest` can fail.
--> app.nika:2:23
|
2 | fn ruft() -> String { return liest() }
| ^^^^^^^^^^^^^^
|
= note: `liest` can fail with `io::IoError`.
= note: A call that can fail looks like any other call, so its failure passes straight through this function unless you handle it.
= help: Add `throws` to `ruft`, or handle it at the call: `… catch { … }`.
for fn liest() -> String throws { return fs::read_to_string("x.txt", fs::Root::Anywhere) } one
line above. The shape is Appendix C.4’s: the statement is underlined, the
first note says what the call can fail with, as the ledger the call was
resolved against records it (13.5), and the help names the two ways out.
Handling: catch. The block supplies the replacement value, or it leaves
the function.
let config = load() catch {
eprintln(f"{error}")
return // leaves the function
}
let port = read_port() catch { 8080 } // replacement value
Inside the block the error is named error. A handler that passes the
error on writes throw error. A catch over an expression that cannot fail
is refused (NK1134): the expression is the value, and the handler goes.
The catch has the type of the value it guards, or that type with ? where
the block hands back null: config above is what load() hands back, and a
member of it is reached as on that value. A null from the block widens a T
to T?, the way a ?? fallback or an if arm with null widens (3.5); a T
that is already T? stays T?, there is no T??. A block that hands back a
value of another type is refused (NK1103).
let port = read_port() catch { null } // i64?
match parse(line) catch { null } {
v => use(v)
null => {}
}
let fine = (parse(line) catch { null }) != null // bool
Telling failures apart. error is the sum of the errors that can arrive at
this point, as the compiler inferred them. A handler tells them apart with the
patterns of 3.4, where a path with :: names a variant:
let config = load() catch {
match error {
ConfigError::NotFound(p) => Config::default()
ConfigError::BadSyntax { line, .. } => {
eprintln(f"config broken at line {line}")
return
}
else => throw error
}
}
A match over error is complete when it names every type that can arrive: each variant of it, or the type by a bare name. A fatal type need not be named, since it passes on unless named. Where the set holds something the compiler cannot name - a call into code no ledger describes - the match needs else (NK1151). Outside a match, such an error is known by what every member has: Error’s methods, f"{error}" and throw error. What only one member has is reached through the match. A library’s error type arrives as that type: a function that reads
a file hands its failure on as an io::IoError. No throw in the program
raised it, so it has no site, and error.full() says so; the failures that
joined it travel with it through every caller that hands it on.
A catch block that begins with an arm is that match (ADR-catch-1):
f() catch { regex::Error => { … } else => throw error } means f() catch {
match error { regex::Error => { … } else => throw error } }, under the same
rules - patterns, a type by its name, completeness (NK1151), a fatal error
taken only by name - and its value is the arm’s. A block holds statements or
arms, never both; a block of statements is a handler as above.
Fatal errors. An error type is fatal where impl Fatal for T says so,
beside its impl Error for T. Fatal has no methods.
enum Abort { Now(String) }
impl Error for Abort { fn message(self) -> String { return "aborted" } }
impl Fatal for Abort {}
A catch takes a fatal error only in a match arm over error whose pattern
names a variant of its type. A handler without such an arm, an else arm, a
bare name and throw error do not take it: it passes the catch as if no
catch stood there, the handler does not run, and the error leaves the
function, which declares throws (NK2605 otherwise).
let n = run() catch { 0 } // an `Abort` passes on
let m = run() catch {
match error {
Abort::Now(why) => {
eprintln(why)
1
}
else => 0 // not for `Abort`
}
}
process::exit(code) throws process::Exit(code), which is fatal
(Part III 17.1). At the end of main it ends the program with code; every
other error that leaves main ends it with 1 (Part III A.2).
When a set grows. The variants of an error type and the set of error types
arriving at a catch are both closed: a match that names all of them is
complete, and a new one makes it incomplete. When a callee gains a failure, a
match that named every type without else is refused (NK1151), and the
message names the new type and the call that brings it in. A handler with
else, or one that does not match on error, takes the new error as it takes
everything, and is named once in the build output (NK2402, given at the
call inside the handler). Under --locked the build fails until the ledger is
regenerated and committed. The author of a pub function whose set grows is
warned (NK2404): callers that name every type will stop building.
Every error carries its site and its chain. The site is where the error
was raised. The chain holds every error that joined on the way: a cleanup that
failed while the stack was unwinding is attached to the original as a
secondary error rather than replacing it (6.4), and so are the failures of
tasks nobody read when their group ends (8.1.2). The joined errors are kept in the order they
joined, each with its own site, and a log, an uncaught failure and
nikaia explain show them under the first. They are a diagnostic, not a value:
a program does not read them, and throw error hands them on. A catch
catches one error and chooses by its type. The site costs nothing at run time:
the compiler writes it into the binary as text.
An error that crosses an async boundary carries where it crossed. Beside
its site (where it was raised), it records where the child was
started (the async form) and where the error arrived (the wait
point or the join, 8.1.2). A chain of bindings gives one started and arrived pair per
crossing, in order, and each secondary error carries its own. Like the site,
both are text the compiler writes into the binary, and nothing is recorded
where nothing fails. The long form shows them as lines under the message:
raised at fetch.nika:12:9
started at main.nika:4:5
arrived at main.nika:9:5
A stack trace is not carried. NIKAIA_TRACE=1 asks for one; without it
there is none, and the long form says so. Under it, the trace of an error that
crossed an async boundary is the child’s trace, a line
── started at <site> ──, and the trace of the code that waited, for each
crossing.
Printing it: short is the default.
eprintln(f"{error}") // the message, and nothing else
eprintln(f"{error.full()}") // the site, the chain, and a trace if one was captured
{error} is the message the author wrote. A failed HTTP handler answers with a
generic 500 and logs the rest.
The form you type without thinking is the one you may show a stranger.
full() is an ordinary call: a hole holds an expression (2.5), and a method
call is one.
Every error knows the site that raised it and has a short form of it a
person can read out. nikaia explain NK-2C7 leads from (NK-2C7) to the line
that threw it, with no log file, also when the working tree has moved on.
A call the language performs can fail. Two places perform a call user code
did not write: the end of a block, where a resource is cleaned up (6.4), and a
loop’s step over a fallible stream (NK2701). In both the enclosing function
declares throws, and the compiler names the resource or the loop.
It is one rule with two codes. Where a call can fail, written by user code
or performed by the language, the failure fails the enclosing function, the
function declares throws, and the compiler names the call that is the
reason. NK2605 is the written call and the cleanup at a closing brace, which
the message names as the cleanup of the value; NK2701 is the loop’s step.
panic)An unrecoverable error is a logic bug, such as reading the tenth item of a
list of five. A panic in a task ends that task, and the program goes on; a panic
in main ends the program (Part III A.2). Where the machine cannot unwind, the
process ends.
The Panic Hook (std::panic::on_panic)
An abort runs no normal cleanup. Before the process dies, or before the crashed
task is isolated where the program survives, the runtime calls one registered
function: the panic hook. It is the place for a crash dump, a crash report,
or flushing a diagnostics log.
use std::panic
fn main() {
// Global, one per application. Set it early.
// The hook may not pause — mid-panic there is nothing to pause on — and
// that promise is on `on_panic`'s parameter type, not on the lambda:
// a lambda carries no promise of its own.
panic::on_panic fn(info) {
// 'info' carries: message, file/line, and the stack trace.
// Pattern: open crash resources at startup, only WRITE here.
crash_log.write_report(info)
}
run_app()
}
The rules:
on_panic is refused with NK2604. A
crash-reporting library exports a function that the application’s hook
calls.info.The hook may not pause. That promise is on on_panic’s parameter type, not
on the lambda; a lambda carries no promise of its own (5.3).
A program performs several tasks concurrently at both values of
user_parallelism, such as waiting for a download while answering input. The
mechanism is the pause (8.1).
A function that performs I/O, such as reading a file or downloading a URL,
pauses without blocking the program. There is no await keyword.
Within one task, the order is the written order: let a = fs::read("x", root)
pauses, and the line after it does not run until a is there. What runs
meanwhile is some other task; a pause never starts one. A task exists only
where the program starts one: with async (8.1.2), as an element of a
par_iter chain (Part II 12.6), as a select arm (Part II 12.4), or under a
supervisor or a server. Where the program waits for one - the first use of its
value, h.join(), its group’s end - is where two tasks meet again. The marker
for waiting stands where something branches, and nowhere else.
Two statements run in the order they are written, whether or not they touch anything in common. No analysis stands between the source and the schedule. A program that wants two things to run together says so (8.1.2).
The compiler may write a statement that assigns to a place before the ifs
directly in front of it where no program can tell: none of those ifs reads
or writes that place, the statement reads nothing they write, neither side
calls a function that is not pure, does input or output, pauses, locks or
spawns, neither side can stop the program, and the ifs hold only such
assignments and ifs, nothing that leaves. It does so in every build
(ADR-379).
asyncWhat a task is. Every task is started in a group and lives at most as
long as the group. A child task’s group is a block or a call; a free
task’s group is the process (8.2). A task borrows what outlives its group and
takes what does not: a child task borrows what was bound outside its group, a
free task takes everything it uses (8.3). Every task allocates from a memory
block of its own (Part II, Chapter 12), and ends in one of four ways: with a
value, a failure, a crash (task::Crashed) or a full block
(task::OverBudget). That end goes to its group’s receiver. Each form that
starts tasks states only its group’s policy:
| Form | Its group | Policy |
|---|---|---|
async (here) |
the enclosing async { } or block |
wait for all, keep every failure |
par_iter (Part II 12.6) |
the call | per element: results in input order, every failure kept |
select (Part II 12.4) |
the block | the first wins, the rest are cancelled |
supervisor::run (Part II 12.8) |
the call | restart by strategy and policy |
serve (Part III) |
the server | one per connection, by admission |
async(detached) (8.2) |
the process | nobody waits but main’s end; a bound handle is the receiver |
async starts a task. It is a prefix of an expression: it stands on the
right of a =, as an argument, as an operand, or as an expression statement,
and never before a let, a loop or a function’s declaration (NK2115):
whether a function pauses is inferred (8.1). The task runs beside the code after
its start, its window, and is waited for at its wait point. There is no
await.
let user = async db::load_user(id)
let rights = async db::load_rights(id)
let prefs = cache::load_prefs(id) // runs while both loads are in flight
render(user, rights, prefs) // waits for both first
The value form, async e, starts e at once and hands back its value, waited
for where the value is first needed:
let x = async e: just before the first statement of the
binding’s block that names x anywhere in it: in a nested block, a loop, a
branch whether or not it is taken, a lambda. A binding nothing names is
waited for at the end of its group. The binding takes the patterns and the
written type a let takes (2.1); the first statement naming any of its names
waits.f(async g(), async h())
runs g and h beside each other and calls f when both are done;
f(async g(), h()) runs g while h() computes on the main line.let t = async a() + async b().let (a, b) = (async g(), async h()) runs both.The callee never sees a pending value: f is handed a plain T, so no
function’s signature says whether its arguments were computed beside it. The
value form has no handle and cannot be cancelled. Tasks start in the written
order, left to right.
A task with nothing beside it is refused (NK2110): f(async g()) with no
other argument computed beside it, a binding named in the very next statement,
an async statement that ends its group. The help removes the word.
The handle forms. An option after the word hands back a handle instead of the value.
async(handle) fn { … } starts at once and hands back a
task::ChildHandle[T], bound to the group. It borrows and needs no Send and
no heap: it points at the task’s slot in its group. It has join(),
join(within: d) (8.2) and cancel(), which take it, and no detach(). A child handle that leaves its
group - returned past it, stored in a value that outlives it - is refused
(NK2122). One neither joined nor cancelled is waited for at its group’s end,
and its failure goes to the group as an unread failure does.async(deferred) fn { … } hands back a child handle whose task does not
run until h.start(). A deferred handle not started or cancelled on some
path, or started twice, is refused (NK2121). Its window begins at
start().async(detached) fn { … } starts a free task and hands back a
TaskHandle[T] (8.2). detached and deferred combine,
async(detached, deferred) fn { … }.A task of several steps is a lambda. let x = async fn { … }, async fn {
… } and the handle forms run the lambda’s body as the task (5.3): return in
it ends the task with its value. A lambda that names an argument is refused
(NK2103): a task is handed nothing.
async e as a statement starts a task whose value nobody reads; a value it
hands back is dropped. It is waited for at the end of its group.
async xs.par_iter() … .collect() is one task that runs the whole chain
(Part II 12.6) beside the code after it.
async { … } is a group: its statements run in order, as any block’s, and
its } waits for every task started inside it. Its value is its last
expression’s, once every task has ended. An async { } that starts no task is
refused (NK2111).
The group. A child task belongs to the innermost async { } around it in the
same function, branches between them included; where there is none, to the
innermost block that holds the async form. async starts only tasks whose
number stands in the source: an async form inside a loop’s body is refused
(NK2120), and the help writes xs.par_iter().for_each fn(x) { … }. A
lambda’s body is a function of its own (5.3), so a lambda written in a loop may
hold one. What was bound inside the group, in a block that ends before the
group does, the task takes with it as a free task does (8.3). A break,
continue or return that would leave a task is NK1132.
A task and the code beside it meet on nothing, and the compiler checks it.
A bound task is checked against every statement of its window, other tasks’
starts included; the tasks of one call’s arguments and of one operation’s
operands are checked against each other and against what is computed beside
them on the main line. A pair that meets on a resource is refused with
NK2104, and the message names the resource: two tasks that both write to
stdout are refused. A binding that the window changes - a mut method, an
assignment, a mut argument - while a task names it is refused the same way,
naming the binding. Two reads meet on nothing. A task this compiler cannot
account for is not refused (Part III C.4). --overlaps reports each task, its
window, its wait point and its group.
A pending binding named in another task is waited for in that task.
let a = async load(id)
let b = async enrich(a) // b's task waits for a
show(b)
b’s task reads a beside a main line that reads it too; the main line
waits for a where it first names it. Where b’s task takes a by value,
a is handed over where b starts, and a later mention of a is NK2105.
Two tasks that read a read the one value. Where a fails, the first
mention of it in written order receives the failure, and every other task that
waits for a is cancelled.
Options. async(limit: n, within: d, admission: s) stands before { and
before the task’s expression; options and the handle options are written in
one list, without a positional zone.
limit: i64 on an async { }: at most n of its tasks at once; the
others wait, unstarted, in written order. A task is under every limit
around it. Below 1 it panics, naming the option. A form that starts one
task does not take it (NK2112). limit: 1 written as the literal runs the
tasks one at a time, in order, and they are not checked against each other.within: time::Duration: a deadline counted from where the form
starts, on every form, value or handle. When it runs out, its tasks still
running are cancelled, and task::TimedOut arrives where the value is first
needed or at the join; for a statement form and a group, at the group’s end.
It is a failure and meets the handlers below, the fallback included:
async(within: 5.seconds()) http::get(url) catch { Page::empty() } gives
Page::empty() when the deadline runs out. A limit on one wait is join’s
option, not the task’s (8.2).admission: task::Admission: who decides when the next task starts
(Part II, Chapter 12).Admission. A child task starts only where its block fits the budget of the
task that runs the group and a descriptor is free; otherwise it waits,
unstarted, until a sibling ends. limit: caps on top of that. At the system’s
ceiling of descriptors a task waits; it never fails for it.
Cancelling is through a handle only. h.cancel() stops the task at its
current pause; its values are torn down and its outcome, a failure included,
is dropped. It takes the handle. The group’s end waits until a cancelled child
task has stopped, its cleanups included. A value form cannot be cancelled; a
program that may want to writes async(handle) fn { e }. within: cancels by
itself.
A failure is held until it arrives, and the same handlers meet it in every form, in this order.
let page = async http::get(url) catch { Page::empty() } // the fallback
let size = page.len() catch { 0 } // a catch where it arrives
let h = async(handle) fn { crawl(site) } catch { [] } // a handle's fallback
let found = h.join() catch { [] } // a catch where it arrives
catch inside the task’s expression or lambda is the
task’s own and runs at once, when the task fails.h.join(), for a handle. A catch that encloses that point handles it.catch written directly after the async
expression runs, the last handler before the receiver: for the value form
at the wait point, its value becoming the value; for a handle inside the
join, its value becoming the join’s; for a detached task nobody joins, in
the task itself when it fails.A cancellation the program asks for, h.cancel() or a select arm that loses,
is not a failure and runs no fallback. task::OverBudget is one and runs it,
and so is task::TimedOut from a deadline (within: above). The
task::TimedOut of a join(within:) whose own limit ran out is the join’s
error, not the task’s: it meets a catch around the join, and no fallback runs
for it (8.2). Which handler runs depends on the path that reaches the
mention. Once a catch where the failure arrived has taken it, a later mention
of x on that path is refused unless the binding has a fallback (NK2118).
The function throws what the fallback throws, or the task’s whole set where
there is no fallback.
A failure nobody read is thrown at the end of the group. It is a failure
of a statement form, of a binding nothing names, of one whose mention did
not run, or of a child handle nobody joined; a fallback runs for it there.
Where the group ends normally (its last statement, a return, a break), the
first such failure in written order is thrown, and the others travel as its
secondary list, in written order. Where a failure leaves the group, the
group waits for every task, and that failure leads; the unread failures are
its secondary list, in written order. Leaving a group waits for its tasks
and cancels none.
A task that panics cancels its siblings, and the group throws
task::Crashed where it ends. A task whose memory block is full ends with
task::OverBudget (Part II, Chapter 12) and fails only itself.
A body that holds an async form pauses, so it is not sync, whatever
its tasks do (Part II, 12.1). A task that never pauses runs on the pool
where tasks run in parallel; at no, it runs when the main line next
pauses or waits, so that the pausing tasks written before that point are
started first. A child task that pauses is run by its group, on the group’s
thread, and is never handed to the runtime. An async form inside an open lock
is refused (NK2203).
The meaning is the same at both values of user_parallelism; the duration
differs. At no, a task that computes still overlaps with another task’s
waiting, because the waiting is not user code. Computation beside I/O
overlaps under both values; computation beside computation overlaps only under
yes (1.2).
async(detached)async(detached) starts a free task: its group is the process, so it may
keep running after the function that started it has ended. It takes a lambda
holding the code to run: the one lambda form (5.3), whose body is a block
whether it holds one line or several.
async(detached) fn { println("I am running in the background!") }
spawn fn { … }, the form this replaces, is still accepted and means the same
until every program is moved; then spawn is released (ADR-async-5 D13).
A free task does not end the program. An error that arrives in a task ends the task, process::Exit (7.1) included, and every such error has a receiver (ADR-346):
async(detached) whose result is not bound is detached: nobody waits for it. If it ends with an error that its fallback (8.1.2) does not handle, the error is printed on standard error in the long form of an uncaught error; a fatal one (7.1) also replaces the exit status, at the end of main and not earlier, the first one counting (Part III A.2).let h = async(detached) fn { … } binds a TaskHandle[T], and on every path out of the scope it is taken by h.join(), h.join(within: d) (below), h.detach() or h.cancel(), or handed on: returned, stored in a field or a container, or passed to a function. A handle taken in only one branch is not taken; otherwise NK2109.h.join() throws what the task’s body throws, beside task::Crashed for a task that panicked and task::OverBudget for one whose block was full, and takes the ordinary way: a catch, or throws on the joiner. A fatal error nobody catches reaches main and sets the exit status there.h.join(within: d) limits this wait to d, counted from the call. When d runs out, join throws task::TimedOut, the task keeps running, and the handle stays live: it can be joined again, cancelled or detached. With on_timeout: task::OnTimeout::Cancel (task::OnTimeout is Keep | Cancel, Keep by default), the task is cancelled as cancel() cancels it, its cleanups run, the call throws task::TimedOut, and the handle is used up. The task’s own within: (8.1.2) also ends in task::TimedOut at the join.join(within:) without on_timeout: task::OnTimeout::Cancel takes the handle only where it does not time out. On the path where it throws task::TimedOut, the handle is still live and must be taken on every path out of the handler: in a match error arm for task::TimedOut, in an else or binding arm that can match it, and in the whole handler where it does not match on the error; otherwise NK2109. With on_timeout: task::OnTimeout::Cancel it is taken on both paths. A child handle (8.1.2) left live is waited for at its group’s end.h.detach() makes a bound handle’s task detached, as an unbound one is.async(detached, deferred) fn { … } does not run until h.start(), as a deferred child task does (8.1.2).let h = async(detached) fn { fetch(url) }
let page = h.join(within: 5.seconds()) catch {
match error {
task::TimedOut => { h.cancel(); "" } // the handle is still live here
else => throw error
}
}
let m = async(detached) fn { fetch(mirror) }
let copy = m.join(within: 5.seconds(), on_timeout: task::OnTimeout::Cancel) catch { "" }
async(detached) fn { log_forever() } // detached
let w = async(detached) fn { watch() }
w.detach() // detached as well
A free task may keep running after the function that started it has
finished, so it does not borrow. Under the contextual capture rules (5.4),
async(detached) is a detached context: a variable used inside the task is
moved into it. There is no move keyword.
let message = "Hello"
// 'message' is implicitly moved into the task (detached)
async(detached) fn { println(message) }
// Compiler Error: 'message' now belongs to the task.
// println(message)
The type of the value decides between two cases. For ordinary data (a
string, a number, a struct or collection of those) a move is a move. A handle
on a Shared[T] is duplicated rather than moved (6.2), so the name outside
the task keeps working. message here is a string, so the rest of this
section is the data case.
A program that needs the value afterwards clones it before the task is
built and gives the task the copy. A .clone() inside the body clones the
task’s own copy and leaves nothing for the parent.
let message = "Hello"
let copy = message.clone() // made here, while `message` is still ours
async(detached) fn { println(copy) } // the copy is what moves into the task
println(message) // OK: `message` never left
Data moved into a task and used again afterwards is refused with NK2101:
error[NK2101]: You're using `message` after a background task took it.
--> main.nika:4:5
|
4 | println(message)
| ^
|
= note: A task started with `async(detached)` may outlive this function, so it takes the variables it uses with it. `message` is a `String`, so it went to the task.
= note: Numbers, `bool`s, views and `Shared[T]` handles aren't taken: they're copied.
= help: Make a copy before starting the task and give the task the copy: `let copy = message.clone()`, then use `copy` inside the task.
NK2101 belongs to the data case only. A handle on a Shared[T] used again
after the task is built is not refused, and no .clone() is written.
NK2101 is raised only where the type is known and a move takes the value
away. A number, a bool, a scalar and a view are copied, so
let message = "Hello" is not this case and let message: String = "Hello"
is. An assignment between the task and the later use clears it. The form
above is the one spelling of a free task, the trailing lambda of 5.3. A lambda
that names an argument is refused with NK2103: a task is handed nothing.
A task means the same thing at both values of user_parallelism. At yes
a task goes to a pool of futures over the user-pool worker count and runs on
a thread of its own, and its future must be Send. At no every task
interleaves on the one thread (Part II 11.2).
At user_parallelism = no, user code runs on one thread. The runtime uses a
hidden sidecar to carry out heavy I/O without blocking that thread.
The runtime starts before the first statement of user code, with one I/O
thread always and a pool for user code only at user_parallelism = yes. What
runs on the I/O thread is std’s own code and nothing else: the boundary is a
closed list of operations, not a queue of closures. A file read is handed to
the kernel where the machine can complete it. A file operation is a
slot, on the ring or in an I/O worker’s reply; asking whether it has finished
never blocks, so the main loop runs whatever else is ready and parks in the I/O
only when nothing is.
A package is a directory. The files in it see one another with no use at
all: they share one namespace, and a name declared in any of them can be written
in any other.
// file: src/parse.nika (the same package as src/main.nika)
pub struct Row { pub id: i64 }
fn helper() -> i64 { return 41 }
// file: src/main.nika
fn main() {
let r = Row { id: helper() + 1 } // no `use`, and no prefix
println(f"{r.id}")
}
Two files of one package may not declare the same name. A use naming a
file of the same package is refused.
A package reaches another package by its name. use http makes the package
http reachable and does nothing else. Every name from it is written with its
prefix, at every use. A package is depended on in [dependencies] (Part III,
13.3), and a use naming no dependency is refused.
use http
use request_handling as rh
fn handle(r: http::Request) -> rh::Response {
let body: http::Body = http::read(r)
return rh::ok(body)
}
The prefix falls where a name is written, not where a value is used:
r.path and r.header("host") carry none.
No name is brought in: not by a glob, not by a braced list, not one at a time:
error: `use` can't bring in single names.
--> main.nika:1:11
|
1 | use http::{Request, Response}
| ^
|
= help: Write `use http`, and `http::Request` where you need it, or `use http as h` for a shorter prefix.
use x as y shortens a long prefix once, in one place.
A prefix is introduced before it is used: http::Request without
use http is refused. One name per file: two packages under the same name
in one file, by alias or by collision, are refused.
A name denotes one thing. A second declaration is refused with NK1148,
with the caret on the one that arrived. A fn, a struct, an enum, a
trait and a grammar declare a name. A method belongs to its type, so
two types may each have a len; a rule belongs to its grammar and is
reached as Json::value.
A directory under src/ that holds .nika files is a package of the
project, named by its path: src/model/ is model, src/model/rows/ is
model::rows. It is reached with use model or use model::rows and needs no
[dependencies] entry; its names are written with the last segment as their
prefix, rows::Row. Packages of one project may use one another, in a cycle
too. Part III 13.3’s [directories] treats a directory otherwise.
use std::fs names the standard library rather than a package. It brings no name in either: it names a
module and the module’s items are reached through it, as a package’s prefix
works. A use whose last segment is a type is refused with NK1156. What
needs no use is the list in 1.3. HashMap is not on that list: it is
use std::collections and collections::HashMap.
A diagnostic names the package rather than the alias: the type is
http::Request whatever one file calls the package. Outside a project a
.nika file is compiled on its own: a package is a directory of a project,
and a directory of loose examples is a directory of programs.
Visibility is per package.
comptime binding are visible inside
the package that declares them, in every file of that directory, and
nowhere else.pub keyword:
pub.pub.The boundary is the package and not the file. Moving a declaration from one file of a package to another changes no visibility.
Reaching a private item or field from another package, by reading a field or
by giving one a value in a struct literal, is refused with NK1110, and the
message says which package keeps it:
error[NK1110]: You can't use `secret` from here: it's private to `utils`.
--> main.nika:4:5
|
4 | let n = utils::secret()
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= note: Everything is private to its package unless it's declared `pub`.
= help: Write `pub fn secret` in `utils`, or use something it makes public.
pub makes an item available to every package. A private field is reached
through a constructor or a method, so a type’s invariants hold outside its
package.
// file: network.nika
// Private: only usable inside its package
struct Config {
port: i32
}
// Public: Usable by anyone
pub struct Server {
// Private field: Can only be changed by Server methods
config: Config,
// Public field: Can be read/written by anyone
pub name: String
}