Type System and Static Semantics — M1 subset
Status: normative for Milestone 1 (increment 1b implements it). Later milestones extend these rules. Where language-spec.md describes a later feature, it does not apply until then. Error codes are listed in diagnostics.md.
1. Types
| Type | Values | Notes |
|---|---|---|
i8 i16 i32 i64 | two's complement, −2^(n−1) … 2^(n−1)−1 | |
u8 u16 u32 u64 | 0 … 2^n−1 | |
isize / usize | pointer width of the target (64 on every M1 target) | from TargetInfo, never the host |
bool | true, false | |
void | no value | function return type only |
| never | no value; the expression does not complete | internal only (Process.exit); not nameable |
Not in M1 (error N0216): floats, char, strings as values, user-defined types, references. Floats came in M11 (f32, f64, §35); character literals are integers (§4). In M1 a string literal was allowed only as a direct argument of Console.write/writeLine; since M6c it is a StringView value (§22.2).
void may appear only as a return type (writing -> void is the same as omitting the return type). A never expression is accepted wherever any type is expected.
2. Names and scopes
A package is the unit of compilation and of naming (ADR 0013, tooling.md §4): its files, its dependencies, and lib/core. A build checks the package being built together with every package it depends on, directly or not.
Package scope. All files of a package share one item scope. Items are referenced by simple name. Two items of one package with the same name are an error (N0201), even in different files.
namespacedeclarations are accepted and have no effect (retired by ADR 0013; they will become an error).Dependencies (M10 T5). A package names its dependencies in its manifest (
tooling.md§4). A dependency's public items are reached by qualifying them with the dependency's name:- in a type,
geometry.Point,geometry.Pair<u8>; - in an expression,
geometry.area(p),geometry.LIMIT,geometry.Shape.Circle(2), a struct literalgeometry.Point { x: 1, y: 2 }; in a pattern,geometry.Shape.Circle(r).
A static function of a generic type of a dependency is called through an import (
Pair<u8>.make()), sincegeometry.Pair<u8>.make()does not parse (grammar.md §2.1: in an expression, generic arguments follow a simple name only).- in a type,
Dependency names are reserved. In a package, nothing else may have the name of one of its dependencies: no item, parameter, local, loop variable or pattern binding (N0315). So
geometry.x, withgeometrya dependency, always means the dependency's item, and the compiler resolves it before type checking (PackageNames).Imports.
import geometry;at the top of a file brings every public item of the dependencygeometryinto that file's scope;import geometry.Point;brings one (N0311 if it has none of that name, N0312 if it is not public). An import applies to its file only. The name before the.must be a dependency of the package (N0310).Visibility between packages. Only
publicitems are visible to other packages, and of a public struct only itspublicfields and methods;internal, the default, keeps an item within its package, andprivatewithin its file (§12). Naming another package's non-public item is N0312. A public function, constant, field or struct whose signature names a type its dependents cannot see (a public function returning an internal struct) is N0314. Animpl I for Tapplies whereverIandTare both visible.Built-in scope (outside the package scope, so a user item hides a built-in):
Console.write,Console.writeLine,Process.exit,truncate, and the primitive type names.lib/coreis in scope in every package, unqualified, and no package may redeclare one of its names (N0201, §29.2).Function scope. Parameters, then nested block scopes. A local is visible from the end of its declaration to the end of its block.
No shadowing of locals. Declaring a local or parameter whose name is already a local or parameter in scope is an error (N0203), as in C#. Shadowing an item with a local is allowed.
Lookup order for a simple name: locals (innermost first), the package's items, the file's imports,
lib/core, built-ins. Two imports that both provide a name are an error where the name is used (N0313), not where it is imported. Fora.b: ifais a local or an item of the package or oflib/core,.bis a member as before; otherwise,abeing a dependency (and so not a local or item, N0315),a.bis that dependency's itemb.Messages and C names write an item of a dependency with its package's name,
geometry.Point; the C backend gives it a prefixed name,nvT_geometry_Point(c-backend.md§2.1).
3. Bindings and mutability
let x = e;declares an immutable local and requires an initialiser (N0220).var x = e;/var x: T = e;/var x: T;declare a mutable local. Without an initialiser the type is required (N0221), and the variable must be definitely assigned before every read (§7).- Without an annotation, a local's type is the type of its initialiser. An initialiser of type
voidis an error (N0224). - Parameters and
constitems are immutable. Assigning to them, or to alet, is N0211. - Assignment targets in M1 are locals only. Member targets (
a.b = …) are N0216.
4. Integer literals
- A suffixed literal (
7u8) has the suffix's type. - An unsuffixed literal takes the expected type from context (§5) if that type is an integer type. With no integer expectation it defaults to
i32. - The value must be representable in the literal's type (N0206). A literal directly under unary minus is checked as a negative value, so
-128i8and-9223372036854775808(asi64) are valid. - Character literals (M11, G4a).
'k'is an integer literal whose value is the character's Unicode scalar value (grammar.md§1.6, escapes included:'\n'is 10). It takes its type from context like an unsuffixed literal, soc == 'k'withc: u8compares bytes, and'0'..'9'is a range. With no integer expectation it is au32. In an 8-bit type it must be ASCII (at most0x7F), since a larger scalar value is more than one byte in UTF-8 and would not equal any byte of the text (N0206). It is a pattern too (§15.2). Nova has nochartype: text is UTF-8 bytes (§22.2), and a character type is left for when text gains character iteration.
5. Expected types (bidirectional checking)
Each expression is checked against an optional expected type:
local initialiser → the annotated type; assignment value → the target's type; argument → the parameter type;
returnvalue → the return type;constvalue → its declared type;if/whilecondition →bool; each value branch of aniformatchexpression → the expression's expected type (§15.3).An expression's actual type must equal the expected type, unless it is never (N0205).
Flexible operands. An expression is flexible if it consists only of unsuffixed integer literals combined with unary
-, parentheses and the arithmetic/bitwise binary operators, or (M11) if it is aniformatchexpression whose expression branches are all flexible (§15.3). Since M11 (G7), unsuffixed float literals combined the same way are flexible too; they take an expectedf32orf64instead (§35.2). For a binary operator whose operands must have the same type:- if the operator's result type is the operand type and an integer type is expected, both operands are checked against it;
- otherwise, if exactly one operand is flexible, the other operand is checked first (without expectation) and its type becomes the flexible operand's expected type;
- otherwise both are checked without expectation (flexible ones default to
i32).
So
x + 1withx: u8types1asu8, andlet y: u64 = 2 * 3;types both literals asu64.
6. Operators
All binary operators require both operands to have the same type. There are no implicit conversions.
| Operators | Operand types | Result | Runtime behaviour (implemented in 1c) |
|---|---|---|---|
+ - * | integer | same | overflow → panic |
/ % | integer | same | zero divisor → panic. MIN / -1 → panic (overflow). / truncates toward zero, % has the sign of the dividend, and MIN % -1 is 0 |
& | ^ | unsigned integer | same | |
<< >> | left: unsigned integer; right: any integer type | left type | amount < 0 or ≥ bit width → panic. >> is logical |
== != | integer or bool | bool | |
< <= > >= | integer | bool | |
&& || | bool | bool | short-circuit |
unary - | signed integer | same | -MIN → panic |
unary ! | bool | bool |
Explicit overflow behaviour (M11, G4b; OD-6). The operators panic on overflow. Where a program wants another behaviour, every integer type has nine methods. Each takes one argument of the receiver's type, and a flexible literal adapts:
| Method | Result | On overflow |
|---|---|---|
x.wrappingAdd(y), x.wrappingSub(y), x.wrappingMul(y) | T | the result modulo 2^bits, in two's complement for signed types |
x.checkedAdd(y), x.checkedSub(y), x.checkedMul(y) | T? | null (otherwise Some of the result) |
x.saturatingAdd(y), x.saturatingSub(y), x.saturatingMul(y) | T | the nearest of T's minimum and maximum |
None of them panics. Their names are reserved on integer types: an impl for an integer type cannot define a method with one of these names (N0228). In C they are static inline helpers of rt.h with no undefined behaviour: a wrapping operation computes in the unsigned type of the same width and converts back with nv_as_iN (c-backend.md §6).
Bitwise operators and shifts are restricted to unsigned types in M1. This avoids the implementation-defined and undefined parts of C's signed bit operations. Reinterpret with truncate<u32>(x). Relaxing this later is backward compatible. A shift amount that is a flexible literal is typed as u32. Other mismatches are N0207.
7. Conversions
e as Tis allowed only when every value of the source type is representable inT(N0208 otherwise):- identity;
uN → uMandiN → iMfor M ≥ N;uN → iMfor M > N;bool →any integer type (false→0,true→1).isize/usizeuse the target width. Floats (M11, §35.4):f32 → f64, and the integers that a float type holds exactly.
truncate<T>(e):eof any integer type,Tany integer type. The result isemodulo 2^bits(T), reinterpreted inT(two's complement). It is always defined. Use it for narrowing and for signed↔unsigned reinterpretation.
8. Calls and statements
- A callee must be a package function, or one of the built-ins below (N0210 otherwise). Functions are not values in M1 (since M11 they are, §36.1). The argument count must match (N0209), and each argument is checked against its parameter type.
- Built-ins:
Console.write(x)andConsole.writeLine(x):xis an integer, abool, or a string literal.writeLine()with no argument writes a newline. Both returnvoid.Process.exit(code: i32): has type never.truncate<T>(e): see §7. Exactly one type argument (N0216 for other generic arguments).
- An expression statement must be a call (N0225).
1 + 2;is rejected. break/continuemust be insidewhileorloop(N0214).return;only in avoidfunction.return e;only in a non-voidfunction (N0215).
9. Control flow facts
- Definite assignment. A local may be read only if it is assigned on every path from its declaration to the read (N0212). Compound assignment (
x += 1) readsx. Code that is unreachable is not checked. M1 has no moves, so the analysis is a forward must-analysis over the structured statements; it moves to NIR in M2. - Termination. A statement may complete unless it is a
return,breakorcontinue, or a call of type never. It also may not complete if it is a block containing such a statement, anifwith anelsewhose two branches both cannot complete, or aloopwith nobreakthat targets it. Awhileloop may always complete, whatever its condition: writeloopfor infinite loops. Aniformatchexpression (§15.3) cannot complete when none of its branches can: its condition or scrutinee does not complete, or every branch is a block that cannot complete. - Missing return. If the end of a non-
voidfunction body may be reached, that is N0213.
10. Constants
const N: T = e;whereeis a constant expression: literals, other constants, and unary, binary,asandtruncateapplied to constant expressions. Calls are not constant (N0217).- Constants are evaluated at compile time with the runtime semantics of §6. An operation that would panic at runtime (overflow, division by zero, bad shift) is a compile error (N0217).
- A constant that depends on itself is N0218.
- Uses of a constant are replaced by its value, typed as the constant's declared type.
- Constant arrays (C9, ADR 0014):
Tmay be[E; N]withEan integer type orbool, andean array literal[e1, …, eN]or repeat[v; N]of constant expressions. A use is the whole array's value, like an array literal: it can be indexed and iterated, and bound to a local to be sliced or borrowed. Arrays of other types, and of arrays, are N0216. @exporton a constant makes it C data (§28.4).
11. main
If a function named main exists, it must take no parameters and return i32 or void (N0219). Whether main is required is decided by novac build (increment 1c).
12. Structs (M2a)
struct Point {
x: i64;
y: i64;
fn sum(this) -> i64 => this.x + this.y; // method: `this` by value
fn origin() -> Point => Point { x: 0, y: 0 }; // static function
}- Declaration. Top level only. Field names and member names (fields and functions) are unique within the struct (N0228). Field types are any non-
voidM1 type or another struct. A struct must not contain itself by value, directly or through other structs (N0227: infinite size). Recursive data needs indirection (Box, M3). Empty structs are allowed. - Names. A struct name is a package item (unique with functions and constants, N0201) and a type name. In expressions,
S.f(...)calls the static functionfofS. - Copy. In M2a every struct is a Copy type: its fields are Copy and
dropdoes not exist yet. Assignment, argument passing and return copy the value. OD-7's rule ("Copy iff all fields Copy and nodrop") takes effect in 2c. - Literals.
S { f1: e1, …, fn: en }must initialise every field exactly once (missing: N0229, unknown: N0230, duplicate: N0231), in any order. Eacheiis checked against the field's type. Initialisers are evaluated in source order (intermediate-representation.md), not in declaration order. - Field access.
e.fwhereehas struct typeSandfis a field ofShasf's type (unknown field: N0230;eis not a struct: N0234). An access chaina.b.cis a place ifais a local orthis. - Field assignment.
a.f = eanda.f op= erequire the root localato be avar(N0211).thisis immutable in a by-value method. The root must be definitely assigned as a whole first (N0212): M2a has no partial initialisation. - Methods. A function in a struct whose first parameter is
thisis a method.thishas the struct's type and is an immutable binding (a copy for Copy structs). It is called ase.m(args), whereeis evaluated first and passed asthis. A static function (nothis) is called asS.f(args). Calling a method asS.m(), or a static function ase.f(), is N0233.thisanywhere other than a method's receiver position or body is N0232. - Private fields (C7c). A field declared
private(private data: T;) is accessible only in the source file that declares its struct (language-spec.md§5: "visible within the declaring type or file"), and in any instance of a generic one there: read, assigned, borrowed and given in a literal. Anywhere else, naming it is N0296. A struct with private fields can therefore only be made by code in its own file, so types that cooperate, like a lock and its guard, can share their fields. This is the one place where a visibility modifier takes effect within a package (§2). It lets a type keep an invariant that safe code cannot break, such as "the data is reached only through a guard" (integrations/cosmoos/c7/sync.nova).publicandinternalfields, and the default, stay visible throughout the package. - Operators. No operator applies to struct values in M2a (
==included: N0207).Console.writecannot print structs (N0207). - Layout. Fields are laid out in declaration order with C's rules (
@repr(C)by default,memory-model.md§2). The compiler computes size, alignment and field offsets fromTargetInfo. The generated C checks them with_Static_assert(c-backend.md§2), so a disagreement with the real C compiler fails the build instead of miscompiling.
13. References (M2b)
The algorithm that enforces these rules is in lifetimes.md.
13.1 Types
borrow T(shared) andborrow mut T(exclusive), whereTis any non-reference, non-voidtype.borrow borrow Tis rejected (N0246). At runtime a reference is a non-null pointer.Where references may appear (v0):
- parameter types;
letlocals with an initialiser;- return types (with a source list, §13.5).
They may not appear in struct fields (E3), constants, or
varlocals (N0246). Struct fields of reference type need lifetime parameters (OD-3).borrow Tis Copy. Aborrow mut Tvalue is never copied: every use reborrows it (§13.3).
13.2 Creating references
borrow p/borrow mut p, wherepis a place: a local,this, a field path, or a path through a reference such asr.f((*r).f). Borrowing anything else is N0247; bind the value to a local first.borrow mut prequirespto be mutable: rooted at avarlocal, or reached through aborrow mutreference (N0248). Aletbinding, a parameter, a by-valuethis, and anything reached through a shared reference are immutable. That holds through aborrow mutorSliceMutstored where a shared reference leads: inh.vwithh: borrow Holderandv: borrow mut Vector<T>, the vector cannot be changed, as with Rust's& &mut T.Call sites.
- A
borrow Tparameter accepts a place or value of typeT, borrowed implicitly. A value becomes a temporary that lives until the end of the statement. - A
borrow Tparameter also acceptsborrow Torborrow mut T; the latter is coerced by a shared reborrow. - A
borrow mut Tparameter acceptsmut p, wherepis a mutable place of typeT, or aborrow mut Tvalue (reborrowed).
The
mutmarker is required for a plain place and is rejected where the parameter is notborrow mut(N0249). It makes mutation visible at the call site, as C#'srefdoes.- A
Receivers.
fn m(borrow this)andfn m(borrow mut this)declare reference receivers;thisthen has typeborrow S/borrow mut S. Ine.m(…)the receiver is borrowed implicitly, with no marker. Forborrow mut this,emust be a mutable place or aborrow mut Svalue (N0248).
13.3 Using references
- Auto-deref. Where a value of type
Tis needed (an operand, a by-value argument, an initialiser of typeT, a return of typeT,Console.write), aborrow Torborrow mut Toperand is read through:*r. Field accessr.fand method callsr.m()also look through references. - Writes through references.
r = v,r op= vandr.f = vwrite to the referent. This requiresborrow mut(N0211: "cannot assign through a shared reference"). A reference local itself can never be re-pointed. - Reborrow of
borrow mut. Using aborrow mut Tvalue as a call argument, an initialiser or a return value creates a new mutable loan of*r. The original reference is unusable while the new one is live, and usable again afterwards (B4). This holds for a field of typeborrow mut TorSliceMut<T>(s.left) as for a variable: it is reborrowed, not moved out of its struct, and inside a closure it is a mutable use of the struct. Its referent must be mutable from where it is used (§13.2): a field reached through a shared reference cannot be reborrowed mutably (N0248). - References support no operators of their own:
==compares the referents (via auto-deref).
13.4 Borrow rules (summary of lifetimes.md)
While a loan is live (the reference, or something derived from it, is used later), the borrowed place may not be:
- written, mutably borrowed, or go out of scope, if the loan is shared;
- accessed at all, if the loan is
borrow mut.
Diagnostics: N0240 (write), N0241 (conflicting borrow), N0242 (read while mutably borrowed), N0243 (out of scope or temporary dropped while borrowed).
13.5 Returning references
- A function returning
borrow T/borrow mut Twith exactly one reference parameter (counting a reference receiver) returns something derived from that parameter. - Otherwise the return type names the sources:
fn longer(a: borrow T, b: borrow T) -> borrow(a, b) T. Missing, unknown or non-reference sources are N0245, and so is aborrow mutresult naming a shared parameter. Afterborrow, a(begins a source list only when it holds names separated by commas and a type follows the); otherwise it begins a parenthesised type (M11, §22.1), soborrow (T?)is a reference to an optional andborrow(a) (T?)names a source. - The body may only return references derived from the named parameters (N0245). A reference to a local or temporary of the function is N0244.
14. Move types, destruction, using and defer (M2c)
Normative for 2c. It implements ownership.md O1–O5 and R3. The NIR analyses are described in intermediate-representation.md.
14.1 Copy and move types (OD-7)
- Copy: integers,
bool, andborrow T. A struct is Copy iff it has nodropblock and every field is Copy. - Move types: structs with a
dropblock or a move-type field, andborrow mut T(whose uses are reborrows, §13.3, so in practice it is never moved out of a variable). - Needs drop: a struct with a
dropblock, or one with a field that needs drop. Every type that needs drop is a move type. The converse does not hold.
14.2 drop blocks
struct File {
fd: i32;
drop { Sys.close(this.fd); } // `this` is `borrow mut File`
}- At most one
dropblock per struct (N0228). Inside it,thishas typeborrow mut S. The block cannot be called explicitly. It runs exactly once when a value ofSis destroyed (O4), and the fields that need drop are destroyed after it, in declaration order. - Moving a field out of a value whose type has a
dropblock is N0252: thedropblock needs the whole value. Moving out ofthisinsidedropis impossible anyway (§14.3).
14.3 Moves
Using a move-type place by value moves it:
- an initialiser or right-hand side;
- a by-value argument or a by-value
thisreceiver; - a struct-literal field;
- a
returnvalue.
The place is then uninitialised. Reading, borrowing or moving it again before it is reassigned is N0250 ("use of moved value"); this is O2 and O3.
move pexplicitly moves out of the placep, even for a Copy type. Afterwardspis uninitialised, which ends a value's use early.Partial moves (O2): moving
p.fout of a local is allowed if no struct on the path fromptofhas adropblock.pis then partially initialised.p.fmay be reassigned, after whichpis whole again. Readingpas a whole, or borrowing it, needs all of it initialised (N0250).Not allowed:
- moving out of a place reached through a reference (N0251), because the referent must stay valid; this includes
thisinborrow/borrow mutmethods; - moving out of a
usingbinding (N0253).
- moving out of a place reached through a reference (N0251), because the referent must stay valid; this includes
Moves and borrows: moving a place while it is borrowed is a conflict, treated like a write (N0240).
14.4 Initialisation (replaces §9's first bullet)
The analysis runs on NIR move paths (intermediate-representation.md):
- A place must be definitely initialised to be read, borrowed or moved. If it may never have been assigned, that is N0212. If it may have been moved, that is N0250.
- Assigning a field
p.frequires the rest ofpto be initialised (no piecewise construction of an uninitialised struct). Re-initialising a moved-out field is allowed.- If some enclosing struct of
p.fhas adropblock, that whole struct must be initialised,fincluded: such a struct is never partially moved (N0252), so a missing field means the whole value was moved. This also covers a struct whose only field isf(found by the mutation suite, M2d).
- If some enclosing struct of
14.5 Destruction (O4)
- A value that needs drop is destroyed exactly once, at the first of these:
- the end of its variable's scope: the block end, a
break/continueleaving the block, or areturn; - the end of its statement, for a temporary;
- being overwritten by an assignment, in which case the old value is destroyed first.
- the end of its variable's scope: the block end, a
- Order: locals in reverse declaration order; then a struct's
dropblock; then its fields in declaration order. By-value parameters are destroyed when the function returns, after the locals. - Moved values are not destroyed. A value that has been moved on some paths gets a runtime drop flag. A partially moved struct destroys only the fields it still has.
Process.exitand panics do not run destructors or deferred statements (panic = abort, OD-5).
14.6 using
using x = e; declares a local that cannot be moved out of (N0253) and is destroyed at the end of its block, like any local (R3). It documents that the value's lifetime is the scope: a lock guard, an arena, a file.
Since M4b: a using binding is pinned, not immutable. Its contents may be changed and it may be borrowed mutably (arena.reset(), frame.enter()). It cannot be assigned as a whole (N0211) or moved (N0253). This also lets scoped values that must be mutably borrowed, such as scratch frames, live in a using binding, since they cannot be var.
14.7 defer
defer s; schedules the statement s to run when the enclosing block exits on any path:
- the block end;
break/continueout of it;return(after the return value is computed).
Deferred statements and local destructions run in reverse order of their position, so a defer runs before the locals declared before it are destroyed, and after the locals declared after it. s may not contain return, break, continue or another defer (N0254).
15. Enums and match (M2d)
enum Shape {
Empty,
Circle(i64),
Rect(i64, i64),
}
fn area(s: Shape) -> i64 {
match (s) {
Shape.Empty => { return 0; }
Shape.Circle(r) => { return 3 * r * r; }
Shape.Rect(w, h) => { return w * h; }
}
}15.1 Declarations and values
enum E { V1, V2(T1, …), … }at the top level. Variants are unique (N0228) and have zero or more positional payload types: any non-reference, non-voidtype (E3 applies to payloads too). An enum must not contain itself by value (N0227).- Values.
E.Vfor a variant without payload;E.V(e1, …)with exactly the payload arity (N0209). Eacheiis checked against its payload type. - Copy and drop. An enum is Copy iff every payload type is Copy. It needs drop iff some payload needs drop; destroying it destroys the payloads of the active variant. Enums have no
dropblock (v0). - Layout.
- A tag (
u8for ≤ 256 variants, elseu16/u32), then a union of the variants' payloads, each laid out like a struct. - Size and alignment follow C's rules for
struct { tag; union { … }; }. The generated C asserts them. - Nova does not reorder or niche-optimise (v0).
- A tag (
15.2 match
MatchStmt = 'match' '(' Expr ')' '{' Arm* '}'
Arm = Pattern '=>' Block ','?
Pattern = '_' | IntLiteral | '-' IntLiteral | 'true' | 'false' | 'null'
| QualifiedName ('(' Payload (',' Payload)* ')')? -- E.V, E.V(x, _), Ok(E.A(x)) (M11)
Payload = Binding | Pattern -- a nested pattern: M11 (G1)
Binding = Identifier | 'var' Identifier | '_' -- 'var': M3c (§18.4)A payload that is a single identifier is a binding; Some(…), Ok(…), Err(…) and E.V… there are patterns.
- This section's
matchis a statement; §15.3 has the expression form. Arms are tried in order; the first that matches runs its block. - Scrutinee types:
- an enum: variant patterns;
- an integer: literal patterns of that type;
bool:true/false;- or a reference to one of these.
- Exhaustiveness (N0256): enum matches must cover every variant or end with
_. Integer matches need_.boolmatches need both values or_. An arm that can never match (a repeated variant or literal, or anything after_) is N0257. - Bindings. A variant pattern binds each named payload to a new immutable local in the arm:
- Scrutinee of type
E(a value): the binding is a copy of the payload. Since M3c a non-Copy payload is moved out, and the match consumes its scrutinee (§18.4). - Scrutinee of type
borrow E/borrow mut E: the binding is aborrow T/borrow mut Tto the payload, and the borrow checker treats it like any reborrow of the scrutinee. _binds nothing.
- Scrutinee of type
- The scrutinee is evaluated once. When it is a place it is not moved (only Copy payloads are read). A temporary scrutinee lives until the end of the
matchstatement. - Nested patterns (M11, G1). In a payload's position, a pattern instead of a binding matches the payload itself: a variant pattern on an enum payload (
Ok(E.A(x)),Some(Some(_))), a literal on an integer orboolpayload (Some(3)), ornullon an optional payload (Ok(null)). Each is checked against the payload's type exactly as a wholematchis checked against the scrutinee's, at any depth.- Order. Arms are tried in order. Within an arm, the outer variant is tested first, then each nested pattern, left to right and depth first. Testing only reads tags and values: an arm that does not match moves nothing.
- Bindings inside nested patterns follow the rules above. On a value scrutinee they copy, or move (§18.4). On a reference scrutinee they are references to the nested payload.
- A payload that is itself a reference (
borrow E) takes only a binding or_, not a nested pattern. Matching through it is a separatematchon that reference (N0205 otherwise). - Exhaustiveness and unreachable arms are decided over the whole patterns. A match is exhaustive when every value of the scrutinee's type matches some arm: N0256, whose message names one value that matches none, such as
Ok(E.B). An arm is unreachable when every value it matches is matched by an earlier arm: N0257. The decision is the usefulness algorithm of pattern-matching compilers (Maranget, Warnings for pattern matching, 2007), with each enum's variants,true/false,null/SomeandOk/Erras the constructors. Integer literals never cover their type: an integer position needs a binding or_somewhere.
15.3 if and match expressions (M11, G5)
let sign = if (x < 0) -1 else if (x == 0) 0 else 1;
let name = match (shape) {
Shape.Circle(_) => "circle",
Shape.Square(side) => if (side > 10) "big square" else "square",
Shape.None => { return; },
};IfExpr = 'if' '(' Expr ')' Branch 'else' (IfExpr | Branch)
MatchExpr = 'match' '(' Expr ')' '{' (ValueArm (',' ValueArm)* ','?)? '}'
ValueArm = Pattern '=>' Branch
Branch = Expr | Block- Where. In expression position (an initialiser, an operand, an argument, a
returnvalue, …). Aniformatchthat begins a statement is the statement form (§9, §15.2), as before. Anifexpression needs itselse(N0100 otherwise). - Branches. A branch is an expression, the value of the whole when that branch is taken, or a block that cannot complete (§9): one that ends in
return,break,continueor a call of type never, such as{ return; }or{ Process.exit(1); }. A block that can complete has no value: N0235. (A block that ends in a value, as in Rust, is not part of G5.) - Extent. An expression branch extends as far right as an expression can:
if (c) 1 else 2 + 3isif (c) 1 else (2 + 3). Write parentheses to use anifexpression inside a larger one:(if (c) 1 else 2) + 3. Value arms are separated by commas; a block arm needs none. - Type.
- With an expected type (§5), every expression branch is checked against it, and the whole has that type.
- Without one, the first expression branch that is neither flexible (§5) nor
nullis checked first, and its type is the type of the whole; the other branches are checked against it. If every branch is flexible, the type is its literals' default:f64if any is a float literal (§35.2), elsei32. A branch of type never (and every block branch) fits any type. If no branch is an expression, the whole has type never.
- Evaluation. The condition (or scrutinee) is evaluated once, then exactly one branch.
matchexpressions follow §15.2 in everything else: the patterns, exhaustiveness (N0256) and unreachable arms (N0257), bindings by copy, move or reference, and consuming a value scrutinee (§18.4). - Ownership. A branch's value is moved (or copied) into the result. A move in one branch leaves the place maybe-moved after the whole, as after an
ifstatement (§14.4). A reference produced by a branch carries that branch's loans, so the result carries the loans of every branch that can produce it (lifetimes.md). - Temporaries made while evaluating a branch end when the branch ends, as at the end of a statement (
lifetimes.md§6). A reference to one cannot be the branch's value (N0243).
16. Arrays, slices and for (M2d)
16.1 Types
[T; N]is a fixed array ofNelements.Nis an integer literal (no suffix orusize) with 1 ≤ N ≤ 2^48 − 1, and the array is at most 2^31 − 1 bytes. It is laid out asNconsecutiveTs: sizeN × size(T), the alignment ofT.- v0 restriction:
Tmust be a Copy type, so arrays are Copy and never need drop. Arrays of move types need element-wise move tracking (future). Slices may have move-type elements since M6b (§21.3). - Otherwise arrays may appear anywhere a value may: locals, parameters, results, struct fields, enum payloads and other arrays. Constants of array type are not supported yet (N0216).
- v0 restriction:
Slice<T>/SliceMut<T>are borrowed views(pointer, length)of a run ofTs in an array or another slice. They follow every rule for references (§13), with the same scoping:- only in parameters,
letlocals with an initialiser, and return types with a source list; - never in fields, payloads, array elements, constants or
varlocals (N0246); Slice<T>is Copy, and each use of aSliceMut<T>reborrows it;SliceMut<T>coerces toSlice<T>;- a slice holds a loan of the whole array (or slice) it was taken from (
lifetimes.md); - a function returning a slice has no
borrow(...)source list: it must have exactly one reference (or slice) parameter, which the result is derived from (§13.5's default rule); - the element type follows the array rules: Copy, never a reference.
- only in parameters,
- Internally,
Slice<T>isborrow [T]andSliceMut<T>isborrow mut [T], with[T]an unsized array type that only exists behind a slice. That is why every reference rule applies.
16.2 Values
[e1, …, en]: all elements have the same type (the expected element type, or the first element's), n ≥ 1.[v; N]: N copies ofv.a.lengthis ausize: the constantNfor arrays, the stored length for slices. For an array,amust be a place (a variable, field or element), because the constant would skip evaluating it (N0216).
16.3 Indexing and slicing
a[i]:ais an array or slice place, or a reference to an array;iis ausize(a flexible literal becomesusize);i ≥ a.lengthpanics ("index out of bounds", reported at the index expression);a[i]is a place: it can be read (a copy), assigned (if mutable: an array rooted at avaror reached throughborrow mut, or aSliceMut), and borrowed.
a[lo..hi],a[lo..],a[..hi],a[..]give aSlice<T>of elementsloup to but not includinghi(default0anda.length).lo ≤ hi ≤ a.lengthis checked; otherwise the program panics.borrow mut a[lo..hi]gives aSliceMut<T>and needsato be mutable. As a call argument for aSliceMut<T>parameter,mut a[lo..hi]means the same.- Slicing needs a place (N0247): slice a local, not a call result.
- Evaluation order: the array place (with its own indexes), then
lo, thenhi. In an assignmenta[i] = v, the target's indexes are evaluated and checked beforev. - The borrow checker treats all elements of an array as overlapping (no index reasoning in v0). Borrowing
a[0]and then writinga[1]while the borrow is live is rejected.
16.4 for
The header is parenthesised, like while and match, so a name before the body is never read as a struct literal.
for (i in lo..hi) { … }:itakes each value fromloup to but not includinghi. Both bounds are evaluated once, have the same integer type (flexible literals adapt), andiis immutable in the body.for (x in seq) { … }:seqis an array place, a slice, or a reference to an array.xis a copy of each element from index 0 upward. The element is read when the loop reaches it, and the length is read once at the start.Non-Copy elements (M11, G2): when the element type
Tis not Copy,xis aborrow Tto the element, in place, instead of a copy (which could not be made: N0251). This is the rule of amatchon a reference (§15.2). The borrow checker treats it as a shared borrow of that element for as long asxis used. While it lives, the sequence cannot be changed (§13.4, §23.1). A Copy element is still copied, as before.breakandcontinuebehave as inwhile.continueproceeds to the next element.The loop variable is immutable. It has the bounds' type for a range, and the element type for a sequence (or
borrow T, above). Like every local, it cannot shadow another (N0203).
16.5 Bulk operations (M9b)
Two methods write a whole slice in one operation. A program computes the same thing with them as with the element loop they replace; they exist so that the copy costs one check instead of one per element.
fn copy(out: SliceMut<u8>, to: usize, src: Slice<u8>, from: usize, n: usize) {
out[to..to + n].copyFrom(src[from..from + n]); // two range checks, one length check, one copy
}d.copyFrom(s), withd: SliceMut<T>ands: Slice<T>(aSliceMut<T>coerces): ifs.length != d.lengththe program panics ("slice lengths differ", reported at the call) and writes nothing. Otherwise eachd[i]becomess[i], as ifswere first copied to a temporary. In safe code the two cannot overlap, becauses's loan would conflict withd's (N0241, N0242); ifunsafecode makes them overlap (§27.3,Slice.fromRaw), the result is still defined.d.fill(v), withv: T: every element ofdbecomesv. Nothing is checked.Tmust be Copy (N0259), because both methods copy elements.- The receiver is a
SliceMut<T>(a parameter, a local or a call result; reborrowed, like any use of aSliceMut), or a slicing expressiona[lo..hi]in any of its forms, which is then taken asborrow mut a[lo..hi]. That needsato be mutable (N0248), asmut a[lo..hi]does for aSliceMutargument. ASlice<T>receiver is N0211. Arrays and vectors are written through a slice:a[..].fill(0),v.viewMut().fill(0). - Evaluation is left to right: the receiver (its place, bounds and range check), then the argument, then the length check, then the writes. The receiver's loan starts before the argument is evaluated: these calls are not two-phase (B5 applies only to
borrow mut thismethods of a place). So the argument cannot read the destination:s.fill(s[0])is N0242, andlet v = s[0]; s.fill(v);is the fix. - The result is
void. - Not provided in M9b: a copy within one slice whose ranges may overlap, comparisons, searches.
d.swap(i, j)(M11 G8c), withi, j: usize: exchanges elementsiandjof aSliceMut<T>(or of a vector, which lends its elements mutably).Tmay be any element type, Copy or not: the elements are moved, not copied. Both indices are checked first (panic "index out of bounds", reported at the call), and nothing changes if either is out of range.i == jchanges nothing. Effects: none.
Diagnostics: N0259 (an element type that is not Copy, an invalid length, or a too-large array). The other errors reuse N0205, N0211, N0216, N0230, N0246, N0247 and N0248 (diagnostics.md).
17. Results and error propagation (M3a)
enum ParseError { Empty, BadDigit(u8), Overflow }
fn digit(c: u8) -> Result<u32, ParseError> {
if (c < 48u8 || c > 57u8) { return Err(ParseError.BadDigit(c)); }
return Ok((c - 48u8) as u32);
}
fn twoDigits(a: u8, b: u8) -> Result<u32, ParseError> {
return Ok(digit(a)? * 10 + digit(b)?);
}Result, Ok, Err and AllocError are built in (ADR 0003). User generics come in M6; the names below then become library items with the same meaning.
- Splitting (M13 G5):
s.splitAt(mid)gives aSliceHalves<T>whoseleftiss[..mid]andrightiss[mid..].s.splitAtMut(mid), on aSliceMut<T>or a vector, gives aSliceHalvesMut<T>of twoSliceMut<T>that do not overlap, so both may be written at once, at a split known only at run time. Both panic ("slice range out of bounds") ifmidis past the end; the panic names lib/core's line, not the caller's. The halves borrowsfor as long as either lives. They are lib/core code (SliceSplitting<T>,lib/core/slices.nova).
17.1 Types
Result<T, E>is a built-in enum with two variants, in this order:Okwith payloadT, andErrwith payloadE. Everything in §15.1 applies: layout, Copy iffTandEare Copy, needs drop iff either does, and only the active variant's payload is destroyed.Tmay bevoid, in which caseOkhas no payload.Emust not bevoid. Neither may be a reference or slice (N0246, E3); arrays, structs, enums and results are fine.- A type contains
TandEby value:struct S { r: Result<S, i32>; }is recursive (N0227). AllocErroris a built-in enum{ OutOfMemory }(Copy). The namesResult,Ok,ErrandAllocErrorcannot be redeclared as items (N0201).- Two
Resulttypes are equal iff theirTand theirEare equal.
17.2 Values
Ok(e),Ok()(forT = void) andErr(e)need an expectedResulttype that fixesTandE. That type comes from a declared variable type, a parameter type, a return type (return Ok(x);), or a field or payload type. Without one, it is N0260 ("write the type").- The payload is checked against
TorElike a call argument against its parameter (§5).
17.3 Matching
- Patterns
Ok(x),Ok(_),Ok(forT = void),Err(x)andErr(_)are written without a type prefix. The bindings and exhaustiveness rules of §15.2 apply unchanged: a non-Copy payload is bound by matching onborrow r(N0258 otherwise).
17.4 e? and e!
Postfix = Primary ( … | '?' | '!' )* -- '!' is postfix only when it follows an operande?(propagate).emust have aResult<T, E>type (N0262). The enclosing function must returnResult<U, E>with the sameE(N0261);Fromconversions come in M6.- If
eisOk(v), the value ofe?isv(of typeT;voidifTisvoid). - If
eisErr(x), the function returnsErr(x)at once, exactly like areturnstatement. The statement's temporaries end, then every scope is left (drops and deferred statements), innermost first. Likereturn,?is not allowed in a deferred statement (N0254).
e!(unwrap).emust have aResult<T, E>type (N0262). The value isvforOk(v);Err(_)panics with "unwrapped an Err value". Panic is abort, so nothing is destroyed.- In both cases
eis evaluated once and consumed. The payload moves out of it (or is copied); nothing else oferemains to be destroyed. - As an expression statement:
f()?;andf()!;are allowed when the operand is a call, like calls themselves (N0225 otherwise). - Precedence: postfix, like a call or member access:
parse(a)?.valueand-x?work as expected (-applies tox?).!directly followed by=lexes as!=. Writer! == 1with a space.
17.5 Discarded values (correction to §14.5)
- An expression statement whose value needs drop (for example a call
make();returning aRes) destroys the value at the end of the statement, like any other temporary. Before M3a the value was never destroyed: an O4 violation, found while specifying 3a.
Diagnostics: N0260 (no expected Result type for Ok/Err), N0261 (? in a function whose return type is not a Result with the same error type), N0262 (? or ! on a non-Result).
18. Boxes and allocators (M3b)
struct Account {
id: u64;
balance: i64;
fn deposit(borrow mut this, amount: i64) { this.balance += amount; }
}
fn open(id: u64) -> Result<Box<Account>, AllocError> {
return Box.create(Account { id: id, balance: 0 });
}
fn main() -> i32 {
var acct = open(7)!;
acct.deposit(100); // through the box
return 0;
} // the box is dropped: Account destroyed, memory freedBox, SystemHeap and FailingAllocator are built in (ADR 0003).
18.1 Allocator types
SystemHeapandFailingAllocatorare built-in, field-less, Copy structs. They are the only allocator types in M3; user allocators come with interfaces in M6 (memory-model.md§3.2). Their values are written as struct literals:SystemHeap {}.SystemHeapis the platform heap (malloc/freein hosted and CosmoOS userland builds).FailingAllocatorallocates from the platform heap, but can be told to fail, for testing error paths in the language itself:FailingAllocator.failAfter(n: u64): the nextnallocations throughFailingAllocatorsucceed, and every later one fails;FailingAllocator.reset(): allocations never fail (the initial state). The countdown is process-wide; frees are never affected.
- Their names, and
Box, cannot be declared as items (N0201).
18.2 Box<T, A>
Box<T, A>owns exactly one value of typeTin memory obtained from allocator typeA(H1).Box<T>meansBox<T, SystemHeap>.Amust be an allocator type (N0263).Tis any value type exceptvoidand references or slices (N0246).- A box is a move type that needs drop, and is one pointer wide (8 bytes on LP64). Destroying it destroys
T(its drop glue), then returns the memory toA. - A type does not contain the target of a
Boxby value, so recursive types are built with boxes:enum List { Nil, Cons(i32, Box<List>) }is allowed (no N0227). - Creation (built-in functions):
Box.create(v):Result<Box<T>, AllocError>;Box.createIn(a, v)withaa value of allocator typeA:Result<Box<T, A>, AllocError>.Tis the type ofv. An expected typeResult<Box<X, _>, _>provides the type hint forv(soBox.create(5)can make aBox<u8>).vis moved into the call. If the allocation fails,vis destroyed and the result isErr(AllocError.OutOfMemory).
- Access through a box works like access through a reference (§13.3), with the box's own mutability:
- fields
b.f, elementsb[i]and method callsb.m()reachT(auto-deref); - the contents are mutable iff the box is: the box variable is
var, or the box itself is reached throughborrow mutor another mutable place.b.f = 1needs avar b; - a
Box<T>argument for aborrow Tparameter borrows the contents, andmut blends them for aborrow mut Tparameter; - where a Copy
Tis expected, aBox<T>is read through (auto-deref); - a
borrow Box<T>converts toborrow T, and aborrow mut Box<T>toborrow mut Torborrow T: a reborrow of the contents (for example, a payload bound by reference in a match); borrow bborrows the box itself (borrow Box<T>).
- fields
- Borrowing rules: a place inside a box is part of the box. Moving, overwriting or dropping the box while a loan of its contents is live is rejected (N0240, N0243), as for any place and its fields. (Unlike a reference going out of scope, a box going out of scope destroys what it points to.)
- Moving out of a box's contents: see §18.3 (M3c).
- No null boxes exist. A
Boxis never Copy and has no==.
Diagnostics: N0263 (an allocator argument that is not an allocator type).
18.3 Moving out of a box (M3c)
- A box held in an owned place owns its contents as a part of that place: a local, or a field path of one, not reached through a reference. Its fields may be moved out (
let l = n.left;,move n.left), exactly like fields of a struct local (§14.3–14.4):- the box cannot be used as a whole again (moved, passed, matched, borrowed) until every moved-out field is re-initialised by assignment (N0250);
- a field of a
Twith adropblock cannot be moved out (N0252); - through a reference, a box's contents are as immovable as anything else (N0251).
- Destroying a partially moved box destroys the fields still initialised (in declaration order), then returns the memory to the allocator.
T's drop glue does not run, andThas nodropblock by the rule above. - There is no way to move the whole contents out in v0 (
Twould need an unbox operation, M6).
18.4 Moving payloads out in match (M3c; replaces N0258)
Binding = Identifier | 'var' Identifier | '_'var xmakes a mutable binding;xan immutable one. Both may bind any payload type.- Consuming match. If the scrutinee is a value (not a reference) and some arm binds a non-Copy payload, the match consumes its scrutinee:
- the scrutinee is moved when the match starts. A place scrutinee is left uninitialised, as by
move; ausingbinding cannot be consumed (N0253); - in an arm whose pattern lists payloads (
E.V(…)), every payload of the variant moves out: named ones into their bindings, and_ones into hidden locals of the arm. Payloads not bound by name are therefore destroyed when the arm ends, before the bindings' own scope ends. A payload matched by a nested pattern (M11) moves into a hidden local too, and the same rule applies to it: if its pattern lists payloads, they move out of it in turn; - in any other arm (
_,E.Vwithout a list, a variant without payload), the value is destroyed when the match statement ends.
- the scrutinee is moved when the match starts. A place scrutinee is left uninitialised, as by
- Otherwise (Copy payloads only, or a reference scrutinee) the rules of §15.2 apply unchanged.
- N0258 is no longer reported (since M3c).
18.5 Destroying deep structures (R-9)
- A box's drop glue is recursive in general: destroying a
Tdestroys its fields, which may own further boxes. For a recursive type that would use stack in proportion to the structure's depth. - Tail iteration. The compiler finds the tail of
T: the last field (in destruction order) that needs drop, followed through structs, and through enum variants' last payloads, until it reaches aBox<T>again. If there is one,Box<T>'s glue becomes a loop:- destroy everything of the current
Texcept the tail, in the normal order; - take the tail box's pointer (null for variants that end elsewhere);
- free the current memory, then continue with the tail's contents.
- destroy everything of the current
- Only the moment the memory is freed changes: the current node is freed before the tail is destroyed instead of after. No program can observe that difference; every
dropblock runs in the order §14.5 defines. - Consequence: lists (
Cons(x, Box<List>)) are destroyed in constant stack depth. Trees use stack in proportion to the number of non-tail steps on a path (the left children in{ left, right }). Balanced trees therefore use logarithmic depth, and a left-degenerate tree still recurses (R-9 records this).
19. Regions (M4)
Arena, and in later increments FixedBufferAllocator, ScratchFrame, Pool<T> and PoolBox<T>, are built in (ADR 0004). The safety rules are R1–R4 (ownership.md): an allocation borrows its region.
19.1 Scoped types (M4a)
A type is scoped if a value of it may hold loans:
- references and slices (§13, §16);
Result<T, E>whoseTorEis scoped (a scoped result);- the scoped region values:
FixedBufferAllocator(§19.4),ScratchFrame(§19.5) andPoolBox<T>(§19.6).
Every rule for references applies to scoped types (N0246):
- they may appear only in parameters, locals with an initialiser, and return types;
- never in fields, payloads of user enums, array elements, box contents or constants.
References and slices must be let locals, because assigning to a reference writes through it. Since M4c, other scoped values (scoped results and region values) may be var: assigning one replaces it, like any value. For the borrow checker, the local then holds the loans of every value it is assigned (a sound over-approximation).
A function returning a scoped type needs return sources: the default rule (one reference parameter) or a borrow(...) list (§13.5). In this rule, "reference parameter" now means "parameter of a scoped type", and the receiver this counts when it is borrowed.
The borrow checker treats every local of a scoped type as holding loans (lifetimes.md §3). Moving or copying a scoped value moves or copies its loans, as for references. A scoped result's loans reach the Ok payload through ?, ! and match.
19.2 Arena (M4a)
fn process(input: Slice<u8>) -> Result<u64, AllocError> {
using arena = Arena.create(SystemHeap {}, 65536u64);
let counts = arena.allocArray(256usize, 0u32)?; // SliceMut<u32>, a loan of `arena`
for (b in input) { counts[b as usize] += 1u32; }
var distinct: u64 = 0;
for (c in counts) { if (c != 0u32) { distinct += 1; } }
return Ok(distinct); // a u64, not arena memory
} // the arena frees all its memory hereArenais a built-in move type that needs drop. It is not scoped: it owns its memory, which it obtains from a free-standing allocator.Arena.create(a, chunkSize), withaan allocator value (SystemHeap {},FailingAllocator {}) andchunkSize: u64, makes an empty arena. It cannot fail: memory is obtained in chunks ofchunkSizebytes when needed. An allocation larger than a chunk gets a chunk of its own.Allocation (R1). Both methods take
borrow thisand return a scoped result that carries a shared loan of the arena:arena.alloc(v):Result<borrow mut T, AllocError>, withvmoved into fresh arena memory;arena.allocArray(n, fill)withn: usize:Result<SliceMut<T>, AllocError>,ncopies offill.Tmust be Copy.nmay be 0 (an empty slice);n × size(T)beyond the address space isErr(OutOfMemory).
The returned references are mutable: each allocation is fresh memory that nothing else refers to.
R2:
Tmust not need drop, and must not be scoped (no references or slices in arena memory, E3). Both are N0264.arena.reset()takesborrow mut thisand releases all memory. Because it is a mutable borrow, it is rejected while any allocation is live (B1/B2). Dropping or moving the arena is likewise rejected while allocations are live.Destroying an arena releases all its memory at once (bulk release). Since no allocation needs drop (R2), this equals destroying each allocation.
Failure: if a chunk cannot be obtained from the allocator, the allocation returns
Err(AllocError.OutOfMemory); the arena stays usable.
19.3 What cannot escape (M4a, the R1 consequences)
- An allocation cannot be returned from the function that owns the arena: N0244, like any reference to a local.
- It cannot be stored in a box, a struct, an enum payload or an array: those cannot hold scoped values (N0246).
- It cannot outlive the arena: a
reset, drop, move or scope end of the arena while the allocation is live is N0240–N0243.
Diagnostics: N0264 (a type that a region cannot hold).
19.4 FixedBufferAllocator (M4b)
fn countWords(text: Slice<u8>) -> Result<u32, AllocError> {
var storage = [0u8; 4096];
let fba = FixedBufferAllocator.over(mut storage); // a mutable loan of `storage`
let spans = fba.allocArray(64usize, Span { start: 0u32, len: 0u32 })?;
...
}FixedBufferAllocator.over(mut s)makes an allocator over caller-provided bytes.sis a mutable place of type[u8; N], or aSliceMut<u8>. The allocator holds a mutable loan of the storage, so the storage cannot be read or written while the allocator or any of its allocations is live.FixedBufferAllocatoris scoped (§19.1): aletlocal, a parameter or a return type with sources, never stored. It is a move type without drop; nothing is freed.alloc(v)andallocArray(n, fill)work as forArena(§19.2), including R2 (N0264). Each allocation is aligned within the storage. When the remaining bytes are not enough, the result isErr(AllocError.OutOfMemory); the storage is never grown. There is noreset: to reuse the storage, let the allocator end and make a new one.- An allocation's loans include the allocator's loan of the storage, so the storage stays borrowed while any allocation is live, even after the allocator's last use.
19.5 Scratch frames (M4b; ADR 0004)
fn render(n: usize) -> Result<u64, AllocError> {
using frame = Scratch.enter(); // a root frame
let buf = frame.allocArray(n, 0u8)?; // scoped to `frame`
{
using inner = frame.enter(); // a nested frame borrows `frame` mutably
let tmp = inner.alloc(1u64)?;
...
} // `inner`'s memory is released here
return Ok(buf.length as u64);
} // `frame`'s memory is released hereScratch memory is a runtime-owned stack of heap chunks (
SystemHeap); it is not the native stack.Scratch.enter()makes a root frame. Only one root frame may be live at a time (per thread; v0 is single-threaded): entering a second one panics ("a scratch frame is already active"). Code that needs scratch memory below a frame receives the frame (borrow ScratchFrame) and nests.frame.enter()makes a nested frame. For the borrow checker a frame has two parts: its memory, which allocations borrow, and its allocation right, which every allocation call uses briefly. The rules:- an allocation result carries a shared loan of the frame's memory;
- each
alloc/allocArraycall needs a shared borrow of the allocation right for the call only; enter()takes a mutable loan of the parent's allocation right, which the nested frame carries.
While a nested frame is live, the parent therefore cannot allocate or nest again (B1), but allocations already made from the parent stay usable: they lie below the nested frame's memory. Destroying or moving a frame conflicts with both parts. This makes frames strictly LIFO.
Calling a frame method needs a frame that is a place (
using frame = …): N0247 otherwise.ScratchFrameis scoped (§19.1) and needs drop. Destroying a frame releases everything allocated from it (and from frames nested in it), and returns chunks to the heap.alloc(v)andallocArray(n, fill)work as forArena, including R2. A failure to obtain a chunk isErr(AllocError.OutOfMemory).Frames are bound with
using(R3; §14.6): nesting borrows the frame mutably, and a scoped value cannot be avar. Since frames are scoped, they cannot be returned from the function that entered them.
19.6 Pool<T> and PoolBox<T> (M4c)
struct Particle { x: i32; v: i32; }
fn simulate() -> Result<i32, AllocError> {
using pool: Pool<Particle> = Pool.create(SystemHeap {}, 64u32);
var a = pool.take(Particle { x: 0, v: 2 })?; // PoolBox<Particle>: a loan of `pool`
let b = pool.take(Particle { x: 10, v: -1 })?;
a.x += a.v; // through the box
return Ok(a.x + b.x);
} // b, a, then the pool are destroyedPool<T>is a built-in allocator for up tocapacityvalues of one typeT.Pool.create(a, capacity), withaan allocator value andcapacity: u32, makes an empty pool.Tcomes from the expected type (let pool: Pool<T> = …); without one it is N0260.- It cannot fail: the slots are allocated from
aat the firsttake. Pool<T>is a move type that needs drop; destroying it returns the slots toa. It is not scoped.
pool.take(v)takesborrow thisand returnsResult<PoolBox<T>, AllocError>:vmoves into a free slot. The result isErr(AllocError.OutOfMemory)when all slots are taken, or when the slots cannot be allocated;vis then destroyed.PoolBox<T>owns one slot and its value, like aBox(§18.2):- auto-deref, the box's own mutability, borrowing its contents, and moving fields out of it (§18.3) all work as for
Box; - it is a move type that needs drop: destroying it destroys the
T(its drop glue), then returns the slot to the pool. Values are destroyed one by one, soTmay need drop: R2 does not apply to pools; - it is scoped: it carries the shared loan of the pool from
take. It cannot be returned past the pool, stored in a field or box, or outlive the pool (§19.3); Tmust not be scoped (E3; N0264).
- auto-deref, the box's own mutability, borrowing its contents, and moving fields out of it (§18.3) all work as for
- Destroying the pool while a
PoolBoxfrom it is live is rejected (B2), so every slot is free when the pool is destroyed.
20. Generics (M6a)
struct Pair<T> {
first: T;
second: T;
fn swap(this) -> Pair<T> => Pair { first: this.second, second: this.first };
}
enum Maybe<T> { Nothing, Just(T) }
fn largest<T>(p: borrow Pair<T>, pick: bool) -> borrow T {
if (pick) { return borrow p.first; }
return borrow p.second;
}
fn main() {
let p = Pair { first: 1, second: 2 }; // Pair<i32>, inferred from the fields
let q = p.swap();
let m: Maybe<u8> = Maybe.Just(7u8);
let r = largest(p, true); // T = i32, inferred from the argument
let s = largest<i32>(q, false); // explicit type argument
}20.1 Declarations
TypeParams = '<' Identifier (',' Identifier)* '>'
FunctionDecl = 'fn' Identifier TypeParams? '(' … ')' …
StructDecl = 'struct' Identifier TypeParams? '{' … '}'
EnumDecl = 'enum' Identifier TypeParams? '{' … '}'- A top-level function, struct or enum may declare type parameters. Inside the declaration, each parameter is a type. Methods of a generic struct see the struct's parameters; methods cannot declare parameters of their own in v0.
- Names must be distinct, and must not shadow items or built-in types (N0201).
20.2 Using generic items
- Types:
Pair<i32>,Maybe<Pair<u8>>. A generic struct or enum must always be given all its type arguments in a type (N0204). - Calls:
largest<i32>(p, true)gives the arguments explicitly.largest(p, true)infers them from the argument types: each parameter type is matched against its argument's type. An integer literal argument isi32unless the parameter type fixes it. A parameter that the arguments do not determine is N0265 ("write the type arguments"). - Values:
Pair { first: 1, second: 2 }infersTfrom the field values, or takes it from the expected type.Pair<u8> { … }gives it. LikewiseMaybe.Just(7)infers from the payload, andMaybe.Nothingneeds an expected type or explicit arguments:Maybe<u8>.Nothing. - Static functions:
Pair<i32>.make(…).
20.3 Checking generic code once
- A generic declaration is checked once, for all type arguments. Each type parameter is an opaque type, about which only what holds for every type is known:
- it is a move type that may need drop. A value of it can be moved, borrowed, passed, returned, stored in fields of the declaring type, and matched as a payload;
- nothing else is allowed: no operators, no fields, no methods, no literals, no
==(N0207, N0234, N0205). Interfaces (§21) add operations through bounds.
- Move checking, borrow checking and reachability run on the generic code too. A body that would be wrong for some type argument is rejected at the definition, whether or not it is ever instantiated (for example, using a
Ttwice is N0250). - Generic declarations that are never used produce no C code.
20.4 Instantiation (monomorphisation)
- Every distinct use (
largest<i32>,Pair<Res>) is an instantiation: a copy of the declaration with the type arguments substituted. It is checked again and compiled to C. - An instantiated struct or enum is an ordinary type for every other rule. It is Copy iff its substituted fields or payloads are, and needs drop iff one of them does.
Pair<i32>is Copy;Pair<Res>is a move type. - The layout is that of the substituted declaration. Recursion is checked per instantiation (N0227).
- Instantiation is depth-limited: a chain of more than 64 nested instantiations (polymorphic recursion such as
f<T>callingf<Box<T>>) is N0266.
Diagnostics: N0265 (type arguments that cannot be inferred), N0266 (instantiation too deep).
21. Interfaces (M6b)
interface Shape {
fn area(borrow this) -> i64;
}
struct Square { side: i64; }
impl Shape for Square {
fn area(borrow this) -> i64 => this.side * this.side;
}
fn total<S: Shape>(shapes: Slice<S>) -> i64 {
var t = 0i64;
for (i in 0..shapes.length) { t += shapes[i].area(); }
return t;
}21.1 Declarations
InterfaceDecl = 'interface' Identifier TypeParams? '{' MethodSig* '}'
MethodSig = Attribute* 'fn' Identifier '(' Receiver (',' Param)* ')' ('->' Type)? ';'
ImplDecl = 'impl' Type 'for' Type '{' FunctionDecl* '}'- An interface lists method signatures. Every method has a receiver (
this,borrow this,borrow mut this). An interface may be generic (interface Comparable<T>). Its methods cannot have type parameters of their own, and v0 has noSelftype. impl I for T { … }implements interfaceI(with its type arguments,I<X>) for a concrete typeT: a struct, an enum, or a built-in type such asi32.Tmust not be generic in v0 (impl<T> I for Pair<T>is future work).- The block must define exactly the interface's methods, each with the same receiver kind, parameter types and return type, after the interface's type arguments are substituted (N0267). It may not define other functions.
- Each type may implement each interface instantiation at most once (N0267).
- An
implblock's methods are ordinary methods ofT.x.area()works on anySquare, and their names must not clash withT's other methods (N0228).
- Interfaces are not types: an interface is a bound (§21.2), and since M13 also the
Iof adyn Itype behind a pointer, which dispatches at run time (§37; ADR 0015). - A method signature may carry an effect list,
@effects() fn visit(borrow mut this);, which bounds everyimplof it (§37.3).
21.2 Bounds
- A type parameter may have bounds:
<T: Shape>,<T: Comparable<T> + Copy>. - Inside generic code, a value whose type is
T(or a reference to one) may call the methods ofT's bounds, with the signatures the interfaces give (after substitution). A call that no bound provides is N0234. If two bounds provide the same method name, the call is ambiguous (N0267). Copyis a built-in bound: aT: Copyis a Copy type in the generic code (it can be used twice and stored in arrays) and never needs drop. Only Copy types satisfy it.- At every instantiation (a call, type or value that gives
T), each bound must hold: the argument implements the interface (for a type parameter argument, its own bounds include it), or is Copy forCopy. Otherwise it is N0268, reported where the instantiation is written. - Dispatch is static: an instantiation calls
T'simplmethod directly (monomorphisation).
21.3 Slices of move types (correction to §16.1)
Slice<T>andSliceMut<T>may have any non-scoped element type, not just Copy ones. A slice only views elements: reading one by value still needs a Copy type (N0251 otherwise), but borrowing one (borrow s[i],s[i].method()) does not. Arrays still need Copy elements (§16.1), since an array owns its elements and v0 tracks no per-element moves.
Diagnostics: N0267 (an impl that does not match its interface, a duplicate impl, or an ambiguous bound method), N0268 (a type argument that does not satisfy a bound).
22. Optionals and string views (M6c)
22.1 Optionals T?
fn find(xs: Slice<i32>, wanted: i32) -> usize? {
for (i in 0..xs.length) { if (xs[i] == wanted) { return i; } } // i converts to usize?
return null;
}
fn firstTwo(xs: Slice<i32>) -> usize? {
let a = find(xs, 1)?; // null propagates
return a + 1;
}
fn main() {
let numbers = [3, 1, 2];
match (find(numbers[..], 2)) {
Some(i) => { Console.writeLine(i); }
null => { Console.writeLine("absent"); }
}
let i = find(numbers[..], 3)!; // panics on null
}T?(postfix?in a type) is an optionalT, for any non-voidT. It is a built-in enum with two variants, in this order:null(no payload) andSome(T). Everything in §15.1 applies: it is Copy iffTis, needs drop iffTdoes, and is scoped iffTis (borrow T?).Optional references and references to optionals (M11, G6).
?applies to the whole type before it, soborrow T?is an optional reference,(borrow T)?. A reference to an optional is written with parentheses:borrow (T?),borrow mut (Box<Node>?). A type may be parenthesised anywhere a type is written (grammar.md §2). Diagnostics print the two as written:borrow i32?andborrow (i32?). A reference to an optional is a reference like any other (§13): a match on it binds references to the payload,r == nullreads the referent, andr = vthrough aborrow mut (T?)replaces the optional, as innovafn append(slot: borrow mut (Box<Node>?), value: i32) { match (slot) { Some(node) => { append(mut node.next, value); } null => { slot = Some(Box.create(Node { value: value, next: null })!); } } }Values.
nullneeds an expected optional type (N0260).Some(e)makes a present value; with an expectedT?,eis checked againstT, otherwiseTise's type.- Where a
T?is expected (an initialiser, an argument, a return value, a field), a value of typeTconverts toSome(value)implicitly.
Matching. Patterns
null,Some(x)andSome(_)are written without a type prefix; the rules of §15.2 and §18.4 apply.Comparisons.
o == nullando != nulltest for absence, for anyT(the value is only read, not consumed). Other comparisons of optionals are not defined (N0207).o!is the value, or panics with "unwrapped null".o?is the value, or returnsnullfrom the enclosing function, which must return an optional (N0261).?on an optional in a function returning aResultis N0261 in v0 (language-spec.mdEx11).Layout. In general, like any enum:
{ tag; T }. Since M9e′, an optional whose payload is a pointer that is never null is that pointer alone (the niche layout;language-spec.md§2): null isnull, and any other value isSome. That coversBox<T, A>?, except a pool box (two pointers), andborrow T?andborrow mut T?, except slices and string views (a pointer and a length). Such a pointer is never null:- allocation returns a non-null pointer or fails (
nova_allocgives a zero-sized object one byte); - a stack-placed box points to its slot;
- a reference points to an object, and one made from a raw pointer must be non-null (
unsafe.md§2, U2).
An optional of such an optional (
Box<T>??) has the general layout around the niche one. The layout changes no semantics, and optionals do not cross the C boundary (§28.3), so no C code sees it.Box<Node>?is 8 bytes rather than 16 on a 64-bit target.- allocation returns a non-null pointer or fails (
22.2 StringView
fn greeting(loud: bool) -> StringView {
if (loud) { return "HELLO"; }
return "hello";
}
fn main() {
let s: StringView = "Nova language";
Console.writeLine(s);
Console.writeLine(s.length); // 13 (bytes)
let first = s[0..4]; // "Nova": a view of s
Console.writeLine(first == "Nova");
for (b in s.bytes()) { … } // Slice<u8>
}StringViewis a shared, read-only view of UTF-8 text(pointer, length in bytes). It is scoped likeSlice<T>(§16.1), and Copy.- String literals have type
StringView(their bytes are static data). A literal's view holds no loans. A function may therefore return aStringViewwithout scoped parameters (likegreetingabove). The return check still rejects any view that borrows a local (N0244). - Operations.
s.lengthis the length in bytes (usize);s[i]is the byte ati(u8, bounds-checked);s.bytes()is the same bytes as aSlice<u8>;s[lo..hi](any bound may be omitted) is aStringViewof bytesloup tohi. Besides the range check of §16.3, both bounds must fall on character boundaries (not inside a UTF-8 multi-byte sequence). Otherwise it panics with "string slice not on a character boundary";==and!=compare the bytes;Console.write(s)andConsole.writeLine(s)print it.
for (b in s)is not defined: iterate overs.bytes()(characters come withStringmethods later).
Diagnostics: none new. null without an expected type is N0260; ? and ! on non-optional, non-result values are N0262.
23. Vector<T, A> and String<A> (M6d)
23.1 Vector<T, A>
fn sum(xs: Slice<i32>) -> i32 {
var total = 0;
for (x in xs) { total += x; }
return total;
}
fn main() -> i32 {
var v = Vector<i32>.create(); // no allocation until the first push
v.push(1)!; // Result<void, AllocError>
v.push(v.length as i32)!; // two-phase borrow (B5)
let r = borrow v[0];
// v.push(3)!; // error[N0240]: `v` is borrowed by `r`
Console.writeLine(r);
v.push(3)!; // fine: `r` is dead
Console.writeLine(sum(v)); // a vector argument for a slice parameter
return 0;
}Vector<T, A>owns a growable, contiguous buffer ofTvalues obtained from allocator typeA(H2).Vector<T>meansVector<T, SystemHeap>.Amust be an allocator type (N0263).Tis any value type exceptvoidand scoped types (N0246; references in aggregates are OD-3).- A vector is a move type that needs drop and is never Copy. It is not scoped. Destroying it destroys the elements from index 0 upward, then returns the buffer to
A. - Creation (built-in functions on the type; the type arguments come from the explicit list or, without one, from an expected
Vector<T, A>orResult<Vector<T, A>, _>; N0260 otherwise):Vector<T>.create(): an empty vector. It allocates nothing;Vector<T>.withCapacity(n)(n: usize):Result<Vector<T, A>, AllocError>, an empty vector with room fornelements (n == 0allocates nothing).
- Members:
v.lengthandv.capacity(usize);v[i]is elementi, bounds-checked (panic "index out of bounds"). It is a place, like an element of a slice: it can be read (CopyT), borrowed (borrow v[i],borrow mut v[i]) and assigned (v[i] = xdestroys the old element). Writing needs a mutable vector: avar, or one reached throughborrow mut. Moving an element out is rejected (N0251), as for a slice;v.push(x):Result<void, AllocError>. It appendsx, growing the buffer when it is full (new capacity: twice the old, at least 4). If the allocation fails,xis destroyed, the vector is unchanged, and the result isErr(AllocError.OutOfMemory);v.pop():T?, the last element moved out, ornullif the vector is empty;v.clear(): destroys every element; the capacity stays;v.truncate(n)(M11 G8c,n: usize): ifn < v.length, destroys the elements from indexnup, lowest first, and the length becomesn; otherwise nothing changes. The capacity stays;v.swap(i, j), and the methods taking closures (sortBy,forEach,any,all,retain): §16.5 and §36.4;v.view(): the elements as aSlice<T>, andv.viewMut()as aSliceMut<T>;- a
Vector<T>argument for aSlice<T>parameter is borrowed asv.view(), andmut vfor aSliceMut<T>parameter asv.viewMut()(like a box's contents, §18.2); for (x in v)isfor (x in v.view())(§16.4).
- Calls take
thislike methods:push,pop,clearandtruncatetakeborrow mut this, and their arguments are evaluated before the mutable loan starts (two-phase, B5,lifetimes.md§5).length,capacity,viewand element reads takeborrow this. - Invalidation (H2, acceptance criterion 1 of M6): an element borrow, a view, or anything derived from them is a loan of the vector. While one is live,
push,pop,clear, element writes,viewMut, moving the vector and destroying it are rejected (N0240, N0241, N0243), exactly as for a place and its fields. The vector makes no address-stability promise: growth moves the elements. - No
==, noCopy. - Layout (v0):
{ T *ptr; u64 length; u64 capacity }(24 bytes on LP64). An empty vector's pointer is a valid, aligned, never-dereferenced address (not null), so views of it are valid.
23.2 String<A>
fn greet(name: StringView) -> Result<String, AllocError> {
var s = String.withCapacity(name.length + 7)?;
s.append("Hello, ")?;
s.append(name)?;
return Ok(s);
}
fn main() -> i32 {
let msg = greet("Nova")!;
Console.writeLine(msg.view()); // a StringView borrowed from msg
return 0;
}String<A>owns UTF-8 text in a growable buffer (StringmeansString<SystemHeap>). It is a move type that needs drop, likeVector<u8, A>, but a distinct type.- UTF-8 invariant: text only enters a string through
appendof aStringView, which is always valid UTF-8 (§22.2), so a string's bytes are always valid UTF-8. Its bytes cannot be written:s[i] = bis rejected (N0211), andborrow mut s[i]and mutable slices of a string (N0248). - Creation:
String.create()(no allocation) andString.withCapacity(n):Result<String<A>, AllocError>;String<A>.create()for another allocator. - Members:
s.append(text)(text: StringView):Result<void, AllocError>, two-phase likepush. Growth is as for vectors. On failure the string is unchanged;s.appendNumber(n)(M11, G4c;nof any integer type, or a reference to one): appendsnin decimal, with a-before a negative value and no leading zeros or+:0,-128,18446744073709551615. It isResult<void, AllocError>, two-phase and unchanged on failure, as forappend. An integer literal argument has its usual default type (§4). Any other argument type is N0207;s.clear(),s.length(bytes),s.capacity;s.view(): the text as aStringView, a loan ofs; aStringargument for aStringViewparameter is borrowed the same way;- read-only access as for a view:
s[i](u8),s[lo..hi](aStringView, boundary-checked),s.bytes(); - printing:
Console.writeLine(s)prints the text of a string place.
- Appending a view of the string itself (
s.append(s.view())) is rejected (N0240): the view is a live shared loan ofswhen the call's mutable loan starts. - Layout (v0): as
Vector<u8, A>.
Diagnostics: none new. The errors reuse N0205, N0207, N0209, N0211, N0240–N0251, N0260 and N0263.
24. SlotMap<T, A>, Handle<T> and StableVector<T, A> (M6e)
24.1 SlotMap<T, A> and Handle<T>
struct Node { value: i32; next: Handle<Node>?; }
fn main() {
var nodes = SlotMap<Node>.create();
let a = nodes.insert(Node { value: 1, next: null })!;
let b = nodes.insert(Node { value: 2, next: a })!; // a handle is a plain value, not a loan
match (nodes.get(b)) {
Some(node) => { Console.writeLine(node.value); }
null => { }
}
let removed = nodes.remove(a); // `a` is now stale
Console.writeLine(nodes.get(a) == null); // true: generation mismatch
}SlotMap<T, A>owns values ofTin reusable slots from allocator typeA(SlotMap<T>meansSlotMap<T, SystemHeap>; N0263).Tis any value type exceptvoidand scoped types (N0246). A slot map is a move type that needs drop, and is not scoped. Destroying it destroys the values it holds, then frees its storage.Handle<T>names one inserted value: a slot index and the slot's generation. It is a Copy value, not a loan: it can be stored in structs, vectors and other slot maps (unlike a reference, E3), and it outlives inserts, removals and moves of the map.==and!=compare handles. Handles are only made byinsert.- Operations:
SlotMap<T>.create(): an empty map; it allocates nothing (type arguments as for vectors, §23.1);m.insert(v):Result<Handle<T>, AllocError>, takingborrow mut this(two-phase, B5). If the storage cannot grow,vis destroyed and the result isErr(AllocError.OutOfMemory);m.get(h):borrow T?(a loan ofm),nullifhdoes not name a live value;m.getMut(h):borrow mut T?(a mutable loan ofm; needs a mutable map);m.remove(h):T?, the value moved out (ornull), takingborrow mut this;m.contains(h):bool;m.length: the number of live values (usize).
- Validation: every access compares the handle's generation with the slot's. A removed value's handles never match again: a slot's generation changes on every insert and removal, and a slot whose generation would wrap around is retired instead of reused. A handle used with another map of the same
Tis memory-safe: it yields that map's value in that slot, ornull(a logic error, as with any index). - Invalidation: references from
get/getMutare loans of the map, soinsertandremoveare rejected while one is live (N0241). The map may reallocate its slots: it promises stable identity (handles), not stable addresses. - Layout (v0):
{ slot *ptr; u64 length; u64 capacity; u64 count; u32 free }(40 bytes), each slot{ u32 generation; u32 nextFree; T value };Handle<T>is{ u32 index; u32 generation }.
24.2 StableVector<T, A>
fn main() {
let names = StableVector<i32>.create(); // `let`: push needs only `borrow this`
names.push(10)!;
let first = borrow names[0];
names.push(20)!; // fine: elements never move
Console.writeLine(first);
}StableVector<T, A>is a grow-only sequence whose elements never move (memory-model.md§4): it allocates chunks of 8, 16, 32, … elements and never reallocates them. Ownership, element types,create()and destruction are as forVector(§23.1).sv.push(x):Result<usize, AllocError>(the new element's index). It takesborrow this: appending changes only the length and the chunk table, never an existing element, so element borrows stay valid across it (interior mutability; like region allocation, §19.2). On failurexis destroyed and the vector is unchanged.sv.length(usize) andsv[i](a bounds-checked place, as for vectors: read,borrow,borrow mutand assignment, the last two needing a mutable vector).- No
pop,clear, views orfor(the elements are not contiguous): iterate withfor (i in 0..sv.length). Aborrow mut sv[i]is a mutable loan ofsv, sopushis rejected while one is live (N0241). - Not
Sync(concurrency is later work). Layout (v0):{ void *chunks[32]; u64 length }(264 bytes); elementiis in chunk⌊log2(i + 8)⌋ − 3. At most8 · (2³² − 1)elements; beyond thatpushfails withOutOfMemory.
Diagnostics: none new. The errors reuse N0205, N0209, N0240–N0251, N0260 and N0263.
25. Region parameters (M6x; OD-3, ADR 0006)
enum Kind { Word, Number }
struct Token<region src> { kind: Kind; text: StringView<src>; }
struct Lexer<region src> { input: StringView<src>; position: usize; }
fn next<region src>(lexer: borrow mut Lexer<src>) -> Token<src>? { … } // the token views the input
fn split<region r>(line: StringView<r>) -> Result<Vector<StringView<r>>, AllocError> { … }25.1 Declaring regions
- Structs, enums and functions may declare region parameters before their type parameters:
struct Lexer<region src, T>,fn f<region a, region b, T>(…). In a parameter list,regionfollowed by a name declares a region;regionalone is still an ordinary name. - A method may use its struct's regions.
staticis the built-in region of static data (string literals); it outlives everything.
25.2 Region arguments
- A type with region parameters takes region arguments first, then its type arguments:
Token<src>,Pair<r, i32>. The built-in scoped types take one region optionally:StringView<r>,Slice<r, T>,SliceMut<r, T>, and referencesborrow(r) T,borrow mut(r) T. - Region arguments may be omitted (all of them, never some): in function bodies they are inferred, in signatures elided (§25.6), and in fields they are simply not checked (regions are erased, §25.5).
- A written region must be in scope: a region parameter of the enclosing item or function, or
static. In a function's result,borrow(p)may also name a parameter, as in v0 (§13.5). Otherwise, or with the wrong number of arguments: N0273.
25.3 Scoped types
A type is scoped (§19.1) iff it is a reference, slice or StringView, a scoped region value, a PoolBox, an optional or result with a scoped part, a struct or enum that declares region parameters, an instance of a generic struct or enum with a scoped type argument, or a Vector, StableVector or SlotMap with a scoped element. Handle<T> is not scoped (it holds no loan).
25.4 Where scoped values may be stored
- Fields and payloads: a struct field or enum payload may have a scoped type only if the struct or enum declares at least one region parameter, or the type is a type parameter (its instances are scoped then, §25.3). Otherwise: N0270.
- Generic instances: a user generic type or function may be instantiated with scoped type arguments. If the generic needs an unscoped type there (it boxes
T, makes an array ofT, …), the instantiation is rejected where it is written: N0274. - Collections:
Vector,StableVectorandSlotMapelements may be scoped; so may the value passed to a region'salloc, except a bare reference (N0264: region memory holds values). - Assignment: assigning to a reference local still writes through it (§13.3). Assigning to a field or element of reference type replaces the reference stored there (
token.text = view). - Still rejected (N0246):
Box,Pool, statics and constants, array and slice elements, and written references to references. Region values and pool boxes (FixedBufferAllocator,ScratchFrame,PoolBox<T>) stay out of every place this section opens up (fields, payloads, collection elements, type arguments of user types): they must stay pinned or LIFO in locals.
25.5 Regions are erased
Regions do not change types: Token<a> and Token<b> are the same type, and a value's loans are one set (ADR 0006). Region arguments matter only for the checks of §25.2–§25.4 and for signatures.
25.6 Signatures: flows
Each parameter and the result of a function have flow names:
- the regions written in their types;
borrow(p)in the result contributesp's flow names; - the type parameters their types mention;
- for a parameter of scoped type without written regions: a fresh name of its own (elision);
- for a result of scoped type without written regions: the flow names of the single scoped parameter (v0 elision); with several scoped parameters and none written, N0245 (as in v0).
static is never a flow name. A function's flows are then:
- result ← parameter
iiff they share a flow name: the result may carryi's loans (this generalises v0's return sources, §13.5); - parameter
i← parameterj, foriof typeborrow mut TwhoseTshares a flow name withj: the callee may storej's loans into whatirefers to.
A v0 signature (no regions) has exactly its v0 return sources and no parameter flows.
25.7 Borrow checking (lifetimes.md §3)
With one loan set per value:
- Writing a scoped value into a place of a local (a field, an element, a payload) adds its loans to that local.
- Writing it through a reference adds its loans to the reference and to what the reference was borrowed from, followed back through reborrows, calls (by their result flows) and region allocation. If that reaches a
borrow mutparameterpof the current function, the value's loans must all come from parameters that flow intop(§25.6): otherwise N0272. push,insertandappendwrite their argument through their receiver (rule 2).- At a call, an argument that flows into a
borrow mutparameter adds its loans to what that argument refers to. - In generic code, values whose type mentions a type parameter carry loans like scoped values.
- Destroying a value that is scoped and needs drop is a use of its loans: its
dropmay read them, so they must still be live (N0243 otherwise).
Diagnostics: N0270, N0272, N0273, N0274 (diagnostics.md).
26. Program arguments, files and sockets (C3)
fn main() -> i32 {
let argument = Process.argument(1);
if (argument == null) { Console.writeLine("usage: size FILE"); return 2; }
let path = argument!;
match (File.open(path)) {
Ok(file) => {
var buffer = [0u8; 4096];
var total: u64 = 0;
loop {
match (file.read(mut buffer[..])) {
Ok(n) => { if (n == 0) { break; } total += n as u64; }
Err(e) => { Console.writeLine(e.message()); return 1; }
}
}
Console.writeLine(total);
return 0;
}
Err(e) => { Console.write(path); Console.write(": "); Console.writeLine(e.message()); return 1; }
}
}26.1 Program arguments
Process.argumentCount():usize, the number of command-line arguments including the program name (argument 0).Process.argument(i):StringView?, argumentias a view of static text (no loans), ornullifiis out of range or the argument's bytes are not valid UTF-8 (theStringViewinvariant, §22.2). Arguments are never copied.
26.2 IoError
A built-in enum, like AllocError, with the variants NotFound, PermissionDenied, AlreadyExists, IsDirectory, NotDirectory, InvalidInput, BrokenPipe, ConnectionRefused, AddressInUse, OutOfMemory and Other(i32) (the platform's error number). e.message() is a StringView of static text describing it (the C library's wording for the common cases: "No such file or directory", "Permission denied", …).
26.3 Files and the console
Fileowns an open file. It is a move type that needs drop: destroying it closes the file (an error from closing is ignored). It is not scoped and not Copy.File.open(path):Result<File, IoError>for reading;File.create(path): for writing, created or truncated;File.remove(path):Result<void, IoError>.pathis aStringView; a path that contains a zero byte or is longer than 4095 bytes isInvalidInput.f.read(mut buffer)(buffer: SliceMut<u8>,ftaken asborrow mut this):Result<usize, IoError>, the number of bytes read,0at the end of the file. Interrupted reads are retried.f.write(bytes)(Slice<u8>):Result<void, IoError>; it writes all the bytes or fails.Console.read(mut buffer): reads standard input likef.read.Console.writeBytes(bytes)writes raw bytes to standard output, in order withConsole.write.Console.writeError(text)andConsole.writeErrorLine(text)(text: StringView) write to standard error; standard output is flushed first, so the two keep their relative order.
26.4 TCP sockets
TcpListener.bind(address, port)(address: StringView, dotted IPv4 such as"127.0.0.1"or"0.0.0.0";port: u16,0for any free port):Result<TcpListener, IoError>, listening.listener.port():Result<u16, IoError>, the port it is bound to.listener.accept():Result<TcpStream, IoError>.TcpStream.connect(address, port):Result<TcpStream, IoError>.s.read(mut buffer)ands.write(bytes)as for files;s.shutdownWrite():Result<void, IoError>, so the peer reads the end of the stream.- Both are move types that need drop; destroying one closes the socket.
26.5 Processes
Process.run(program, arguments)(program: StringView, found onPATH;arguments: borrow Vector<StringView>, passed after the program name):Result<i32, IoError>. It starts the program with the caller's standard input, output and error, waits for it, and returns its exit status, or 128 plus the signal number if a signal ended it. Standard output is flushed first, so output keeps its order. A program that cannot be started is an error (NotFound, …); an argument with a zero byte isInvalidInput; if copying the arguments for the C library fails,OutOfMemory.
26.6 Runtime (Profile A and hosted)
Implemented in runtime/hosted/nova_rt_io.c over the C library's open, read, write, close, unlink, socket, bind, listen, accept, connect, getsockname, shutdown, waitpid, and posix_spawnp (hosted) or spawnvp (CosmoOS) (L1, OD-15: public libc functions only). CosmoOS's struct sockaddr (host-order port, 16 address bytes), its spawn API and its wait status encoding differ from POSIX; the runtime selects them at compile time. Generated code only calls functions declared in nova/rt.h, so it still compiles freestanding.
Diagnostics: none new.
27. Raw pointers and unsafe (M8a)
The contract that unsafe code must keep, and what the compiler and the interpreter check, is in unsafe.md. This section gives the typing rules.
/// Safety: `p` points to `n` initialised `u32`s that nothing else writes while this runs.
unsafe fn sum(p: *u32, n: usize) -> u64 {
var total: u64 = 0;
for (i in 0..n) { total += p.add(i).read() as u64; }
return total;
}
/// Safety: the pointer comes from a live array and stays inside it.
fn main() -> i32 {
var a = [1u32, 2, 3, 4];
let p = borrow mut a[0] as *mut u32;
unsafe { p.add(3).write(10); }
Console.writeLine(unsafe { sum(p, a.length) }); // 16
return 0;
}27.1 Types
*T(read-only) and*mut T(writable) are raw pointers toT.Tis any value type, orvoidfor an untyped pointer (*void, C'svoid *). It may not be a reference, slice,StringViewor other scoped type (§19.1, §25.3), ornever(N0281). Since M8d it may be a type parameter: an instantiation with a scoped type argument is then N0274, as for a box ofT.- A raw pointer is one machine address: 8 bytes, aligned to 8 on LP64. It is Copy, needs no drop, holds no loans and is not scoped (U3). So it may appear anywhere a Copy value may: locals, parameters, results, fields, payloads and array elements. Constants of pointer type are not supported (N0216).
*mut Tcoerces to*Twherever a*Tis expected (likeSliceMut<T>toSlice<T>).==and!=compare two pointers of the same type (after that coercion) by address. Ordering comparisons are not defined on pointers (N0207); comparep as usizeinstead.
27.2 Making pointers (safe)
Making and converting a pointer is safe; only using one is not (§27.3).
r as *Tfor a referencer: borrow Torborrow mut T, andr as *mut Tforr: borrow mut T.borrow x as *Tparses as(borrow x) as *T(grammar §2.1). The borrow ends at once: the pointer holds no loan, so later uses ofxare not checked against it.s.ptrfor a slices:*TforSlice<T>,*mut TforSliceMut<T>; for aStringView,*u8. It is the address of element 0 (of where it would be, for an empty slice).p as *Uandp as *mut Ufor any raw pointerp, whatever its pointee and mutability.p as usizefor any raw pointer, andn as *T/n as *mut Tforn: usize(also*void). An integer literal converts too:0 as *mut Tis the null pointer (there is nonullpointer literal:nullis the empty optional, §22.1).p.isNull():bool.
27.3 Using pointers (unsafe)
These are allowed only in an unsafe context (§27.4; N0280 otherwise). p is a raw pointer expression, evaluated once.
p.read(): theTatp, moved out (copied, for a CopyT). Afterwards the memory holds noT: the unsafe code must not read it as one, drop it or let anything else use it as one until it writes a new value there (unsafe.mdU2). A type holding C state (§33) is N0282, since C state never moves. Since ADR 0027; before it,Thad to be Copy.p.write(v): movesvtop(copies, for a CopyT) without reading or destroying what was there.pmust be a*mut T(N0282). It returnsvoid. For a type with@placedmethods (§33.3) the store is a placement: they run atpafterwards, as forBox.create, andvmay be the struct literal without its C state thatBox.createtakes (§33.2).p.readVolatile()andp.writeVolatile(v)(§29.1) still need a CopyT.p.add(n)andp.sub(n)(n: usize) andp.offset(n)(n: isize): the pointernelements (n × size(T)bytes) after (sub: before)p, with the type ofp. The arithmetic wraps modulo 2^64; it never panics.Ptr.toBorrow(p): aborrow Tto theTatp;Ptr.toBorrowMut(p): aborrow mut T, forp: *mut T(N0282 for*T). The reference holds no loans (U3): the borrow checker cannot protect it, so the unsafe code that makes it must bound its use, typically by returning it from a function whose signature ties it to a parameter (§13.5).Slice.fromRaw(p, n): aSlice<T>of thenelements fromp;SliceMut.fromRaw(p, n): aSliceMut<T>, forp: *mut T. LikePtr.toBorrow, the slice holds no loans.- Calling an
unsafe fn(§27.4). - None of these is defined for
*void: cast it to a typed pointer first (N0282).
27.4 Unsafe contexts
unsafe { statements }is a statement;unsafe { e }is an expression whose value ise's (grammar §2). In statement position,unsafe {always starts the statement form.unsafe fn(a modifier after the visibility): calling the function is an unsafe operation, and its body is an unsafe context.mainmay not beunsafe(N0219). Methods may beunsafe; since C5, interface methods may be too (§30.1), and animplmust match (N0267).- Everything lexically inside an unsafe block, an unsafe expression or the body of an
unsafe fnis in an unsafe context, including deferred statements. A nestedunsafeis allowed. - The Safety rule (U2): an
unsafe fn, and every function whose body contains anunsafeblock or expression, must carry a doc comment (grammar §1.2) with a line that starts withSafety:. It states the contract: what callers must guarantee (for anunsafe fn) or why the unsafe operations inside are sound (for a safe function). Without one, N0283. - Safe code is checked exactly as before. An unsafe context only permits the operations of §27.3; it never relaxes the type, move, borrow or region rules for anything else.
27.5 Lowering
- NIR:
p.read()andp.write(v)are a copy from, and an assignment to, the place(*t)of a temporarytholdingp: the sameDerefprojection as references, through a pointer.Ptr.toBorrow,Ptr.toBorrowMutand the pointer casts areCasts; the arithmetic,isNull,.ptrandfromRaware built-ins. The move and borrow checkers ignore places through raw pointers (no loans, no moves, no mutability check beyond §27.3). - C (c-backend §2): see
unsafe.md§4. Pointer arithmetic is done onuintptr_t, so generated C never computes an out-of-object pointer. - Placement (M5,
memory-model.md§5.1 rule 1c): a function that makes a raw pointer from a reference or callsPtr.toBorrow/toBorrowMutorfromRawkeeps all its boxes on the heap ("address exposed").
Diagnostics: N0280 (an unsafe operation outside an unsafe context), N0281 (an invalid pointee type), N0282 (a pointer operation the pointer or its pointee does not allow), N0283 (no Safety: doc comment). Invalid pointer casts are N0208.
28. C interoperability (M8b)
extern "C" {
fn strlen(s: *CChar) -> usize;
fn puts(s: *CChar) -> i32;
}
@repr(C)
struct Point { x: i32; y: i32; }
/// The Manhattan length of a point, callable from C as `geo_length(struct Point)`.
@export("geo_length")
fn length(p: Point) -> i32 => abs32(p.x) + abs32(p.y);
fn abs32(v: i32) -> i32 { if (v < 0) { return -v; } return v; }
/// Safety: the literal ends in a zero byte, as `puts` and `strlen` need.
fn main() -> i32 {
let text = "hello from C\0";
let p = text.ptr as *CChar;
unsafe { puts(p); }
return truncate<i32>(unsafe { strlen(p) });
}28.1 Attributes
An item may be preceded by attributes @name or @name(arguments) (grammar §2). The known ones:
| Attribute | On | Meaning |
|---|---|---|
@export, @export("symbol") | a top-level, non-generic function; a constant (C9) | callable from C under symbol (default: the function's name); for a constant, C data under that name, §28.4 |
@cname("name") | a field of an imported C struct (C9) | its C member name, when that is not a Nova identifier, §31.1 |
@repr(C) | a non-generic struct | a C-compatible struct, §28.5 |
@repr(u8) … @repr(i64) | a non-generic enum without payloads | passed to and from C as that integer type, §28.5 |
@effects(…) | a function, method or extern function | the effects it may have (§32.2) |
@context(atomic), @context(irq) | a function or method; atomic also a struct | a context that forbids effects (§32.3) |
@placed | a method fn m(borrow mut this) | the type's placement initialiser (§33.3) |
@callback | a top-level function | a C address without a symbol, for C callbacks (§34.1) |
@align(n) | a non-generic struct | at least n-byte alignment (M8d, §29.4) |
@noncopy | a struct | a move type even if every field is Copy (OD-7; M8d) |
An unknown attribute, a known one in the wrong place or with the wrong arguments, or the same attribute twice is N0290.
28.2 extern "C" blocks
extern "C" { … }at the top level declares functions defined in C:fn name(parameters) -> Result;, each optionally preceded by attributes and a visibility, with no body, receiver, type parameters or region parameters (N0290)."C"is the only ABI (N0290).- Calling an extern function is an unsafe operation (U1, N0280): Nova cannot check what C does.
- Its C name is its Nova name. It must be a valid C identifier that is not a C keyword (or
bool,true,false,NULL,main), not reserved by C (starting with__, or_and an uppercase letter), and not owned by the runtime and generated code (starting withnova_,nv_,NV_, ornvand an uppercase letter; c-backend §2.1): N0292. Two declarations, or a declaration and an@export, with one C name are N0201. - Parameter and result types must be FFI-safe (§28.3), or N0291.
28.3 FFI-safe types and their C types
| Nova | C, in prototypes and in nova_exports.h |
|---|---|
i8…i64, u8…u64 | int8_t…int64_t, uint8_t…uint64_t |
usize, isize | size_t, ptrdiff_t |
bool | bool |
*T, *mut T | T const *, T *, where T is an integer, bool, a @repr(C) struct, or (recursively) such a pointer |
*void, *mut void | const void *, void * |
*CChar, *mut CChar | const char *, char * |
a @repr(C) struct S | struct S |
a @repr(uN) / @repr(iN) enum | that integer type |
void (results only) | void |
Nothing else crosses: references, slices, string views, boxes, arrays, optionals, results and other structs and enums are N0291. (A slice crosses as its .ptr and .length; a pointer to any other Nova value crosses as *void.) A @repr enum crosses by value only, not behind a pointer or as a field of a @repr(C) struct: inside Nova it is not a plain integer.
CChar is C's char, whose signedness differs between targets. It may only be a pointee (*CChar), never a value (N0281); p as *u8 and q as *CChar convert. It exists so that declarations of C library functions match the C library's own prototypes exactly.
28.4 Exports
@exportmakes a top-level, non-generic, non-method function callable from C, with the C types of §28.3 (N0291 otherwise).maincannot be exported (N0290). The symbol must be a valid, unreserved C name (N0292) and unique (N0201).- The function stays an ordinary Nova function too. Its body is checked like any other; an exported function is not unsafe to call from Nova.
- A C caller must call it with the declared types. A C value for a
@reprenum parameter that is not one of its variants panics (invalid enum value from C) before the body runs. - Exported constants (C9, ADR 0014).
@exportor@export("symbol")on a constant (§10) of integer orbooltype, or of a constant array type[E; N], defines read-only C data with that symbol (default: the constant's name):const CT symbol = v;orconst CE symbol[N] = { … };, declared innova_exports.hasextern const CT symbol;/extern const CE symbol[N];. The symbol follows the rules for functions (N0292, N0201). In Nova the constant is used as any other constant.
28.5 C-compatible structs and enums
@repr(C)on a struct fixes its layout to C's rules (fields in declaration order, each aligned, the size rounded up to the alignment; Nova's layout already is this, and M9 may reorder only structs without it). Its fields must be integers,bool, raw pointers as in §28.3, or@repr(C)structs, and their names valid, unreserved C identifiers (N0291, N0292). In C it isstruct Swith members named as in Nova, of the types Nova uses inside its own C (usizeisuint64_t, pointers have noconst), so that the generated code and C code that includesnova_exports.hdeclare the same struct type. It is otherwise an ordinary struct.@repr(uN)/@repr(iN)on an enum without payloads gives each variant its index (0, 1, …) as that integer type, which must hold every index (N0290). Across the boundary the enum is that integer; a value from C that is not a variant's index panics (invalid enum value from C).
28.6 Building with C
novac buildaccepts C source files (.c) besides Nova files. They are compiled with the generated C and linked into the program, with the same C compiler and flags. With them, a program may have itsmainin C instead of in Nova; without a Novamain, the Nova runtime is not started, so allocation-failure injection (NOVA_FAIL_ALLOC_AT) and program arguments are not available to exported functions.- When a program has
@exportfunctions or@reprtypes, novac also writesnova_exports.h: the@reprstructs and enums, and a prototype for each export. C files include it as#include "nova_exports.h". run --interpruns no C code. Calls to extern functions are interpreted only for a model of a few C library functions (intermediate-representation.md§5.4); any other is an interpreter fault.
Diagnostics: N0290 (attribute or extern declaration misuse), N0291 (a type that is not FFI-safe), N0292 (a C name that is invalid, a C keyword, or reserved). Calls to extern functions outside an unsafe context are N0280.
29. Systems capabilities (M8d)
@repr(C)
struct UartRegs { data: Volatile<u32>; status: Volatile<u32>; }
struct Uart {
regs: MmioPtr<UartRegs>;
/// Safety: `base` is the mapped address of a PL011-compatible UART that no other `Uart` aliases.
public unsafe fn attach(base: usize) -> Uart => Uart { regs: MmioPtr<UartRegs>.at(base) };
public fn putByte(borrow mut this, b: u8) {
while ((this.regs.get().status.read() & 0x20) != 0) { Cpu.spinHint(); }
this.regs.getMut().data.write(b as u32);
}
}29.1 Volatile accesses (unsafe)
p.readVolatile() and p.writeVolatile(v) are read and write (§27.3, same rules: unsafe, Copy pointee, *mut T to write) as volatile accesses: each is performed exactly once, in program order with every other volatile access and call, with the access width of T, never merged, split, cached or removed. In C they are *(volatile T *)p. They are for memory-mapped registers and memory shared with hardware; they do not order accesses between processors (that needs atomics, C7).
29.2 lib/core: Nova sources bundled with the compiler
Some core types are written in Nova itself, over raw pointers and unsafe (ADR 0007 decision 1). Their sources live in lib/core/*.nova, are embedded in novac, and are part of every package, in both profiles. Their names are reserved like built-in names (N0201). Code from lib/core is type-, move- and borrow-checked like user code; functions and types the package does not use are not emitted, and --dump-hir/--dump-nir leave them out.
| Type | What it is |
|---|---|
Volatile<T: Copy> | A cell whose value is read and written only by volatile accesses: v.read() (borrow this), v.write(x) (borrow mut this). One field of type T, so it has T's layout and may be a @repr(C) field |
MmioPtr<T> | The address of a memory-mapped T (typically a @repr(C) struct of Volatile registers). unsafe MmioPtr<T>.at(address: usize) makes one; p.get() is a borrow T and p.getMut() (borrow mut this) a borrow mut T, tied to p by their signatures (§13.5), so register access needs no unsafe; p.address() is the usize |
PhysAddr, VirtAddr | Distinct address types (cosmoos/kernel-memory-and-lifetimes.md): PhysAddr.of(bits: u64), a.bits(), a.offset(n: u64) (checked). No conversion between them, or to and from integers, except through of and bits |
The contract of MmioPtr<T>.at (its Safety: line): the address is mapped, aligned for T, and no other MmioPtr or reference aliases the registers; the program then reaches them only through this value.
29.3 CPU and target queries
Cpu.spinHint(): a hint inside a busy-wait loop (pauseon x86-64,yieldon AArch64, nothing elsewhere).void, safe, allowed in both profiles.Target.pointerBits():u32, the target's pointer width (64 on every target Nova supports).Target.isX86_64()andTarget.isAArch64():bool, the architecture the generated C is compiled for. They are decided by the C compiler, not bynovac(the generated C is the same for every LP64 target), so they are not constants: use them in ordinary conditions. The interpreter answers for the machine it runs on.
29.4 @align(n)
@align(n) on a non-generic struct, with n a power of two from 1 to 4096, raises the struct's alignment to at least n (its size is rounded up to it). In C, the first member is declared _Alignas(n). It may be combined with @repr(C). N0290 for other n or places.
29.5 C function pointers
extern "C" fn(T1, …, Tn) -> Ris the type of a pointer to a C function (grammar §2). Its parameter and result types must be FFI-safe (§28.3) and not@reprenums (N0291). It is one address, Copy, holds no loans, and is FFI-safe itself, so C callbacks can be passed to and returned from C.- Values. The name of an
extern "C"function, an@exportfunction or a@callbackfunction (§34.1) is a value of that type where such a type is expected (a typedlet, an argument, a field, a return value) and the signatures are equal. Only those functions have a C address; naming another function there is N0291.0 as extern "C" fn(…) -> Ris the null pointer,f.isNull()tests for it, andf as usizeis its address. - Calls.
f(args), withfa local, parameter or field of such a type, calls through the pointer. It is unsafe (N0280): the pointer may be null or point to anything. - Nova functions that are neither exported nor
@callback(§34.1, C7d) have no C address. Nova-level function values (fn(…) -> R) and closures are §36 (M11 G8).
29.6 Entry points
A hosted program's entry point is main, called by the generated C main (§11). A freestanding package has no main (cosmoos/profiles.md §4.1): its entry points are its @export functions, which the environment calls (a kernel's init, a boot stub's kmain, a callback). An export's symbol may start with one _ and a lowercase letter (_start), the names platforms use for entry symbols (§28.2 reserves only __ and _ with an uppercase letter); other reserved C names stay N0292.
Diagnostics: no new codes. Volatile accesses outside an unsafe context are N0280; a function pointer type or value that cannot cross to C is N0291; a misused @align is N0290.
30. User-defined allocators (C5)
extern "C" {
fn kmalloc(size: usize, flags: u32) -> *mut void;
fn kfree(p: *mut void);
}
/// CosmoOS's kernel heap: kmalloc and kfree (integrations/cosmoos/c5/kernel.nova).
struct KernelHeap {}
impl Allocator for KernelHeap {
/// Safety: kmalloc returns null or a block of `layout.size` bytes, 16-byte aligned.
fn allocate(borrow this, layout: Layout) -> Result<*mut u8, AllocError> {
if (layout.align > 16) { return Err(AllocError.OutOfMemory); }
let p = unsafe { kmalloc(layout.size, 0) } as *mut u8;
if (p.isNull()) { return Err(AllocError.OutOfMemory); }
return Ok(p);
}
/// Safety: the caller passes a block this allocator returned (Box guarantees it).
unsafe fn deallocate(borrow this, p: *mut u8, layout: Layout) { kfree(p as *mut void); }
}
fn make() -> Result<Box<u64, KernelHeap>, AllocError> => Box.createIn(KernelHeap {}, 42u64);30.1 Layout and Allocator (lib/core/alloc.nova)
struct Layout { size: usize; align: usize; }: a block's size in bytes and its alignment (a power of two).interface Allocator, with two methods:fn allocate(borrow this, layout: Layout) -> Result<*mut u8, AllocError>: a block of at leastlayout.sizebytes aligned tolayout.align, orErr(never a nullOk);unsafe fn deallocate(borrow this, p: *mut u8, layout: Layout): returns a block. Its contract:pcame fromallocateof this allocator type with the same layout, and is returned once.
- Interface methods may be
unsafe(lifting §27.4's earlier rule); animplmust match the interface'sunsafe(N0267). layoutOf<T>()(ADR 0027):T's size and alignment as aLayout, fromTargetInfo. For a type holding C state (§33), whose size only the C compiler knows (§31.2), it is C'ssizeof/_AlignofofT's C type, sonovac run --interpcannot evaluate it. It takes one type argument and no arguments (N0209), and is a static initialiser (§39.4). ADR 0027 named itLayout.of<T>(); Nova has no type arguments on a member call, so it is a function, liketruncate<T>.
30.2 Allocator types
- A user-defined allocator is a non-generic struct with no fields that implements
Allocator. It may be theAofBox<T, A>, andBox.createIn(a, v)takes a value of it. Having no fields, it has no state that a box would need to carry: aBox<T, A>stays one pointer, and dropping it needs no allocator value (the memory-model's free-standing allocators, §3.2 there). State that an allocator needs (counters, a C handle) lives on the C side or in global C state. - The built-in allocators are still
SystemHeapandFailingAllocator.Vector,String,SlotMap,StableVector,ArenaandPooltake only those in v0 (N0263): their growth is in the C runtime, which cannot call Nova methods. - A type named as a box's allocator that is not a struct without fields implementing
Allocatoris N0263.
30.3 Semantics
Box.createIn(a, v)with a user allocator callsallocate(Layout { size: size(T), align: align(T) })once. OnErr, it destroysvand returnsErr(AllocError.OutOfMemory); onOk(p), it movesvtop. Destroying the box destroys the contents, then callsdeallocate(p, the same layout)once (H1). The compiler only callsdeallocatewith blocksallocatereturned, so the contract holds for every safe program.- A
size(T)of 0 is never requested: such boxes still allocate one byte (asSystemHeapdoes). - User allocators are not placement-transparent (M5): their boxes stay on the heap.
- They are allowed in the freestanding profile (
cosmoos/profiles.md§4.1):Boxwith a user allocator needs no hosted runtime.Box.create(SystemHeap) stays hosted-only.
Diagnostics: N0263 (a box allocator that is not one), N0267 (an impl whose unsafe differs).
31. C struct types imported from headers (C6b; ADR 0008)
@header("kernel/bootinfo.h")
extern "C" {
struct cosmoboot_info { // only the fields Nova uses, by their C names
fb_phys: u64;
fb_width: u32;
}
struct bootinfo_framebuffer { phys: u64; width: u32; }
struct cosmoboot_mem_entry; // opaque: only pointed to
}
fn pixels(info: borrow cosmoboot_info) -> u64 => info.fb_width as u64;
fn copyOut(info: borrow cosmoboot_info, out: borrow mut bootinfo_framebuffer) {
out.phys = info.fb_phys;
out.width = info.fb_width;
}31.1 Declarations
- Inside an
extern "C"block,struct name { field: Type; … }declares an imported C type: a type that a C header defines, seen through the fields Nova declares.struct name;imports one with no fields (opaque). The grammar is grammar.md §2'sExternStruct. @header("path", …)on the block names the headers that define its types, one string per header, as C's#include "path"would find them (the build's-Ipaths, then the generated file's directory). A block that declares a struct needs a@header(N0290). A header string must be non-empty and must not contain",\, or a line break (N0290).- The type's C spelling is
struct name. With@ctype("cname")on the declaration, it iscnameinstead, for a C typedef (typedef struct {…} cname;). The name, the@ctypename and every field name must be valid, unreserved C identifiers (§28.2, N0292). - Fields are declared with their C member names, in any order, and only those Nova uses. Their types must be integers,
bool, raw pointers whose pointee C can name (§28.3, including imported types), (since C7b, §33.1) imported types, for embedded members, or (since C9) arrays[E; N]of integers,boolor imported types, for C array membersCE name[N]; otherwise N0291. An embedded member is checked with_Genericonly, since Nova has no size for it. A field's declared type must be exactly the member's C type, under §28.3's mapping (u32isuint32_t,usizeissize_t,*CCharisconst char *,*mut TisT *,[u8; 16]isuint8_t[16]). This is checked when the C is compiled (§31.3), not by novac. (A plainchararray cannot be declared:CCharis only a pointee.) @cname("name")on a field (C9) gives its C member name when that is a Nova keyword (type,in,match, …); Nova code uses the declared name. The C name must be a valid, unreserved C identifier (N0292), and no two fields may have the same C name (N0228).@cnameanywhere else is N0290.- An imported struct has no methods,
dropblock, type or region parameters, or other attributes (N0290). Its visibility is that of a struct.
31.2 Use
- Nova never knows an imported type's size, alignment or field offsets: they are the C compiler's, under the build's flags. So an imported type
Tappears only behind a pointer or a reference:*T,*mut T,borrow T,borrow mut T, and (since C7b, §33) by value as a struct field or a box's contents. As the type of a local, parameter, result, array element, optional, or type argument, it is N0294. No value of it exists in Nova: it is neither Copy nor movable, and has no struct literal (N0294) and no drop. - Field places. Through a reference,
r.fis a place of the declared field type, as for any struct (§12): read it, assign it throughborrow mut, or borrow it. The borrow checker treats it like any field place. A field of raw pointer type,usizeorisizecan be read and assigned but not borrowed (N0294): its C member type (const char *,size_t) is not the C type Nova uses for the value (char *,uint64_t), so a pointer to it would have the wrong type in C. - Array fields (C9) are array places of their declared type, as in any struct (§16):
r.f[i]is bounds-checked againstN,r.f[a..b]is a slice (mutable throughborrow mut), and when the element is an integer orboolthe field can be read and assigned whole. An array of an imported type is C-laid-out (§33.1): its elements are places of the imported type, used through their fields or borrowed, and the array itself is never a value. - Raw pointers. Making, casting and comparing pointers to imported types is safe (§27.2), and
Ptr.toBorrow/Ptr.toBorrowMutmake references from them (unsafe, §27.3).add,subandoffset(unsafe, §27.3) step by the C type's size (since C9): C does the arithmetic. What would copy a value or make a slice of one is N0294:read,write,readVolatile,writeVolatile, andSlice.fromRaw/SliceMut.fromRaw. - Across the boundary,
*Tand*mut Tare FFI-safe (§28.3):struct name const *andstruct name *(or the@ctypename) in prototypes. The type itself is not, by value (N0291).
31.3 Lowering
- The generated C includes every
@header, afternova/rt.h, once each, in the order the blocks appear (files in command-line order);nova_exports.hincludes them too. - Nova emits no definition, typedef or layout assertion for an imported type, and names it by its C spelling. A field place is C's member access (
(*p).f). Pointer fields are converted throughuintptr_twhen read and written, as at exports (c-backend.md §2.3), so thatconstin the header's member type causes no warning. - For every declared field
fof typeF(C typeCF), the generated C asserts both_Generic(((S *)0)->f, CF: 1, default: 0)andsizeof(((S *)0)->f) == size(F), with a message naming the struct, the field and the declaration's file and line. Neither expression is evaluated. A header that lacks a field, or has it with another type in some configuration, fails to compile; it cannot miscompile. - An array field
f: [E; N](C9) is asserted with_Generic(&((S *)0)->f, CE (*)[N]: 1, default: 0), exact where_Genericof the member itself would see a decayed pointer, and, for integer andboolelements, with the size of Nova's array. Its place is(*(A *)(uintptr_t)&(*p).f)(throughuintptr_t, aspmay be aconstpointer), whereAis Nova's array struct (struct { CE e[N]; }, c-backend.md §2), which has the array's size and alignment: an access through it is an access through an aggregate containing the member's type (C11 6.5p7). No copy is made.
31.4 The interpreter
run --interp has no headers, so it has no layout for an imported type. A function that accesses a field of one is an interpreter fault (exit status 70) when it does so. Passing pointers to imported types along is fine.
31.5 Profiles
Imported types are platform-neutral (cosmoos/profiles.md §5). They are allowed in every profile. The same declarations work with CosmoOS's, Linux's or a C library's headers.
Diagnostics: N0290 (a misplaced attribute, a block without @header, a bad header string, a member an imported struct cannot have), N0291 (a field type C and Nova do not both spell), N0292 (a C name), N0294 (an imported type used where Nova would need its size, or a borrow of a pointer or size field).
32. Context effects (C7a; ADR 0009)
extern "C" {
@effects(sleep)
fn mutex_lock(m: *mut mutex);
@effects()
fn spin_unlock(l: *mut spinlock_t);
}
/// Holding one is an atomic context: nothing may sleep while it is live.
@context(atomic)
struct SpinGuard { lock: *mut spinlock_t; drop { unsafe { spin_unlock(this.lock); } } }
@context(irq)
fn onInterrupt(count: borrow mut u64) { count += 1; } // may neither sleep nor allocate32.1 Effects
- There are two effects:
sleep(the code may block) andalloc(it may allocate memory from a heap). - The effects of a function are those of every operation its body may perform, including everything it calls and every value it drops (drop glue,
dropblocks, a box's deallocation). They are inferred over the whole package. Recursion is handled by iterating to a fixed point. - Operations have these effects:
| Operation | Effects |
|---|---|
| a call of a Nova function | that function's effects |
a call of an extern function | its @effects if declared; otherwise sleep and alloc |
| a call through a C function pointer (§29.5) | sleep and alloc |
| a call through a function value or closure (§36) | its type's effect list (§36.3); without one, sleep and alloc |
Box.create, and Box.createIn with a built-in allocator | alloc |
Box.createIn with a user allocator (§30), dropping such a box | the effects of its allocate, or of its deallocate |
allocating from an Arena, a Scratch frame or a Pool, making one; Vector.withCapacity, push; String.append, withCapacity; StableVector.push; SlotMap.insert | alloc |
allocating from a FixedBufferAllocator; every other built-in operation on values, pointers and collections | none |
| console, file, socket and process operations (§26) | sleep (and alloc for Process.run) |
| dropping a value | the effects of its drop glue: drop blocks, the contents' drops, deallocations |
32.2 Declaring effects
@effects(e, …), with eacheeithersleeporalloc, and@effects()for none:- on an
externfunction, it declares what the C function may do. Nova trusts it, as it trusts a prototype: a wrong declaration is a wrong binding (R-2); - on a Nova function or method, it is an upper bound. The inferred effects must be among those declared (N0295, reported at an operation that has the extra effect).
- on an
- An unknown effect name, or
@effectstwice, is N0290.
32.3 Contexts
@context(atomic)on a function forbidssleepin its body.@context(irq), code that may run in a hardware interrupt, forbidssleepandalloc(cosmoos/kernel-memory-and-lifetimes.mdKQ-1: IRQ handlers do not allocate). An operation with a forbidden effect is N0295.@context(atomic)on a struct makes holding a value of it an atomic context. At every point of a function where a local that holds such a value (the struct itself, or a type containing it by value) may be live,sleepis forbidden (N0295). A local is live from its initialisation until it is dropped, moved away or its storage ends, and a parameter from the function's entry. "May be live" joins over the control-flow paths. Dropping the holder itself is checked as if it were no longer live.- A context restricts the body it is declared on, not callers: any code may call an
@context(irq)function. A function called from an atomic context is checked through its inferred effects. - Generic functions and structs are checked in each instantiation. An error is reported once.
32.4 What this proves
The checker proves that Nova code performs no operation whose declared or inferred effects its context forbids. It relies on the @effects of extern functions being right, and proves nothing about C code. It does not prove freedom from deadlock or lock-order inversions. In CosmoOS, might_sleep() and lockdep remain the runtime check behind it (ADR 0009).
Effects are checked in every profile. Only @context, @effects bounds on Nova functions, and @context(atomic) types create obligations, so code that uses none is unaffected.
Diagnostics: N0295 (an operation with an effect its context or @effects bound forbids; the message names the chain of calls that leads to the operation), N0290 (misused @effects or @context).
32.5 Effects of closure arguments: @effectsOf (OD-9c; ADR 0020)
@effectsOf(test)
fn count(values: Slice<i32>, test: borrow fn(i32) -> bool) -> i32 {
var n = 0;
for (v in values) { if (test(v)) { n += 1; } } // not count's own effect: its caller's
return n;
}
fn isEven(v: i32) -> bool => v % 2 == 0;
@context(atomic)
fn underLock(values: Slice<i32>, limit: i32) -> i32 {
return count(values, (v) => v > limit) + count(values, isEven); // allowed: neither closure sleeps
}The attribute.
@effectsOf(p, …)on a function or method names some of its parameters. Each must be of a function or closure type (fn(…),borrow fn(…),borrow mut fn(…), with or without an effect list), or it is N0336. It changes no type. With@effects(…), the bound applies to the function's own effects.Inside the function, a call through such a parameter
p, or through a local that holdsp(copied, cast or reborrowed from it and assigned nothing else), is not one of the function's own effects. So is passingpas the argument for another function's@effectsOfparameter. Every other use is as before:ppassed to a function without the attribute, stored, or captured has its type's effect list wherever it is called.At a call, the effects are the callee's own, plus, for each of its
@effectsOfparameters, the argument's:Argument Its effects a closure literal, or a local known to hold one (optimisations.md §5.1: every assignment to it is that literal, or copies or reborrows of a local that holds it) the literal's body's inferred effects a named function (§36.1), adapted to a closure that function's inferred effects: the adapter calls it directly the caller's own @effectsOfparameter, or a local that holds itnone: the caller's caller counts it anything else its type's effect list (both effects without one, §36.3) N0295 names the argument in its chain: "it calls 'count' with the closure at main.nova:12, which calls …".
As a value, an
@effectsOffunction (§36.1) has its own effects plus each named parameter's type list.Why this is sound. A parameter is never assigned (N0211), and a closure or function value cannot be
var(N0246), so every call throughpduring a call runs the argument the caller passed, whose effects the caller counted. Anything made frompother than a copy or reborrow is counted by its type, where it is called.lib/core's
sortBy,forEach,any,allandretain(§36.4) are@effectsOftheir closure, and so aresortBy's helpers.
33. C state embedded by value (C7b; ADR 0009 §2)
@header("kernel/spinlock.h")
extern "C" {
@ctype("spinlock_t")
struct spinlock_t;
@effects()
fn spinlock_init(lock: *mut spinlock_t, name: *CChar);
}
/// A lock and the data it protects; the C compiler lays it out, and it never moves.
struct SpinLock<T> {
raw: spinlock_t; // by value, zero-filled when made
name: *CChar;
data: T;
/// Runs at the lock's final address, before anything can use it.
@placed
fn init(borrow mut this) { unsafe { spinlock_init(borrow mut this.raw as *mut spinlock_t, this.name); } }
}
fn make(name: *CChar) -> Result<Box<SpinLock<u64>, KernelHeap>, AllocError> =>
Box.createIn(KernelHeap {}, SpinLock { name: name, data: 0 }); // `raw` is left out33.1 C-laid-out types
- A struct field may have an imported C type (§31) by value. In a Nova struct, it embeds that C state; in an imported struct, it declares an embedded member (
struct mutex { lock: spinlock_t; }). - A type is C-laid-out if it is an imported type, an array of a C-laid-out type (C9), or a struct with a field of a C-laid-out type. Like an imported type (§31.2), it has no Nova layout. Generated C defines a C-laid-out Nova struct with its real member types, so the C compiler lays it out, and asserts nothing about its size, alignment or offsets. Allocation sizes come from C's
sizeof, as for every type. - A C-laid-out type appears by value only as a field of another struct, as the contents of a
Box, or (C9) as the element of an array that itself appears so. As a local, parameter, result, optional, enum payload or any other type argument, it is N0294.
33.2 Made in place, never moved
- C state may point into itself once initialised (an empty list head points to itself), so a C-laid-out value is never moved, copied or assigned. Every move of one is N0294.
- The only way to make one is a struct literal given directly to
Box.createorBox.createIn, or nested as a field of such a literal. Fields of imported type are left out of the literal and are zero-filled (C's{0}). Writing one is N0294: Nova has no values of imported types. - A C-laid-out value lives in its box until the box is dropped. Its contents are borrowed (
borrow,borrow mut, throughPtr.toBorrowof a pointer to them) but never moved out. Dropping the box runs the contents' drop glue in place, then frees them.
33.3 @placed methods
@placedon a methodfn m(borrow mut this)with no other parameters and no result makes it the type's placement initialiser. A struct has at most one. Misuse is N0290.- When
Box.create/Box.createInhas placed a C-laid-out value in its box, and before it returns, the compiler calls the@placedmethods of the value's fields' types, depth first, innermost first, in field order, and then the type's own. No other code can see the value before then. A@placedmethod runs C's initialisers (spinlock_init,mutex_init,kobject_init) at the value's final address. - A box whose allocation fails runs no
@placedmethod: the value is dropped, as in §18.2. - The effects of
Box.create/Box.createIn(§32.1) include those of the@placedmethods they run.
33.4 The interpreter
run --interp has no C layout, so making, allocating or accessing a C-laid-out value is an interpreter fault (exit status 70), as for imported types (§31.4).
Diagnostics: N0294 (a C-laid-out type by value outside a field or a box; a move of one; a literal of one outside Box.create/createIn; an imported field in a literal), N0290 (a misused @placed).
34. Callbacks into Nova, and raw boxes (C7d)
@header("kernel/object.h")
extern "C" {
struct kobject;
struct kobject_type { name: *CChar; release: extern "C" fn(*mut kobject); }
}
/// The kernel calls this when the last reference goes; one C function per instance.
/// Safety: `obj` is the first field of a `KObject<T>` made by `KRef<T>.create`, and this runs once.
@callback
unsafe fn release<T>(obj: *mut kobject) {
let object: Box<KObject<T>, KernelHeap> = Box.fromRaw(obj as *mut KObject<T>);
} // dropping `object` drops the data and frees it
// … this.ktype.release = release<T>; in KObject<T>'s @placed method34.1 @callback: C addresses of Nova functions
@callbackon a top-level function gives it a C address, as@exportdoes (§29.5), without a symbol. Where anextern "C" fntype is expected, its name is a value of that type if the signatures are equal. A generic function is named with its type arguments (release<T>inside generic code,release<u64>outside), and each instance has its own C function.- Its parameter and result types must be FFI-safe (§28.3) and must not mention its type parameters, so every instance has the same C signature (N0291). It may be an
unsafe fn: itsSafety:line states what the C caller guarantees.@callbackwith@exportis N0290. - Generated C gives each instance used as a value a
staticfunction with the C types, which converts its arguments and result as an export does (c-backend.md §2.3). Calling through the pointer is unsafe and has thesleepandalloceffects (§32.1). The callback's own body is checked like any other function's.
34.2 Box.intoRaw and Box.fromRaw
Box.intoRaw(b)consumes the boxb: Box<T, A>and returns*mut T, the address of its contents. Nothing is dropped or freed: the caller owns the memory, and leaks it unless it gives it back. It is safe (a leak is not undefined behaviour). It is for handing ownership to C: a kernel object, a deferred free.Box.fromRaw(p)(unsafe, §27.3) turnsp: *mut Tback into a box. The box type, and with it the allocator, comes from the expected type (let b: Box<T, KernelHeap> = unsafe { Box.fromRaw(p) }); without one it is N0205. Contract:pcame fromBox.intoRawof a box of exactly that type, and is given back once.- Neither applies to pool boxes (N0205). Neither has an effect. In the interpreter both work as in C.
34.3 A struct's first field
The fields of a struct, including a C-laid-out one (§33), are laid out in declaration order, so the address of a struct is the address of its first field (C11 6.7.2.1p15). A pointer to the first field converted with as is a pointer to the struct, which is how a kernel callback that receives the embedded struct kobject * or struct quiesce_head * finds its object. Since C7d, a field of an imported struct may also have a C function pointer type (§29.5), such as kobject_type.release.
Diagnostics: N0291 (a callback whose C signature is not FFI-safe or depends on its type parameters), N0290 (@callback misused), N0205 (Box.fromRaw without a box type).
35. Floating-point types (M11 G7; ADR 0011)
fn mean(values: Slice<f64>) -> f64 {
var sum = 0.0;
for (v in values) { sum += v; }
return sum / round<f64>(values.length);
}
fn main() {
let samples = [1.5, 2.25, 4.0];
let m = mean(samples[..]); // 2.5833…
Console.writeLine(truncate<i64>(m * 1000.0)); // 2583: toward zero
let x: f32 = 0.1; // the f32 nearest 0.1
Console.writeLine(x.bits()); // 1036831949
}35.1 Types and values
f32andf64are IEEE 754 binary32 and binary64. Their layout comes fromTargetInfo(size and alignment 4 and 8 on both supported targets). They are Copy, need no drop, hold no loans, and are FFI-safe (Cfloatanddouble, §28.3).- The floating-point environment is the default one, and Nova code never changes it: rounding to nearest, ties to even; no traps; status flags are never read.
- Every operation rounds its exact result once, to its type. There is no fused multiply-add and no evaluation in a wider type (§35.6).
35.2 Literals
- A float literal (grammar.md §1.5:
1.5,1e9,2.5e-3,6.02e23f64) has typef32orf64. Its value is the decimal value rounded to nearest (ties to even) in that type, once:0.1f32is thef32nearest 0.1, not thef64nearest 0.1 rounded again. - With a suffix, the type is the suffix's. Without one, the literal is flexible (§5): it takes an expected
f32orf64(looking through an optional, as integer literals do) and otherwise defaults tof64. A flexible float literal where an integer type is expected, or an integer literal where a float type is expected, is N0205: there are no implicit conversions (write1.0, orround<f64>(n)). - A literal whose value rounds to infinity in its type is N0206 (
1e39f32). A literal that rounds to zero is zero. -before a literal is the negation operator (§35.3), so-0.0is negative zero.
35.3 Operators
| Operators | Operand types | Result | Behaviour |
|---|---|---|---|
+ - * / | the same float type | that type | IEEE 754, rounded to nearest even. Never panics: overflow gives ±infinity, x / 0.0 gives ±infinity, and 0.0 / 0.0, inf - inf and 0.0 * inf give NaN |
unary - | float | same | flips the sign, also of zeros and NaNs |
== != < <= > >= | the same float type | bool | IEEE comparisons: NaN is unequal to everything, itself included, and is neither less nor greater; -0.0 == 0.0 |
+= -= *= /= | as x = x op y (§6) |
- Float arithmetic is the one arithmetic that does not panic on an unrepresentable result (OD-6 is about integers; ADR 0011 Q2).
%, the bitwise operators and the shifts are not defined on floats (N0207).- Floats cannot be
matchscrutinees or patterns (N0207, §15.2). - NaN bits are not portable. Which NaN an operation produces is not specified: x86-64 and AArch64 differ in its sign (inferred from their default NaNs). A NaN compares, converts and prints alike whatever its bits; only
bits()(§35.5) shows the difference.
35.4 Conversions
asstays lossless (§7): it converts a value only to a type that holds every value of the source type exactly. For floats, these are:f32 as f64;i8,i16,u8andu16asf32orf64;i32andu32asf64.
Any other
asfrom or to a float type is N0208.round<T>(e):eof any integer or float type,Ta float type. The result iserounded to nearest (ties to even) inT. It is always defined: anf64too large forf32becomes ±infinity, and NaN stays NaN.truncate<T>(e)witheof a float type andTan integer type rounds toward zero. It panics with "float out of range" ifeis NaN or ±infinity, or if the rounded value does not fit inT. (truncatebetween integer types is unchanged, §7.) A floatTis N0207: useround.checkedTruncate<T>(e): astruncate, but the result isT?,nullwheretruncatewould panic.saturatingTruncate<T>(e): astruncate, but a value belowT's range gives its minimum and one above gives its maximum. NaN gives 0. It never panics.checkedTruncateandsaturatingTruncateneed a floateand an integerT, androunda floatT(N0207 otherwise). All four take exactly one type argument (N0216).
35.5 Representation
f.bits()is the IEEE representation off: au32forf32, au64forf64.f32.fromBits(u)(u: u32) andf64.fromBits(u)(u: u64) make a float from a representation; every pattern is a value, some of them NaNs.- The names
bitsandfromBitsare reserved on float types, as the overflow methods are on integers (§6).
35.6 C and the interpreter
f32andf64are Cfloatanddouble. A float literal is emitted by its bits (nv_f64_from_bits), so its value never depends on the C compiler's decimal conversion.- In a program that uses floats,
rt.hrequiresFLT_EVAL_METHOD == 0(no wider evaluation) and IEEE formats (FLT_RADIX == 2,FLT_MANT_DIG == 24,DBL_MANT_DIG == 53), and disables contraction with#pragma STDC FP_CONTRACT OFF(clang) and#pragma fp_contract(off)(MSVC). The generatedmain.casks for this with#define NV_FLOATSwhen the program has a float local, parameter, result or constant. A program without floats needs none of it. That matters for kernels built with-mgeneral-regs-only: x86-64 then has no SSE, andFLT_EVAL_METHODis not 0. gcc ignores the standard pragma, but does not contract under-std=c11, which Nova's builds use.novacalso passes-ffp-contract=offto clang and gcc. Integrations that compile Nova's C must not enable contraction or-ffast-math(c-backend.md §1). - Conversions in C have no undefined behaviour:
truncateand its variants compare the value with the target's exact bounds before converting (C11 6.3.1.4);round<f32>of anf64outsidef32's range gives the infinity directly, not through a C conversion (C11 6.3.1.5).
- The interpreter computes in .NET's
floatanddouble, which round the same way. Compiled and interpreted programs give the same results, NaN bits aside (§35.3). - Not in G7 (ADR 0011 §3.8):
%, a math library beyond §35.8, parsing floats from text, and other float formats.
35.7 Printing (M11 G7b)
Console.write,Console.writeLineandString.appendNumberacceptf32andf64(and references to them).The digits are the shortest decimal that reads back as the same value. Reading back means rounding to nearest, ties to even, in the value's own type, so an
f32prints withf32's shortest digits:0.1f32prints as0.1. When several decimals of that length read back, the nearest to the value is printed, and on a tie the one with an even last digit.The layout is that of ECMAScript's Number::toString (ECMA-262 §6.1.6.1.20). Write the decimal as 0.d1…dk × 10^n with no trailing zeros. Then:
- if k ≤ n ≤ 21, the digits and n − k zeros:
100,100000000000000000000; - if 0 < n ≤ 21, a point after n digits:
1.5,123456.789; - if −6 < n ≤ 0,
0., then −n zeros, then the digits:0.000001; - otherwise one digit, a point and the rest if k > 1,
e, the sign of n − 1 (+or-), and |n − 1|:1e+21,1.5e-7,5e-324.
- if k ≤ n ≤ 21, the digits and n − k zeros:
NaN,Infinityand-Infinity. Both zeros print as0, as in ECMAScript;bits()tells them apart.In C, the digits come from Schubfach (R. Giulietti, 2020;
runtime/hosted/nova_rt_float.c), with integer arithmetic only and a table of 617 powers of ten generated exactly. It differs from the paper's Java code in one place: a result may have one digit. Java needs at least two, so that code tries one digit fewer only from three digits on, and gives4.9e-324.The file is linked only into programs that print a float. The interpreter finds the same digits by an exact search over rationals instead. .NET's own shortest formatting cannot be used: .NET 10 prints 2^-25 as
2.980232238769531E-08, which reads back as a different double (found in G7b; Python'sreprand Schubfach give2.9802322387695312e-8).
35.8 Math methods (M11 G7c; ADR 0011 §5 Q5)
| Method | Result | Behaviour |
|---|---|---|
x.sqrt() | same type | the square root, correctly rounded (IEEE squareRoot). (-0.0).sqrt() is -0.0; a negative operand or NaN gives NaN |
x.abs() | same type | x with its sign cleared, NaN included |
x.floor(), x.ceil() | same type | the nearest integral value below or above (IEEE roundToIntegralTowardNegative/Positive). Integral values, infinities and NaN are unchanged, and a zero result keeps x's sign: (-0.5).ceil() is -0.0 |
x.min(y), x.max(y) | same type | IEEE 754-2019 minimum/maximum: NaN if either is NaN, and -0.0 is below 0.0. y is checked against x's type, so a flexible literal takes it |
- None of them panics, and none calls a C math library: CosmoOS has no libm.
floor,ceil,abs,minandmaxare exact portable C over integers and bits inrt.h. On baseline x86-64 there is no instruction forfloororceil;roundsdneeds SSE4.1.sqrtis the hardware square root, which is correctly rounded on both targets. It is reached through__builtin_elementwise_sqrtwith clang and__builtin_sqrtwith gcc, built with-fno-math-errno(novac passes it), so it never setserrnoor callssqrt. MSVC uses its C runtime'ssqrt.
- The interpreter uses .NET's
Math/MathFfunctions of the same names, which follow the same IEEE operations. - The eight names
bits,fromBits,sqrt,abs,floor,ceil,minandmaxare reserved on float types: an interface method of one of those names cannot be implemented for a float type (N0228). Wrong argument counts are N0209.
35.9 --no-float (M11 G7c; ADR 0011 §3.7)
novac build --no-floatandnovac check --no-floatreject every float type, float literal andf32.fromBits/f64.fromBitswith N0297. Every float value starts at one of those, so nothing of the program computes with floats.- It is meant for kernels and other code built without FP registers. CosmoOS's kernel builds pass it (
c4.pytoc8.py), andcheck-fpregs.shremains the backstop. Floats in the generic freestanding profile are otherwise allowed (cosmoos/profiles.md§1). - In the corpus, a line
// option: no-floatchecks a file this way (testing.md §3).
Diagnostics: none new. Mismatched float types, and float literals where an integer is expected, are N0205. A literal that overflows is N0206. Operators that are not defined on floats are N0207, and so are float match scrutinees and wrong argument or target types of the conversions. A lossy as is N0208. Wrong type-argument counts are N0216.
36. Function values and closures (M11 G8; ADR 0012)
36.1 Function types and values (G8a)
fn double(x: i32) -> i32 => x * 2;
fn negate(x: i32) -> i32 => -x;
fn applyTwice(f: fn(i32) -> i32, x: i32) -> i32 => f(f(x));
fn main() {
Console.writeLine(applyTwice(double, 5)); // 20
let pick: fn(i32) -> i32 = if (true) negate else double;
Console.writeLine(pick(3)); // -3
let id = identity<i64>; // a generic function, named with its type arguments
Console.writeLine(id(7));
}
fn identity<T>(x: T) -> T => x;The type
fn(T1, …, Tn) -> R(fn(T1, …, Tn)whenRisvoid) is a plain function value: one code address. It is Copy, needs no drop, holds no loans (it is not scoped), and is not FFI-safe: C function pointers stayextern "C" fn(…)(§29.5). Two function types are the same type when their parameter and result types are.Values. The name of a package function is a value of its function type:
- wherever a value can be (
let f = double;), or where a function type is expected, which must then equal the function's; - a generic function is named with its type arguments (
identity<i64>), giving that instance.
Methods are not values: they are only named through their type or receiver. These functions are not values either, N0298 (a new code):
externandunsafefunctions, since a call through a value is safe;- functions with region parameters (§25), since their signatures can store an argument's loans in another argument, which a function type cannot say;
main.
- wherever a value can be (
Calls.
f(args), withfa local, parameter or field of a function type, calls the function it holds. The arguments are checked against the parameter types as in any call (§8, §13.2). The call is safe. Its effects are the type's effect list (§36.3), elsesleepandalloc(§32.1), as for a C function pointer.References. Parameter and result types may be references or other scoped types. A reference result needs exactly one reference (or scoped) parameter: a function type has no names to write a source list with (N0245, §13.5). The borrow checker gives a call's result the loans of every reference argument. That is sound for every function the value can hold, because such a function's result can only come from its parameters (no region parameters, above).
Not in G8a:
==on function values (N0207). Closures are §36.2, and effect lists on function types §36.3.C. A function type is a C function pointer typedef
nvN_…over the functions' C signatures. A value is the function's address, and a call is an indirect call.
36.2 Closures (G8b)
fn countIf(values: Slice<i32>, keep: borrow fn(i32) -> bool) -> i32 {
var n = 0;
for (v in values) { if (keep(v)) { n += 1; } }
return n;
}
fn forEach(values: Slice<i32>, visit: borrow mut fn(i32)) {
for (v in values) { visit(v); }
}
fn main() {
let values = [3, 8, 1, 9, 4];
let limit = 4;
Console.writeLine(countIf(values[..], (v) => v > limit)); // 2: captures `limit`
var total = 0;
forEach(values[..], (v) => { total += v; }); // changes `total`: a borrow mut fn
Console.writeLine(total); // 25
let twice = (x: i32) => x * 2; // captures nothing: a plain fn(i32) -> i32
Console.writeLine(twice(21));
}- Literals (grammar.md §2):
(x: i32, y) => expressionor(x) => { statements }, with()for none. A(…)followed by=>is a closure; a parenthesised expression cannot be followed by=>. The body is a block or an expression, which extends as far right as an expression can (as for branches, §15.3). - Parameter types are written, or taken from an expected function or closure type. The result type is the expected type's, else the expression body's type, else
voidfor a block. A parameter without a type and without an expected type, or a block body that returns a value without an expected type, is N0236 (a new code). - Captures. The body may name the enclosing function's locals and parameters,
thisincluded, also from nested closures. Each one named is captured by reference:- mutably if the body assigns to it or to a place rooted at it, passes it as
mut, borrows it mutably, or calls aborrow mut thismethod or mutating collection method on it; - shared otherwise.
- Field-precise (M13 G5): a captured struct local that the body reaches only through its fields (
s.width,s.items[s.next]), never whole and not from a nested closure, is captured as one reference per field it names, so the closure borrows those fields and the enclosing code may use the others. The mode (shared or mutable) is still decided per local. A local behind a reference (this) is captured whole.
- mutably if the body assigns to it or to a place rooted at it, passes it as
- Types.
- A literal that captures nothing is a plain function value,
fn(…) -> R(§36.1). - One that captures is a closure:
borrow fn(…) -> Rif every capture is shared,borrow mut fn(…) -> Rif any is mutable. - A closure type is a reference (§13):
borrow fnis Copy, andborrow mut fnis unique and reborrowed at each call. Both are scoped, and allowed where references are (§13.1, §25): parameters,letlocals, fields of aggregates with region parameters (borrow(r) fn(…)), and elements of scoped collections. - In source,
borrow fn(…)always means a closure type: a reference to a plain function value cannot be written.
- A literal that captures nothing is a plain function value,
- Loans. A closure's value carries the loans of its captures, shared or mutable, and a loan of its environment. The environment is a hidden local of the block that contains the literal, holding references to the captures. So, by the rules that already apply to references (B1–B5,
lifetimes.md):- a captured variable cannot be changed, moved or ended while a closure that captures it is live;
- nothing else may use a mutably captured one then;
- a closure cannot be used after its block, nor returned from its function (N0243, N0244).
- Conversions.
- Where a closure type is expected, a plain function value converts to it (an adapter closure). For a named function the adapter calls it directly; for any other value it holds a copy and calls through it.
- A
borrow fnconverts to aborrow mut fn: calling a shared closure changes nothing. - A
borrow mut fnwhere aborrow fnis expected is N0299 (a new code): a shared copy could call it while it is changing what it captures. So is a capturing closure where a plainfnis expected.
- Calls
f(args)are as for function values (§36.1): safe, with the effects of the type's effect list (§36.3), elsesleepandalloc. - In the body,
returnreturns from the closure.breakandcontinuecannot leave it (N0214, as outside a loop).?propagates from the closure, whose result type must allow it (N0261). A captured value cannot be moved out: the body reaches it through a reference (N0251). - C. The environment is a struct
nvE_…whose first member is the code pointer, followed by one pointer per capture. The closure's value is a pointer to that first member (C11 6.7.2.1p15). The body is astaticfunction takingconst void *envfirst. A call is(*f)(f, args…).
36.3 Effect lists on function and closure types (G8b′; ADR 0012 §3.7)
extern "C" { @effects() fn spin_lock(l: *mut spinlock_t); @effects() fn spin_unlock(l: *mut spinlock_t); }
@context(atomic)
fn countUnderLock(lock: *mut spinlock_t, values: Slice<i32>, keep: borrow @effects() fn(i32) -> bool) -> i32 {
unsafe { spin_lock(lock); }
var n = 0;
for (v in values) { if (keep(v)) { n += 1; } } // allowed: `keep` neither sleeps nor allocates
unsafe { spin_unlock(lock); }
return n;
}
fn isEven(v: i32) -> bool => v % 2 == 0;
fn noisy(v: i32) -> bool { Console.writeLine(v); return true; }
fn demo(lock: *mut spinlock_t, values: Slice<i32>) {
let limit = 4;
let quiet: @effects() fn(i32) -> bool = isEven; // checked: `isEven` has no effects
countUnderLock(lock, values, (v) => v > limit); // the literal's body is held to @effects()
countUnderLock(lock, values, noisy); // N0295: 'noisy' may sleep: it does console I/O
}- Syntax (grammar.md §2):
@effects(e, …)written immediately beforefnin a function type, with eachesleeporallocas in §32.2:@effects() fn(i32) -> bool, and for closuresborrow @effects() fn(i32)andborrow mut @effects(alloc) fn(i32). No other attribute is allowed there (N0290), and onlyfnmay follow it (a syntax error otherwise): C function pointer types (extern "C" fn, §29.5) take no effect list. - The type. The effect list is part of a function or closure type:
@effects() fn(i32)andfn(i32)are different types. A type without a list may have both effects, sofn(i32)is the same type as@effects(sleep, alloc) fn(i32). Everything else (Copy, scoped, loans, C representation) is as in §36.1 and §36.2. - Calls through a value of such a type have the type's effects (§32.1). Without a list, that is
sleepandalloc. - Values. Every value of a type with an effect list has at most those effects:
- a named function used where such a type is expected (§36.1) is checked after effect inference: its inferred effects must be among the list (N0295, reported where it is named, with the chain of calls);
- a closure literal used where such a type is expected has the list as its body's upper bound, as
@effectsis on a function (§32.2; N0295 in the body). So does the adapter closure of a plain function value (§36.2); - otherwise a value converts only to a type whose list includes its own:
@effects() fn(i32)converts to@effects(alloc) fn(i32)and tofn(i32), andborrow @effects() fn(i32)toborrow fn(i32)(with the kind conversions of §36.2). The reverse is N0299: a value whose effects are not known to be in the list.
- Inference does not narrow types. A named function or closure literal with no expected type gets the type without a list, whatever its effects:
let f = isEven;is afn(i32) -> bool, which cannot be passed where an@effects() fn(i32) -> boolis expected. Write the type (let f: @effects() fn(i32) -> bool = isEven;). - C. The list changes nothing in C: the typedefs of two types that differ only in their lists name the same C type, so a conversion between them needs no cast.
36.4 Library methods taking closures (G8c; ADR 0012 §3.9)
fn main() {
var values = [5, 3, 9, 1, 4];
values[..].sortBy((a, b) => a - b); // 1 3 4 5 9
var total = 0;
values[..].forEach((v) => { total += v; }); // 22
Console.writeLine(values[..].any((v) => v > 8)); // true
Console.writeLine(values[..].all((v) => v % 2 == 1)); // false
var names = Vector<String>.create();
// … push some names …
names.sortBy((a, b) => a.view().length as i32 - b.view().length as i32); // move-type elements: fine
names.retain((n) => n.view().length > 3);
}- Methods. Each takes a closure (§36.2); a plain function value converts to one, so
s.sortBy(compare)works. The closure's parameters are references to the elements, so the element type may be any type, Copy or not:
| Method | Receiver | Closure type | Result |
|---|---|---|---|
s.sortBy(cmp) | SliceMut<T>, or a vector (lending its elements mutably) | borrow mut fn(borrow T, borrow T) -> i32 | void |
s.forEach(visit) | Slice<T> (a SliceMut<T> coerces), or a vector | borrow mut fn(borrow T) | void |
s.any(test), s.all(test) | as forEach | borrow mut fn(borrow T) -> bool | bool |
v.retain(keep) | a Vector<T, A> (a mutable place) | borrow mut fn(borrow T) -> bool | void |
The closure types are borrow mut fn, so a closure that changes what it captures may be passed, and so may a shared one (§36.2). A slicing expression a[lo..hi] is a receiver as for fill (§16.5): a[..].sortBy(cmp).
sortByorders the elements so thatcmp(borrow s[i], borrow s[i + 1]) <= 0for eachi, whencmpis a consistent total order: negative for "less", zero for "equal", positive for "greater".- It is not stable: equal elements may end in any order. The order chosen is a deterministic function of the input and of
cmp's results. - It allocates nothing, calls
cmpO(n log n) times in the worst case, and uses O(log n) stack: an introsort (quicksort with a median-of-three pivot; insertion sort below 16 elements; heapsort past a depth of 2·log₂ n). - If
cmpis not a consistent order, or changes what it captures, the elements end in some order: they are still a permutation of the input, and nothing panics or misbehaves.
- It is not stable: equal elements may end in any order. The order chosen is a deterministic function of the input and of
forEachcallsvisiton each element in index order.anycallstestin index order until it returnstrue(thentrue;falsefor an empty slice), andalluntil it returnsfalse(thenfalse;truefor an empty slice).retaincallskeeponce on each element in index order. It keeps the elements for which it returnedtrue, in their order, and destroys the others after the last call. The vector's capacity is unchanged.- Loans. The receiver is evaluated first and lent for the whole call (mutably for
sortBy,swapandretain), so the closure cannot capture it:v.retain((x) => x < v.length)is N0241, as for any other conflicting loan. - Effects. The methods are
@effectsOftheir closure (§32.5): a call has the closure's effects, plusretain's destruction of the elements it removes. Sos.sortBy((a, b) => a - b)may be used in@context(atomic)and@context(irq)code, ands.sortBy(compare)may too ifcompareneither sleeps nor allocates. - Implementation. The methods are Nova code in
lib/core/slices.nova: static functions of the generic structsSliceAlgorithms<T>andVectorAlgorithms<T, A>, which the method calls name (soSliceAlgorithms<i32>.sortBy(s, cmp)is the same call). They use only safe Nova andswap(§16.5).
36.5 Owning closures: move literals and dyn fn (M13 G3; ADR 0015 §3.6)
fn adder(n: u64) -> Box<dyn fn(u64) -> u64> {
return Box.create(move (x: u64) => x + n)!; // n is moved (here, copied) into the closure
}
fn counter() -> Box<dyn mut fn() -> u64> {
var count: u64 = 0;
return Box.create(move () => { count += 1; return count; })!;
}
fn main() {
let add2 = adder(2);
Console.writeLine(add2(3)); // 5
var next = counter();
next();
Console.writeLine(next()); // 2
var names = Vector<String>.create();
names.push(String.create())!;
let size: Box<dyn fn() -> usize> = Box.create(move () => names.length)!; // a closure owning the vector: Box<dyn fn() -> usize>
Console.writeLine(size()); // 1
}- Types.
dyn fn(T…) -> Randdyn mut fn(T…) -> R(with an optional effect list,dyn @effects() fn(…)) aredyntypes (§37): unsized, so they appear only behind a reference or in a box.Box<dyn fn(T…) -> R, A>is an owning closure: a move type that needs drop, which may be returned, stored in fields and collections, and outlive the function that made it.borrow fn(…)(§36.2) is unchanged and is not writtenborrow dyn fn. - Move literals.
move (x: T, …) => bodyis a closure literal that captures by move: each local it names is moved into the closure when the literal is evaluated, or copied if its type is Copy. Afterwards the local is moved out (§14.3), as after any move. The parameter rules,return,breakand?are those of §36.2. - Where. A move literal may only be the value argument of
Box.create(…)orBox.createIn(a, …)whose expected result is aBox<dyn fn…, A>or aBox<dyn mut fn…, A>. Its parameter and result types come from that type. Anywhere else it is N0333 (its own type is unnamed; OD-9a). - Captures. A move literal may not capture a scoped value (§19.1): a reference, a view, a slice, a closure, or a value with region parameters. That is N0332: the box would outlive the loan.
- Mutation. A body that assigns to a capture, borrows it mutably or calls a mutating method on it changes the closure's own copy. It makes the literal a
dyn mut fn: where adyn fnis expected, that is N0299. A captured value cannot be moved out of the body (N0251): it belongs to the closure, which may be called again. - Calls.
f(args)wherefis a place of typeBox<dyn fn…>,borrow dyn fn…, or a reference to such a box, calls the closure. Adyn mut fnneeds mutable access: avarbox, or aborrow mut, else N0248. A call has the effects of the type's effect list, elsesleepandalloc(§36.3). - Drop. Dropping the box destroys the captured values, in capture order, then frees the box.
- Representation. The closure's environment, a struct of the captured values after the code pointer, is the box's contents. The box is a
dynpair (§37.4) whose table has one entry, the body.
Diagnostics: N0332 (a scoped capture), N0333 (a move literal outside Box.create).
37. Dynamic dispatch: dyn (M13 G2; ADR 0015)
interface Shape {
fn area(borrow this) -> u64;
fn grow(borrow mut this, by: u64);
}
struct Square { side: u64; }
impl Shape for Square {
fn area(borrow this) -> u64 => this.side * this.side;
fn grow(borrow mut this, by: u64) { this.side += by; }
}
struct Rect { w: u64; h: u64; }
impl Shape for Rect {
fn area(borrow this) -> u64 => this.w * this.h;
fn grow(borrow mut this, by: u64) { this.w += by; }
}
fn total(shapes: borrow Vector<Box<dyn Shape>>) -> u64 {
var t: u64 = 0;
for (s in shapes) { t += s.area(); } // a call through the table
return t;
}
fn main() {
var shapes = Vector<Box<dyn Shape>>.create();
shapes.push(Box.create(Square { side: 3 })!)!; // Box<Square> coerces to Box<dyn Shape>
shapes.push(Box.create(Rect { w: 2, h: 5 })!)!;
Console.writeLine(total(shapes)); // 19
var sq = Square { side: 1 };
let r: borrow mut dyn Shape = borrow mut sq; // a reference coerces too
r.grow(2);
Console.writeLine(sq.side); // 3
}37.1 The type dyn I
- For an interface instantiation
I(§21:Shape,Comparable<i32>),dyn Iis the type of "a value of some type that implementsI". Its size is not known, so, like[T](§16), it is unsized: it appears only behind a pointer.borrow dyn Iandborrow mut dyn Iare references (§13). They are scoped, follow every reference rule, andborrow dyn Iis Copy.Box<dyn I, A>(andBox<dyn I>forSystemHeap) owns its value (§18.2). It is a move type that needs drop.- Anywhere else,
dyn Iis N0330: a local, parameter or result of typedyn I, a field, an array or vector element, an optional, a type argument (Vector<dyn I>;Vector<Box<dyn I>>is fine), or an element of a slice type.
- Dyn-compatible interfaces.
dyn Iis allowed only if every method ofItakesborrow thisorborrow mut this. A method that takesthisby value makes the interface incompatible:dyn Iis then N0331, which names that method. A generic interface is checked once its type arguments are given. dynis a keyword in types only (grammar.md §2).dyn Ibinds tighter thanborrow, soborrow dyn Iis a reference to adyn I.
37.2 Coercions
- A value converts to a
dyntype only where that type is expected: an annotatedlet, an argument, a field initialiser, areturn, or an argument of a collection method such aspush. Inference never invents adyntype.borrow T→borrow dyn I, andborrow mut T→borrow mut dyn I(orborrow dyn I);Box<T, A>→Box<dyn I, A>: a move of the box, with no new allocation. The allocator type must be the same.
Tmust implementI(§21.1), else N0268, as for an unsatisfied bound.Tis never scoped (§19.1): no references, closures, views, slices or region parameters, since only an unscoped type can implement an interface (§21.1). So adynvalue carries no loans of its own:borrow dyn Icarries only the loan of the reference it came from, andBox<dyn I>carries none. (Scopeddyn(r) Ivalues are OD-10a.)Tmust be a sized type: a struct, an enum, or a built-in type such asi32. Adyn Idoes not coerce to anotherdyntype, and there is no conversion back toT(no downcasting; OD-10b).
37.3 Calls
x.m(args), wherexis aborrow dyn I, aborrow mut dyn I, aBox<dyn I, A>, or a place of one of these types, calls the methodmofIthrough the value's table. The method is that ofT'simpl I for T.The receiver rules are those of a reference to
T(§13, §21.1):- a
borrow thismethod may be called through any of the three; - a
borrow mut thismethod needs mutable access: aborrow mut dyn I, or a mutable place ofBox<dyn I, A>. Through aborrow dyn Iit is N0248, as for any value behind a shared reference.
- a
Only
I's methods can be called.T's other methods and fields are not visible throughdyn I(N0234).Effects (§32). An interface method may carry an effect list, which bounds every
implof it:novainterface Visitor { @effects() fn visit(borrow mut this, value: u64); }An
implmethod whose effects exceed its interface's list is N0295, reported at theimplmethod. A call throughdyn Ihas the effects ofI's list, orsleepandallocwithout one, as for a closure without a list (§36.3). A call on a genericT: Iis unchanged: it has the effects of the instantiated method (§21.2).Drop. Dropping a
Box<dyn I, A>destroys its value withT's drop glue, then frees it withA.
37.4 Representation (c-backend.md §2)
- A
borrow dyn I, aborrow mut dyn Iand aBox<dyn I, A>with a zero-sizedAare each two words:{ data, vt }.datapoints at the value, andvtpoints at the table for (T,I). - The table is a
static conststruct per pair (T,I) that a coercion names. It holds the drop glue, the value's size and alignment (for a user allocator'sdeallocate), then one function per method ofI, in declaration order. Each function is a generated adapter takingvoid *as its receiver, so no function is called through a pointer of another type. dyn Ivalues have no address identity beyonddata. Twoborrow dyn Ivalues are not comparable (N0209).
Diagnostics: N0330 (dyn I not behind a pointer), N0331 (an interface that is not dyn-compatible).
38. Threads (M13 G4; ADR 0016)
struct Counter { n: u64; }
fn main() {
let counter = Arc<Mutex<Counter>>.create(Mutex<Counter>.create(Counter { n: 0 })!)!;
var workers = Vector<Thread<u64>>.create();
for (i in 0..4u64) {
let mine = counter.clone(); // another owner, moved into the thread
workers.push(Thread.spawn(move () => {
var guard = mine.get().lock();
let data = guard.data();
data.n += i + 1;
return i * 10;
})!)!;
}
var total: u64 = 0;
loop {
match (workers.pop()) {
Some(t) => { total += t.join(); }
null => { break; }
}
}
Console.writeLine(total); // 60
var guard = counter.get().lock();
Console.writeLine(guard.data().n); // 10
}- Static functions of the library's generic types take their type arguments, as for any generic struct (§20.2):
Arc<Mutex<Counter>>.create(…).
38.1 Send and Share
- Two properties of types, inferred and never declared by user code (v1), which may also be used as bounds, like
Copy(§21.2):<T: Send>,<T: Send + Share>. Send: a value may be moved to another thread. It holds for:- integers, floats and
bool; - structs and enums whose fields and payloads are all
Send, and arrays of aSendelement; Box<T>,Vector<T>,String,SlotMap<T>,StableVector<T>andHandle<T>, withT: Sendand the system heap (orFailingAllocator);File,TcpListener,TcpStream;Box<dyn I + Send>andBox<dyn fn(…) -> R + Send>(§38.4);- the library's thread-safe types (§38.3):
Arc<T>,Mutex<T>, the atomics.
- integers, floats and
- It fails for:
- references, slices, views and every other scoped type (§19.1);
- closures (
borrow fn), function values aside (fn(…)isSend: a code address); - raw pointers and imported C types;
- a box or collection with a user allocator;
- regions and pools;
Thread<T>(§38.2);Box<dyn I>andBox<dyn fn…>without+ Send.
Share: shared references to a value may be used from several threads at once. It holds for everySendtype, since no v1 type can be changed through a shared reference except the library's, and those areShareby design. The exception isThread<T>.- Inference is structural and recursive. A recursive type is
Sendunless a path through it reaches a type that is not. Generic code knows only its bounds: aTisSendonly under aT: Sendbound.
38.2 Spawning and joining
Thread.spawn(move () => body)starts a thread that runs the move literal's body (§36.5) and returnsResult<Thread<T>, SpawnError>, whereTis the body's result type.SpawnErroris the enumSpawnError.Resources(the thread or its closure could not be allocated).- The literal takes no parameters. Its captures must be
Send(N0334), and so mustT(N0334). Thread.spawnalso takes aBox<dyn fn() -> T + Send>.- A move literal is allowed here as well as in
Box.create(amending §36.5).
- The literal takes no parameters. Its captures must be
Thread<T>is a move type that needs drop.t.join()consumes it, waits for the thread to finish, and returns itsT. Dropping aThread<T>that was not joined waits for it and destroys its result. There are no detached threads: every thread a program starts has ended beforemainreturns.- A panic in any thread aborts the process (OD-5).
- Effects (§32):
spawnhassleepandalloc.join, and dropping aThread<T>, havesleep.
38.3 Shared state (library types: lib/std/sync.nova, and the atomics in lib/core/atomics.nova)
Arc<T: Share>: shared ownership.Arc.create(v)returnsResult<Arc<T>, AllocError>.a.clone()returns another owner of the same value, anda.get()returns aborrow T(shared only).- The count is atomic. The last owner's drop destroys the value and frees it.
Mutex<T: Send>: mutual exclusion.Mutex.create(v)returnsResult<Mutex<T>, AllocError>; the platform lock is allocated.m.lock()waits for the lock and returns aMutexGuard<T>, scoped tomlike C7's guards. Itsdata()lendsborrow mut T, and its drop unlocks.lockhas thesleepeffect.
- Atomics:
AtomicU32,AtomicU64,AtomicUSizeandAtomicBool, in every profile (ADR 0026). Each operation is a static inline function of the runtime header, so it compiles to the machine's atomic instructions.create(v), thenload(),store(v),swap(v)andcompareExchange(expected, new)(returnsResult<T, T>,Errwith the current value).- The integer types also have
fetchAdd(v)andfetchSub(v), which wrap. - Every operation takes
borrow this, is sequentially consistent, and has no effect. - With an ordering (ADR 0021):
loadWith(o: LoadOrdering),storeWith(v, o: StoreOrdering), andswapWith,compareExchangeWith,fetchAddWith,fetchSubWithtakingo: Ordering(AtomicBoolhas the first four). The enums areLoadOrdering { Relaxed, Acquire, SequentiallyConsistent },StoreOrdering { Relaxed, Release, SequentiallyConsistent }andOrdering { Relaxed, Acquire, Release, AcquireRelease, SequentiallyConsistent }, with C11's meanings (C11 §7.17.3). A load cannot be given release ordering, nor a store acquire, which C11 leaves undefined. A compare-exchange that fails orders as a load:AcquireReleaseasAcquire,ReleaseasRelaxed. - An ordering changes which values an operation may observe, never whether two accesses race: every access is still atomic, so T1–T3 are unchanged. A too-weak ordering is a logic error that neither the interpreter (which runs one thread at a time, a sequentially consistent execution) nor ThreadSanitizer can find.
- These types are
SendandShareby the library's declaration (@threadSafe, accepted only in the library's own files). Theirunsafeinternals are argued inownership.mdT1–T3.
38.4 dyn … + Send
dyn I + Sendanddyn fn(…) -> R + Send(and withmut) aredyntypes (§37, §36.5) whose boxes areSend. A coercion to one needsT: Send, which for a move literal meansSendcaptures (N0334). Adyn … + Sendconverts to the samedyntype without+ Send(the same value: they share their C representation and tables), never the other way.Thread.spawn(f)withf: Box<dyn mut fn() -> T + Send>runs a closure made elsewhere.
38.5 Profiles
- Threads,
ArcandMutexare in the hosted profile: hosts with pthreads or Win32 threads, and CosmoOS userland overcosmo_thread_*andcosmo_mutex_*(cosmoos/profiles.md). In a freestanding build their names are N0293. The kernel uses lib/kernel's locks (ADR 0025). The atomics and their orderings are inlib/core, in every profile (ADR 0026). lib/std/is the hosted library: compiled into hosted packages only. Likelib/core(§29.2), its names cannot be redeclared, and only what a program uses is emitted.
38.6 Fork-join: Thread.both (OD-25; ADR 0018)
Thread.both(a, b)runsaon a new thread andbon the calling thread, waits until both have finished, and returnsvoid. It is in the hosted profile, withThread(§38.5).- Each argument is a closure literal with no parameters and a
voidresult (§36.2), or a function value of typefn()(§36.1). A closure that is not a literal (a variable or a parameter of typeborrow fn()) is refused (N0334): its captures cannot be seen, and the rule below is about them. - Results travel through captures:
Thread.both(() => { low = sum(halves.left); }, () => { high = sum(halves.right); });
- Each argument is a closure literal with no parameters and a
- Captures.
aruns on another thread whilebruns on this one, so each capture of either closure must be safe to use from another thread at the same time:- a capture used shared needs a
Sharetype, where, for this rule only,borrow T,Slice<T>andStringViewareSharewhenTis; - a capture used mutably needs a
Sendtype, where, for this rule only,borrow mut TandSliceMut<T>areSendwhenTis; - a captured closure is refused;
- otherwise N0334, naming the capture and its type. These extended rules apply here only: outside
Thread.bothreferences are neitherSendnorShare(§38.1).
- a capture used shared needs a
- Loans. Both closures are arguments of one call, so both, and every loan they carry, are live for the whole call (§36.2): the borrow checker refuses captures of one value by both where either changes it (B-rules, N0241), and nothing either captures can be changed, moved or ended until
bothreturns. - No failure. If the new thread cannot be started (its runtime record cannot be allocated, or the platform refuses),
bothrunsaand thenbon the calling thread. The result is the same either way, sobothreturnsvoidand a program cannot tell whetheraran in parallel. - A panic in either closure aborts the process (OD-5).
- Effects (§32):
sleepandalloc.
Diagnostics: N0334 (a value that is not Send where one must cross to another thread; for Thread.both, a capture that may not be used from two threads at once, or an argument that is not a literal or a function value).
39. Static items (M14 K1; ADR 0024)
static OPENED: AtomicU64 = AtomicU64.constant(0);
static NAME: StringView = "nova";
fn openOne() -> u64 => OPENED.fetchAdd(1) + 1;
fn opened() -> borrow(static) AtomicU64 => borrow OPENED;
static mut BOOT_FLAGS: u32 = 0;
/// Safety: called once on the boot CPU, before anything else reads BOOT_FLAGS.
unsafe fn setBootFlags(flags: u32) { BOOT_FLAGS = flags; }39.1 The item
static NAME: T = e;andstatic mut NAME: T = e;at package level, with an optional visibility, likeconst(§10). The type is required. A static is never generic, and a function cannot declare one (N0337).- A static is one value for the life of the program, at a fixed address. It is never dropped: a
Twith adropis allowed, and its drop never runs.
39.2 static
Tmust beShare(§38.1), because every thread may use a static (N0337).NAMEin an expression denotes the place*S, whereSis a reference to the static of typeborrow(static) T.staticis the built-in region of §25.1, which outlives everything. So a static is used exactly as a value behind a shared reference is:- it can be read when
TisCopy, borrowed (borrow NAME,borrow NAME.field), and its methods takingborrow thiscalled; - it cannot be assigned or moved, and nothing reached through it can be changed (§13.2: N0211, N0248; a move out of it is N0251, as for any place behind a reference);
- a reference to it, or to anything in it, has the
staticregion. It may be returned from a function whose return type namesstatic(borrow(static) T, §13.5), stored, and sent to another thread.
- it can be read when
- Mutation goes through
T's own synchronisation: an atomic's operations, a lock's guard.
39.3 static mut
Tneed not beShare.NAMEdenotes the place*SwithS: borrow(static) mut T, so it can be read, assigned, borrowed mutably and moved out of as such a place can.- Every use is an unsafe operation (§27.3), N0280 outside an unsafe context. The unsafe code's
/// Safety:comment must argue what the checker cannot. Two uses give two references that may alias, and two threads may race on it (unsafe.mdU3).
39.4 Static initialisers
e is evaluated at compile time to a C constant initialiser. It must be a static initialiser (N0338 otherwise):
- a constant expression of an integer,
boolor float type (§10), or a constant's name; "…".ptr as *T, a string literal's address, for a C string ("name\0".ptr as *CChar; §28);- a string literal, as a
StringView; - a struct literal (§12) whose fields are static initialisers. Private fields may be given only where §12 allows it: inside the struct's own functions, so inside its
@constantconstructors (below); - an array literal or repeat (
[e; N]) of static initialisers; - a call of a
@constantfunction whose arguments are static initialisers; - a call of a C constant macro whose arguments are static initialisers or
here(below); layoutOf<T>()(§30.1).
Not in v1: enum values (including optionals), references other than the static string data above, closures, and dyn. Each of these is N0338.
@constant functions. @constant on a function or method (generic or not) makes it callable in a static initialiser. Its body must be a single expression (=> e, or { return e; }) that is a static initialiser in which its parameters may also appear (N0338). It is an ordinary function too, callable at run time with the same result. A call in an initialiser substitutes the arguments for the parameters, and instantiates generic code as any call does.
C constant macros. @constant on a function in an extern "C" block (§28.2) declares a C macro that expands to a constant initialiser of its result type: @constant fn SPINLOCK_INIT(name: *CChar) -> spinlock_t;. It may be called only in static initialisers and @constant bodies (N0338 elsewhere). Nova trusts the declaration, as it trusts an extern's @effects (R-2).
here (ADR 0027). As an argument of a C constant macro, here is the address of the place the macro's result initialises, with type *mut T for the macro's result type T (N0338 if the parameter's pointee is another type). Through nested @constant constructors the place is the whole path: in static TABLE: Mutex<Table> = Mutex<Table>.constant(name, data), the constructor's MUTEX_INIT_AT(here, name) is given the address of TABLE's field raw. So a macro may build C state that points to itself, as CosmoOS's list heads, wait queues and mutexes do. here is a name, not a keyword: a variable or item called here hides it, and outside static construction it is an ordinary unknown name (N0200). Anywhere else in a static initialiser or a @constant body, including as an argument of a @constant function, it is N0338. A function that uses it builds C state, so it is compile-time only (below).
C state built in place (§33.3). A type with a @placed init may be in a static only through a @constant constructor, whose initialiser must leave the value as init would: for CosmoOS's locks, the C macro does. init does not run for a static.
C state at compile time (§33.1–§33.2). C state is otherwise only made in place, by Box.create, and never held as a value. A static initialiser is the other way to make it, at the static's fixed address, so three places may hold it:
- a static's type, which may hold C state: a static never moves;
- a C constant macro's result type, an imported C type (not FFI-checked: it is C's initialiser, not a value crossing a call; §28.3);
- a
@constantfunction's result type, and its struct literal, which may give a C-state field the result of a C constant macro.
A @constant function whose result holds C state is compile-time only: it is evaluated in static initialisers and other @constant bodies, called anywhere else is N0338, and it is never compiled to C.
39.5 Lowering
- A static is a C object at file scope named
nvS_<name>(with the package's name for a dependency's, as in §2.1 ofc-backend.md),staticunless--splitplaces it innova_instances.cand renames it (c-backend §2.4). It is notconstin C, because its methods may write through the pointers they derive from it. - Its initialiser is the evaluated value: a designated initialiser for a struct (
{ .f_count = 0 }), braces for an array, the macro call for a C constant macro,(const char *)"…"for a string literal's address, and a string view literal for aStringView.hereis(&nvS_<name>.<field>…), an address constant (C11 §6.6p9), andlayoutOf<T>()of a C-laid-outTis{ .f_size = sizeof(…), .f_align = _Alignof(…) }. Sis&nvS_<name>.- The interpreter evaluates every initialiser before
maininto its memory. A C constant macro has no value it can compute, so a program whose statics use one cannot be interpreted (a fault at start).
Diagnostics: N0337 (a static whose type is not Share, or one that is generic or local), N0338 (an initialiser or @constant body that is not a static initialiser, or a C constant macro used elsewhere), N0280 (a use of a static mut outside an unsafe context).