# Nova > Nova is a memory-safe systems programming language with C#- and TypeScript-style syntax, compiled ahead of time to > portable C11. Ownership and borrowing are checked at compile time; there is no garbage collector. It targets > operating-system kernels (CosmoOS, and C codebases such as Linux) as well as ordinary programs. Agents writing Nova: read the agent guide first, and use `novac mcp` (an MCP server with a checker, fixes, a sandboxed interpreter and this documentation) or `novac check --diagnostics-format=json` and `novac fix`. # What Nova is Nova is a memory-safe systems programming language. Its syntax will look familiar if you know C# or TypeScript; its safety comes from ownership and borrowing, as in Rust. It is compiled ahead of time to portable C11, so it runs wherever a C compiler does: on Linux, macOS and Windows, on bare metal, and inside an operating system kernel. ```nova struct Account { id: u64; balance: i64; public fn deposit(borrow mut this, amount: i64) { this.balance += amount; } drop { Console.write("closing account "); Console.writeLine(this.id); } } fn main() { var account = Account { id: 7, balance: 0 }; account.deposit(100); Console.writeLine(account.balance); } ``` ```text 100 closing account 7 ``` ## What it promises In safe code, which is all code outside an `unsafe` block, Nova rules out: - **use after free and double free:** every value has one owner, and it is destroyed exactly once; - **dangling references:** a reference cannot outlive what it refers to, which the compiler checks without lifetime annotations in most code; - **data races between a reference and a write:** while something is borrowed it cannot be changed, and while it is borrowed mutably nothing else can read it; - **out-of-bounds access and reading uninitialised memory:** indexes are checked, and a variable must be assigned before it is read; - **silent integer overflow:** arithmetic that overflows stops the program, in every build. Where wrapping is meant, the code says so (`x.wrappingAdd(y)`). It does this **without a garbage collector or reference counting.** Destruction is deterministic: a value is destroyed at the end of its scope, or when its owner is, in an order the language defines. ## What makes it different - **Lifetimes without annotations, mostly.** A function that takes and returns references names the parameters its result borrows from (`-> borrow(a, b) T`). Only types that store references, and functions over them, declare region parameters. - **Allocation is explicit and pluggable.** Each allocation chooses its allocator: the system heap, an arena, a pool, a fixed buffer, a scratch frame, or your own. Allocation failure is a value (`Result`), handled or propagated with `?`. - **The compiler shows its work.** It can explain which boxes it placed on the stack and which bounds and overflow checks it removed, and why. - **C you can read.** The generated C is structured and warning-free, maps back to Nova lines for debuggers, and has no undefined behaviour by design. A reference interpreter runs the same programs and stops on any memory error, which makes it a second implementation to test the compiler against. - **Built for kernels too.** A freestanding profile without a C library, context effects that reject sleeping while a spin lock is held, C structs imported from the C codebase's own headers, and lock guards and reference-counted kernel objects. All of this is exercised inside [CosmoOS](https://github.com/vkuttyp/CosmoOS), an operating system written in C. ## Status Nova is **pre-1.0**. The language and its tools work end to end, are tested by about 3,000 tests, a random program generator and benchmarks in six languages, and run inside CosmoOS on x86-64 and AArch64. But the language is still changing, there is one implementation, and nobody has used it in production. Its milestones, what is done and what is next are in the [roadmap](/reference/roadmap). Some things it does not have yet: trigonometry and logarithms, iterators beyond `for` over arrays, slices and vectors. Each is planned or recorded as an [open decision](/reference/open-decisions). ## Where to go next - [Install the compiler](./installation) and [write a first program](./getting-started). - Read [the language guide](./basics), chapter by chapter. - Coming from another language? Start with [Nova for Rust, Zig, C, C# and Go programmers](./coming-from/). - The full rules are in the [reference](/reference/): the grammar, the type system and the ownership rules. # Installation ## Install novac On macOS and Linux: ```sh curl -fsSL https://vkuttyp.github.io/Nova/install.sh | sh ``` On Windows, in PowerShell: ```powershell irm https://vkuttyp.github.io/Nova/install.ps1 | iex ``` The script downloads the latest release of `novac` for your machine from [GitHub](https://github.com/vkuttyp/Nova/releases), checks its SHA-256, and installs it in `~/.nova/bin` (`%USERPROFILE%\.nova\bin`). On macOS and Linux it then tells you how to add that directory to your `PATH`; on Windows it adds it for you. `NOVA_VERSION=v0.1.0` installs a given release instead. Run the same line again to update. `novac` is one native executable: it needs no .NET or other runtime. The macOS builds are signed and notarized by Apple. The Linux builds need glibc 2.35 or newer (Ubuntu 22.04, Debian 12, Fedora 36 and later). ```sh novac --version ``` ## Requirements | | For | Notes | |---|---|---| | A C compiler: clang, gcc or MSVC | `novac build` | clang and gcc are fully supported; MSVC is best-effort (`/std:c11`). `check`, `run --interp`, `fmt`, `lsp` and `mcp` need none | | git | dependencies from git repositories | only for [packages](./packages) that use them | | [.NET 10 SDK](https://dotnet.microsoft.com/download) | building `novac` from source | not needed to install or use a release | | Node.js and [Vite+](https://viteplus.dev/) | the VS Code extension, this site | optional | ## Build from source To work on Nova itself, or on a platform without a release, build `novac` from the repository: ```sh git clone https://github.com/vkuttyp/Nova.git cd Nova dotnet build src/Nova.Cli -c Release ``` The compiler is `src/Nova.Cli/bin/Release/net10.0/novac` (`novac.exe` on Windows). It finds its runtime and its core library next to itself, so it can be run from anywhere. To check everything, run the test suite (about 3,300 tests; ten minutes or so, since many compile and run C): ```sh dotnet test Nova.slnx ``` ### Put a source build on your PATH A wrapper script keeps following the build when you rebuild. On macOS and Linux, with `~/.local/bin` on your `PATH`: ```sh cat > ~/.local/bin/novac < hello.nova <<'EOF' fn main() { Console.writeLine("Hello, Nova"); } EOF novac build hello.nova -o hello && ./hello # through C, to a native program novac run --interp hello.nova # or in the reference interpreter ``` `novac build` uses the C compiler in `$CC`, or `cc`; `--cc clang` picks one. On Windows it finds one itself: clang (`winget install LLVM.LLVM`), MinGW's gcc, or Visual Studio's `cl`, and it sets up Visual Studio's headers and libraries for clang and `cl` when you are not in a developer prompt. So with LLVM and Visual Studio's "Desktop development with C++" workload (or its Build Tools) installed, `novac build` works from any terminal and makes `out\hello.exe` ([tooling](/reference/tooling) §8). ## Editor support The repository has a VS Code extension in `editors/vscode`: syntax highlighting, and through `novac lsp` errors as you type, hover, go to definition, the outline and formatting. See [Editors](./editors) to build and install it, and to use the language server from other editors. # Getting started This page makes a package, builds and runs it, and shows the commands you will use every day. It assumes `novac` is [installed](./installation) and on your `PATH`. ## A new package ```sh novac new hello cd hello ``` `novac new` writes a manifest, a first source file, and instructions for AI coding agents: ```text hello/ ├── nova.toml ├── src/ │ └── main.nova ├── AGENTS.md └── .gitignore ``` `AGENTS.md` tells an agent working in the package what Nova is and how to check its work. With `novac new hello --agent claude` (or `gemini`, `copilot`, `cursor`) it also writes that agent's own file and configures Nova's MCP server for it; `--no-agent` writes none. See [Nova for AI agents](./for-agents). `nova.toml` names the package and says what it builds: ```toml [package] name = "hello" version = "0.1.0" kind = "executable" [dependencies] ``` and `src/main.nova` is a program: ```nova fn main() { Console.writeLine("Hello from hello"); } ``` ```text Hello from hello ``` ## Build and run Inside a package, the commands need no file names: ```sh novac build # compiles to C, then to out/hello ./out/hello novac run --interp # runs the program in the reference interpreter instead novac check # type and borrow checks only: the fastest way to find errors novac fmt # formats the package's sources in the canonical style ``` Every `.nova` file under `src/` belongs to the package, and all of them share one scope: a function declared in one file can be called from another without an import. ## A slightly larger program Replace `src/main.nova` with: ```nova /// The number of steps the Collatz sequence takes from `n` to 1. fn steps(n: u64) -> u32 { var value = n; var count: u32 = 0; while (value != 1) { value = if (value % 2 == 0) value / 2 else 3 * value + 1; count += 1; } return count; } fn main() { var longest: u64 = 1; for (n in 1..1000u64) { if (steps(n) > steps(longest)) { longest = n; } } Console.write("longest below 1000: "); Console.write(longest); Console.write(", "); Console.write(steps(longest)); Console.writeLine(" steps"); } ``` ```text longest below 1000: 871, 178 steps ``` `3 * value + 1` is checked: if it overflowed a `u64`, the program would stop with a message naming the line, in every build. Nova never wraps silently. ## When the compiler says no Errors name a code, the place, and usually the fix. This program keeps a reference to a vector's element and then grows the vector, which could move the element: ```nova fn main() { var numbers = Vector.create(); numbers.push(1)!; let first = borrow numbers[0]; numbers.push(2)!; Console.writeLine(first); } ``` ```text main.nova:5:5: error[N0241]: cannot borrow 'numbers' as mutable while 'numbers[0]' is borrowed | 5 | numbers.push(2)!; | ^^^^^^^ = note: 'numbers[0]' is borrowed here (4:5) = note: the borrow is used later here (6:5) ``` The fix is to finish with `first` before the push, or to copy the value (`let first = numbers[0];`). Every code is explained in [Diagnostics](/reference/diagnostics). ## Files without a package For a quick experiment, a package is not needed: give the files to the command. ```sh novac build tool.nova helpers.nova -o tool novac run --interp tool.nova ``` ## Next - [Basics](./basics): values, types, functions and control flow. - [Packages](./packages): dependencies, the lock file, and sharing code. - [The novac command](./novac): every command and option. # Nova for AI agents This page is for AI agents that write Nova, and for the people who set them up. Nova is new, so a model has seen little of it: it will write C#, TypeScript or Rust that looks like Nova and is not. The compiler is built to correct that quickly. It says exactly what is wrong, fixes what it is sure of, and serves its own specification. Machine-readable versions of this site: [llms.txt](https://vkuttyp.github.io/Nova/llms.txt) (an index) and [llms-full.txt](https://vkuttyp.github.io/Nova/llms-full.txt) (the guide and the specification in one file). Every page is also available as Markdown at its address with `.md` added. ## In a new package `novac new` writes `AGENTS.md`, which most coding agents read: what Nova is, the package's layout, the commands that check the work, how to use the MCP server, and the common mistakes below. Name your agent to get its own file and its MCP configuration too: ```sh novac new hello --agent claude # also CLAUDE.md (importing AGENTS.md) and .mcp.json ``` | `--agent` | Writes | |---|---| | `agents` (the default) | `AGENTS.md` | | `claude` | `CLAUDE.md`, `.mcp.json` | | `gemini` | `GEMINI.md`, `.gemini/settings.json` | | `copilot` | `.github/copilot-instructions.md`, `.vscode/mcp.json` | | `cursor` | `.cursor/rules/nova.mdc`, `.cursor/mcp.json` | Nova's part of `AGENTS.md` sits between `` and ``. After updating `novac`, run `novac agents` in the package to refresh that part; whatever you wrote outside it stays as it is. ## Set up the MCP server `novac mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Add it to your agent once: ```sh claude mcp add nova -- novac mcp # Claude Code ``` or, for any MCP client: ```json { "mcpServers": { "nova": { "command": "novac", "args": ["mcp"] } } } ``` Its tools: | Tool | What it does | |---|---| | `nova_check` | type-checks code and returns its diagnostics as JSON, with fixes | | `nova_fix` | applies every fix the compiler is sure of, and returns the files and what remains | | `nova_format` | formats a file in the one canonical style | | `nova_run` | runs a program in a sandboxed interpreter: no files, sockets or processes, and a step and memory budget | | `nova_emit_c` | shows the generated C, and where allocations, bounds checks and overflow checks went | | `nova_explain` | what a diagnostic code means | | `nova_spec_search`, `nova_spec_section` | search and read the specification, which is embedded in `novac` | | `nova_symbol` | the type and declaration of what is at a position | The prompt `write-nova` gives an agent this page and the loop below. The specification and this guide are resources (`nova://docs/…`). The [tooling reference](/reference/tooling) §6 has the details. ## The loop 1. Write the code. 2. Check it: `nova_check`, or `novac check --diagnostics-format=json`. 3. Apply the fixes: `nova_fix`, or `novac fix`. A fix is offered only where the compiler is sure of it: `mut` before an argument, `let` to `var`, `truncate(…)` for a narrowing cast, a missing `;`. 4. Fix the rest yourself. Each diagnostic has a code; `nova_explain` (or `novac explain N0249`) says what it means, and the message often names the specification section to read. 5. Repeat until there are no errors. Then run it with `nova_run`, or build it with `novac build`. Without MCP, the same loop works from a shell: `novac check --diagnostics-format=json main.nova` prints one JSON document with every diagnostic, its position and its fixes, and `novac fix main.nova` applies them. ## Common mistakes These are the mistakes models make most often in Nova, each with the compiler's answer and what to write instead. ### There is no `~` ```nova fn invert(x: u32) -> u32 => ~x; ``` ```text main.nova:1:29: error[N0102]: expected expression, found '~' | 1 | fn invert(x: u32) -> u32 => ~x; | ^ ``` Complement with an exclusive or of all ones: ```nova fn invert(x: u32) -> u32 => x ^ 0xFFFFFFFF; ``` ### There are no type aliases ```nova type Count = u32; ``` ```text main.nova:1:1: error[N0101]: expected 'fn', 'const', 'struct' or 'enum', found 'type' | 1 | type Count = u32; | ^^^^ ``` Write the type itself, or wrap it in a struct when it deserves a name of its own. ### There are no tuples ```nova fn main() { let (a, b) = (1, 2); } ``` ```text main.nova:2:9: error[N0100]: expected identifier, found '(' | 2 | let (a, b) = (1, 2); | ^ ``` Declare two variables, or return a struct with named fields. ### Arrays are not compared with `==` ```nova fn same(a: [u8; 4], b: [u8; 4]) -> bool => a == b; ``` ```text main.nova:1:44: error[N0207]: operator '==' cannot be applied to type '[u8; 4]' | 1 | fn same(a: [u8; 4], b: [u8; 4]) -> bool => a == b; | ^^^^^^ ``` Compare element by element: ```nova fn same(a: [u8; 4], b: [u8; 4]) -> bool { for (i in 0..a.length) { if (a[i] != b[i]) { return false; } } return true; } ``` ### Indexes are `usize`, and a range's type comes from its bounds ```nova fn sum(values: Slice) -> u64 { var total: u64 = 0; for (i in 0..4) { total += values[i]; } return total; } ``` ```text main.nova:4:25: error[N0205]: expected 'usize', found 'i32' | 4 | total += values[i]; | ^ ``` `0..4` is a range of `i32`. Use the length, which is a `usize`, or a typed literal: ```nova fn sum(values: Slice) -> u64 { var total: u64 = 0; for (i in 0..values.length) { total += values[i]; } return total; } ``` ### A block is not a value ```nova fn sign(x: i32) -> i32 { let s = if (x < 0) { -1 } else { 1 }; return s; } ``` ```text main.nova:2:28: error[N0100]: expected ';', found '}' | 2 | let s = if (x < 0) { -1 } else { 1 }; | ^ main.nova:2:39: error[N0100]: expected ';', found '}' | 2 | let s = if (x < 0) { -1 } else { 1 }; | ^ ``` An `if` or `match` expression's branches are expressions, written without braces: ```nova fn sign(x: i32) -> i32 { let s = if (x < 0) -1 else 1; return s; } ``` ### A temporary is not sliced or measured ```nova const TABLE: [u8; 4] = [1, 2, 3, 4]; fn main() { Console.writeLine(TABLE.length); } ``` ```text main.nova:4:23: error[N0216]: 'length' needs an array variable or field; bind the value to a local first | 4 | Console.writeLine(TABLE.length); | ^^^^^^^^^^^^ ``` Bind it to a local first, as the message says: ```nova const TABLE: [u8; 4] = [1, 2, 3, 4]; fn main() { let table = TABLE; Console.writeLine(table.length); } ``` ```text 4 ``` ### A function in a field is not called through the field ```nova struct Counter { step: fn(i32) -> i32; } fn advance(c: borrow Counter, n: i32) -> i32 => (c.step)(n); ``` ```text main.nova:5:49: error[N0210]: this expression cannot be called | 5 | fn advance(c: borrow Counter, n: i32) -> i32 => (c.step)(n); | ^^^^^^^^ ``` Pass the function or closure as a parameter, and call the parameter: ```nova fn advance(step: fn(i32) -> i32, n: i32) -> i32 => step(n); ``` ### A borrowed string in an enum needs a region ```nova enum Failure { Refused(StringView), Io, } ``` ```text main.nova:2:13: error[N0270]: a payload of scoped type 'StringView' needs a region parameter on the enum: 'enum Failure' (type-system.md §25.4) | 2 | Refused(StringView), | ^^^^^^^^^^ ``` A `StringView` borrows its text, so a type holding one says from where. For fixed messages, an enum of reasons and a function that words them is simpler: ```nova enum Reason { TooLarge, BadMagic, } enum Failure { Refused(Reason), Io, } fn describe(reason: Reason) -> StringView => match (reason) { Reason.TooLarge => "too large", Reason.BadMagic => "bad magic number", }; ``` ### A method cannot share a field's name ```nova struct Account { balance: i64; fn balance(borrow this) -> i64 => this.balance; } ``` ```text main.nova:4:8: error[N0228]: 'Account' already has a member named 'balance' | 4 | fn balance(borrow this) -> i64 => this.balance; | ^^^^^^^ ``` Name the method for what it does (`current`), or make the field `public`. ### What the compiler fixes for you `nova_fix` and `novac fix` repair these without being asked twice: a `let` that is assigned (`var`), an argument for a `borrow mut` parameter without `mut`, a `mut` where the parameter is a shared `borrow`, `as` between integer types that can lose information (`truncate(…)`, which wraps), and a `;` missing after a statement. ## Where to read more The [guide](./introduction) teaches the language; [Coming from another language](./coming-from/) maps what you know. The rules themselves are the [specification](/reference/type-system), with the [grammar](/reference/grammar) and every [diagnostic](/reference/diagnostics). # Basics This chapter covers what every program uses: values and types, variables, functions, operators, and control flow. Each example is a whole program you can run with `novac run --interp file.nova`. ## A program A program is one or more `.nova` files with a `main` function. `main` takes nothing and returns nothing or an `i32` exit status. ```nova fn main() -> i32 { Console.writeLine("Hello, Nova"); return 0; } ``` ```text Hello, Nova ``` `Console.write` and `Console.writeLine` print integers, booleans, floats and text. ## Types | Type | Values | |---|---| | `i8` `i16` `i32` `i64` | signed integers, two's complement | | `u8` `u16` `u32` `u64` | unsigned integers | | `isize` `usize` | the target's pointer width; lengths and indexes are `usize` | | `f32` `f64` | IEEE 754 floating point | | `bool` | `true`, `false` | | `StringView` | a read-only view of UTF-8 text; string literals have this type | Later chapters add structs, enums, arrays, references, optionals (`T?`), results (`Result`), boxes and collections. There is no `char`: text is UTF-8 bytes, and a character literal such as `'k'` is an integer. ## Variables `let` declares a variable that cannot be assigned again; `var` one that can. The type is inferred from the initialiser, or written after a colon. ```nova fn main() { let name = "Nova"; // StringView let year: u16 = 2026; var count = 0; // i32: an integer literal's default type count += 1; var total: u64; // declared now, assigned before it is read total = 40 + 2; Console.writeLine(name); Console.writeLine(year); Console.writeLine(count); Console.writeLine(total); } ``` ```text Nova 2026 1 42 ``` Reading a variable that might not be assigned yet is a compile error, and so is assigning to a `let`. Two locals in scope cannot have the same name: there is no shadowing. ## Integer literals and the expected type An integer literal takes its type from where it is used. In `let year: u16 = 2026;` it is a `u16`; in `x + 1` it is `x`'s type; with nothing to go by it is an `i32`. A suffix fixes it: `7u8`, `100_000u64`. Literals can be written in hex, binary and octal (`0xFF`, `0b1010`, `0o17`), with `_` between digits. A value that does not fit its type is a compile error. A character literal is the character's Unicode value, typed like any other literal: ```nova fn isDigit(c: u8) -> bool => c >= '0' && c <= '9'; fn main() { Console.writeLine(isDigit('7')); Console.writeLine(isDigit('x')); Console.writeLine('\n' as u32); } ``` ```text true false 10 ``` ## Functions ```nova fn add(a: i64, b: i64) -> i64 { return a + b; } fn square(x: i64) -> i64 => x * x; // an expression body fn greet(name: StringView) { // no return type: returns nothing Console.write("Hello, "); Console.writeLine(name); } fn main() { Console.writeLine(add(2, square(3))); greet("reader"); } ``` ```text 11 Hello, reader ``` Parameters are immutable. Arguments are evaluated left to right. A function that returns a value must return on every path, which the compiler checks. ## Operators Both operands of an operator must have the same type: there are **no implicit conversions**, not even between `i32` and `i64`. | Operators | On | Notes | |---|---|---| | `+ - * / %` | integers, floats | integer overflow stops the program; so does dividing by zero | | `& \| ^ << >>` | unsigned integers | a shift by the width or more stops the program | | `== != < <= > >=` | integers, floats, `bool` (`==`, `!=`), text (`==`, `!=`) | | | `&& \|\| !` | `bool` | `&&` and `\|\|` short-circuit | When overflow is expected, say what should happen: ```nova fn main() { let big: u8 = 250; Console.writeLine(big.wrappingAdd(10)); // 4: modulo 256 Console.writeLine(big.saturatingAdd(10)); // 255: clamped match (big.checkedAdd(10)) { // u8?: null on overflow Some(sum) => { Console.writeLine(sum); } null => { Console.writeLine("overflow"); } } } ``` ```text 4 255 overflow ``` The same three families exist for `Sub` and `Mul`, on every integer type. ## Conversions `as` converts only when no value can be lost: `u8 as u32`, `i32 as i64`, `u32 as i64`, `bool as u8`, `i32 as f64`. Anything that could lose information is spelled out: ```nova fn main() { let big: u32 = 300; Console.writeLine(truncate(big)); // 44: the low 8 bits let x: f64 = 2.75; Console.writeLine(truncate(x)); // 2: toward zero; out of range stops the program Console.writeLine(round(1234567890123u64)); // the nearest f64 let n: i32 = -1; Console.writeLine(n as i64); } ``` ```text 44 2 1234567890123 -1 ``` ## Control flow `if`, `while`, `for` and `match` put their condition in parentheses, and their bodies are blocks. ```nova fn main() { var n = 0; while (n < 3) { n += 1; } for (i in 0..3) { // 0, 1, 2 if (i == 1) { continue; } Console.write(i); } Console.writeLine(); var tries = 0; loop { // until a break tries += 1; if (tries == 5) { break; } } Console.writeLine(n + tries); } ``` ```text 02 8 ``` `for (x in values)` walks an array, a slice or a vector (see [Collections](./collections)). ### `if` and `match` are expressions too ```nova fn describe(n: i32) -> StringView => match (n) { 0 => "zero", 1 => "one", _ => if (n < 0) "negative" else "many", }; fn main() { Console.writeLine(describe(0)); Console.writeLine(describe(7)); Console.writeLine(describe(-2)); let sign = if (-2 < 0) -1 else 1; Console.writeLine(sign); } ``` ```text zero many negative -1 ``` An `if` expression needs its `else`. A branch can also be a block that leaves, such as `{ return; }`, which is how an early exit fits inside an expression. ## Constants ```nova const SECONDS_PER_DAY: u32 = 24 * 60 * 60; fn main() { Console.writeLine(SECONDS_PER_DAY); } ``` ```text 86400 ``` A constant is evaluated at compile time. An overflow there is a compile error rather than a run-time stop. ## Comments `//` comments run to the end of the line, `/* … */` comments may span lines, and `///` before an item is its documentation, which [`novac doc`](./formatting-and-docs) collects. ## Next [Structs, enums and patterns](./structs-and-enums). # Structs, enums and patterns ## Structs A struct groups named fields. Fields end with `;`. Functions declared inside are its methods (with a `this` receiver) or static functions (without one). ```nova struct Point { x: i64; y: i64; fn origin() -> Point => Point { x: 0, y: 0 }; // static: Point.origin() fn manhattan(this) -> i64 { // `this` by value (Point is Copy) return abs(this.x) + abs(this.y); } fn moveBy(borrow mut this, dx: i64, dy: i64) { // changes the point in place this.x += dx; this.y += dy; } } fn abs(v: i64) -> i64 => if (v < 0) -v else v; fn main() { var p = Point.origin(); p.moveBy(3, -4); Console.writeLine(p.manhattan()); let q = Point { x: 1, y: 2 }; // every field, in any order Console.writeLine(q.x + q.y); } ``` ```text 7 3 ``` - A **struct literal** names every field exactly once. - The **receiver** says how a method gets its struct: `this` (a copy, or a move), `borrow this` (read-only access) or `borrow mut this` (changes the caller's value). Calling a `borrow mut this` method needs a `var` (or a mutable reference); the call borrows it for the duration of the call. - A struct is **Copy**, copied on assignment, when every field is Copy and it has no `drop` block. Otherwise it moves: see [Ownership](./ownership). - Fields are laid out in declaration order with C's rules, so a struct is the same in C. ### Visibility Items and members are `internal` by default: visible throughout their package. `public` makes them visible to other packages (see [Packages](./packages)); `private` on a field limits it to the file that declares the struct, which lets a type guard an invariant. ## Enums An enum is one of several variants, each with zero or more positional payloads. Variants are written with the enum's name. ```nova enum Shape { Empty, Circle(i64), Rect(i64, i64), } fn area(s: Shape) -> i64 => match (s) { Shape.Empty => 0, Shape.Circle(r) => 3 * r * r, Shape.Rect(w, h) => w * h, }; fn main() { Console.writeLine(area(Shape.Circle(2))); Console.writeLine(area(Shape.Rect(3, 4))); Console.writeLine(area(Shape.Empty)); } ``` ```text 12 12 0 ``` ## `match` `match` tries its arms in order. It must be **exhaustive**: every possible value matches some arm, or the compiler names one that does not. An arm that can never be reached is an error too. As a statement, each arm is a block; as an expression (above), each arm is a value, separated by commas. ```nova enum Command { Move(i32, i32), Wait(u32), Quit, } fn run(command: Command) { match (command) { Command.Move(0, 0) => { Console.writeLine("stay"); } Command.Move(x, _) => { Console.write("move x by "); Console.writeLine(x); } Command.Wait(seconds) => { Console.write("wait "); Console.writeLine(seconds); } Command.Quit => {} } } fn main() { run(Command.Move(0, 0)); run(Command.Move(5, 1)); run(Command.Wait(3)); run(Command.Quit); } ``` ```text stay move x by 5 wait 3 ``` Patterns can be nested: a variant inside a variant (`Ok(Shape.Circle(r))`), a literal in a payload's position (`Command.Move(0, 0)`), `null` or `Some(…)` for optionals, and `_` for anything. Integers and booleans can be matched directly, with `_` for the rest. ```nova fn kind(n: u8) -> StringView => match (n) { 0 => "zero", 'a' => "the letter a", _ => "something else", }; fn main() { Console.writeLine(kind(97)); Console.writeLine(kind(3)); } ``` ```text the letter a something else ``` ## Optionals `T?` is a `T` or `null`. A plain `T` converts to `T?` wherever one is expected. ```nova fn find(values: Slice, wanted: i32) -> usize? { for (i in 0..values.length) { if (values[i] == wanted) { return i; // converts to usize? } } return null; } fn firstOfTwo(values: Slice) -> usize? { let a = find(values, 2)?; // `?`: if null, return null return a; } fn main() { let numbers = [5, 2, 9]; match (find(numbers[..], 9)) { Some(i) => { Console.writeLine(i); } null => { Console.writeLine("absent"); } } Console.writeLine(find(numbers[..], 7) == null); Console.writeLine(firstOfTwo(numbers[..])!); // `!`: the value, or stop the program if null } ``` ```text 2 true 1 ``` - `o == null` and `o != null` test for absence. - `o!` is the value; on `null` it stops the program with "unwrapped null". - `o?` is the value; on `null` the enclosing function returns `null` (it must return an optional). - A reference can be optional (`borrow T?`), and so can a box (`Box?`); both are one pointer. ## Results `Result` is the other built-in enum: `Ok(value)` or `Err(error)`. It is how functions report failure; see [Errors](./errors). ## Next [Ownership and borrowing](./ownership): what happens when a value is assigned, passed and destroyed. # Ownership and borrowing Nova manages memory without a garbage collector by tracking, at compile time, who owns each value and who is looking at it. This chapter explains the rules, why they hold, and what the compiler says when a program breaks them. The formal rules are in [Ownership](/reference/ownership) and [References and the borrow checker](/reference/lifetimes). ## Copy types and move types Integers, floats, `bool`, references and string views are **Copy**: assigning one copies it, and both copies stay usable. A struct or enum is Copy when everything in it is Copy and it has no `drop` block. Everything else is a **move type**: boxes, vectors, strings, files, and any struct that owns one or has a `drop` block. Assigning a move-type value, passing it, returning it or storing it **moves** it. The old place is then empty, and using it is a compile error. ```nova struct Buffer { size: u32; drop { Console.writeLine("freed"); } } fn consume(b: Buffer) {} fn main() { let b = Buffer { size: 64 }; consume(b); Console.writeLine(b.size); } ``` ```text main.nova:11:5: error[N0250]: 'b.size' is used here after it was moved (on at least one path) | 11 | Console.writeLine(b.size); | ^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` Moves are what make destruction safe: a value has exactly one owner, so it is destroyed exactly once. ## Destruction A value is destroyed when its owner goes away: at the end of the variable's scope, when a temporary's statement ends, or when an assignment overwrites it. Destroying a value runs its `drop` block, if it has one, and then destroys its fields. Locals are destroyed in the reverse order of their declaration. ```nova struct Noisy { id: u32; drop { Console.write("drop "); Console.writeLine(this.id); } } fn main() { let first = Noisy { id: 1 }; let second = Noisy { id: 2 }; { let inner = Noisy { id: 3 }; } // inner is destroyed here var replaced = Noisy { id: 4 }; replaced = Noisy { id: 5 }; // 4 is destroyed by the assignment let moved = first; // first is moved: it is destroyed once, as `moved` Console.writeLine("end of main"); } ``` ```text drop 3 drop 4 end of main drop 1 drop 5 drop 2 ``` At the end of `main`, the locals are destroyed in reverse order of declaration: `moved` (holding 1), `replaced` (holding 5), then `second`. `first` was moved, so it is not destroyed again. A value moved on only some paths gets a hidden flag, so it is still destroyed exactly once. A **panic** or `Process.exit` does not run `drop` blocks: a panic stops the program at once (there is no unwinding). ### `defer` and `using` `defer s;` runs `s` when the enclosing block exits by any path: its end, `break`, `continue` or `return`. `using x = e;` declares a local that cannot be moved away: it lives exactly as long as its block, which documents that its lifetime *is* the scope (a lock guard, an arena, a file). ```nova fn work(fail: bool) -> i32 { defer Console.writeLine("cleanup"); if (fail) { return 1; } Console.writeLine("working"); return 0; } fn main() { Console.writeLine(work(false)); Console.writeLine(work(true)); } ``` ```text working cleanup 0 cleanup 1 ``` ## Borrowing Most code does not want to take ownership; it wants to look at a value or change it in place. A **reference** does that without moving: - `borrow T` is a shared reference: it can read, and any number can exist at once; - `borrow mut T` is an exclusive reference: it can read and write, and while it is in use nothing else can touch the value. ```nova struct Counter { hits: u64; } fn show(c: borrow Counter) { // looks at the counter Console.writeLine(c.hits); } fn record(c: borrow mut Counter) { // changes it in place c.hits += 1; } fn main() { var counter = Counter { hits: 0 }; record(mut counter); // `mut` at the call: this call changes `counter` record(mut counter); show(counter); // borrowed for the call; no marker needed } ``` ```text 2 ``` A `borrow T` parameter accepts the value itself, borrowed implicitly. A `borrow mut T` parameter needs `mut` before the argument, so that every place where a value may change is visible where it is called, as C#'s `ref` is. ### The rule While a borrow is **live**, meaning the reference or anything made from it is still used later, the borrowed value may not be: - changed, mutably borrowed or destroyed, if the borrow is shared; - used in any way, if the borrow is mutable. The compiler checks this within each function, on every path. A borrow ends at its last use, not at the end of its scope. ```nova fn main() { var total = 0; let r = borrow total; total += 1; Console.writeLine(r); } ``` ```text main.nova:4:5: error[N0240]: cannot assign to 'total' while 'total' is borrowed | 4 | total += 1; | ^^^^^^^^^^^ = note: 'total' is borrowed here (3:5) = note: the borrow is used later here (5:5) ``` Moving the `Console.writeLine(r)` above the `+=` fixes it: the borrow is then dead before the write. The same rule is what stops a vector from growing while an element is borrowed, since growing can move its elements. ### Returning references A function that returns a reference must return something borrowed from its parameters, never from its own locals. With one reference parameter, that is assumed. With several, the result names the ones it may borrow from: ```nova fn larger(a: borrow i64, b: borrow i64) -> borrow(a, b) i64 => if (a > b) a else b; fn firstPositive(values: Slice) -> borrow i64 { for (i in 0..values.length) { if (values[i] > 0) { return borrow values[i]; } } return borrow values[0]; } fn main() { let x: i64 = 3; let y: i64 = 7; Console.writeLine(larger(x, y)); let numbers = [-1i64, 4, 9]; Console.writeLine(firstPositive(numbers[..])); } ``` ```text 7 4 ``` The caller then knows how long the result stays valid: as long as the arguments it borrows from. Returning a reference to a local is rejected, since the local is gone when the function returns: ```nova fn pick(a: borrow i32) -> borrow i32 { let copy = a + 1; return borrow copy; } fn main() { Console.writeLine(pick(1)); } ``` ```text main.nova:3:5: error[N0244]: cannot return a reference to 'copy', which is local to 'pick' | 3 | return borrow copy; | ^^^^^^^^^^^^^^^^^^^ = note: borrowed here (3:12) ``` ### Two mutable borrows Two `borrow mut` references to one value can never be live together, even as two arguments of one call: ```nova fn addTo(target: borrow mut i32, amount: borrow mut i32) { target += amount; } fn main() { var x = 1; addTo(mut x, mut x); } ``` ```text main.nova:7:18: error[N0241]: cannot borrow 'x' as mutable while 'x' is mutably borrowed | 7 | addTo(mut x, mut x); | ^^^^^ = note: 'x' is mutably borrowed here (7:11) = note: the borrow is used later here (7:5) ``` ## References in structs: regions A struct that holds a reference or a view must say so, with a **region parameter**: the region is the lifetime of what the reference points into. Values of such a type cannot outlive that region. ```nova struct Word { text: StringView; start: usize; } fn firstWord(line: StringView) -> Word { var end: usize = 0; while (end < line.length && line[end] != ' ') { end += 1; } return Word { text: line[0..end], start: 0 }; } fn main() { let line = "hello wide world"; let word = firstWord(line); Console.writeLine(word.text); } ``` ```text hello ``` Plain functions that take and return references never need region parameters; only types that *store* references, and functions that pass such types through, do. Regions are a compile-time idea: they cost nothing at run time. ## What the checker does not do The borrow checker works within one function at a time, using the signatures of the functions it calls. It is deliberately simpler than Rust's: some correct programs are rejected. When that happens, the usual fixes are to end a borrow earlier, copy a value out, split a function, or use an index or a [slot map handle](./collections) instead of a stored reference. ## Next [Errors](./errors): `Result`, `?`, `!` and panics. # Errors Nova has two kinds of failure, and keeps them apart: - **Recoverable errors** are values: a function that can fail returns `Result`, and the caller decides what to do. Allocation failure and I/O errors are of this kind. - **Bugs** stop the program: an index out of bounds, an integer overflow, dividing by zero, unwrapping an error or a `null`. This is a **panic**: the program prints the reason and where it happened, and exits at once. There is no unwinding and no catching. ## `Result` ```nova enum ParseError { Empty, BadDigit(u8), TooLarge, } fn parse(text: StringView) -> Result { if (text.length == 0) { return Err(ParseError.Empty); } var value: u32 = 0; for (c in text.bytes()) { if (c < '0' || c > '9') { return Err(ParseError.BadDigit(c)); } let next = value.checkedMul(10); if (next == null) { return Err(ParseError.TooLarge); } value = next! + (c - '0') as u32; } return Ok(value); } fn main() { match (parse("2026")) { Ok(n) => { Console.writeLine(n); } Err(ParseError.BadDigit(c)) => { Console.write("not a digit: "); Console.writeLine(c); } Err(_) => { Console.writeLine("no number"); } } match (parse("20x6")) { Ok(n) => { Console.writeLine(n); } Err(ParseError.BadDigit(c)) => { Console.write("not a digit: "); Console.writeLine(c); } Err(_) => { Console.writeLine("no number"); } } } ``` ```text 2026 not a digit: 120 ``` `Result` is an enum with two variants, `Ok(T)` and `Err(E)`, matched without a type prefix. `T` may be `void` (`Result`, with `Ok()`); `E` is usually an enum of the ways the function can fail. ## `?`: pass the error on In a function that returns a `Result` with the same error type, `e?` is the value inside `Ok`, or returns the `Err` to the caller at once. Every local is destroyed on the way out, as for any `return`. ```nova enum ParseError { Empty, BadDigit(u8), } fn digit(c: u8) -> Result { if (c < '0' || c > '9') { return Err(ParseError.BadDigit(c)); } return Ok((c - '0') as u32); } fn twoDigits(text: StringView) -> Result { if (text.length != 2) { return Err(ParseError.Empty); } return Ok(digit(text[0])? * 10 + digit(text[1])?); } fn main() { Console.writeLine(twoDigits("42")!); match (twoDigits("4x")) { Ok(n) => { Console.writeLine(n); } Err(_) => { Console.writeLine("not two digits"); } } } ``` ```text 42 not two digits ``` The same works on optionals: `o?` returns `null` from a function that returns an optional. ## `!`: this cannot fail `e!` is the value inside `Ok` (or a present optional's value). If it is an `Err` (or `null`), the program panics. Use it where failure would be a bug, or in small programs and tests where stopping is the right answer: ```nova fn main() { var names = Vector.create(); names.push("ada")!; // allocation failure stops this program names.push("grace")!; Console.writeLine(names.length); } ``` ```text 2 ``` ## Allocation failure is a value Every operation that allocates returns a `Result<…, AllocError>`: `Box.create`, `Vector.push`, `String.append`, `Arena.alloc` and the rest. A program that must survive running out of memory, such as a kernel, handles it; a program that need not, writes `!` or passes it on with `?`: ```nova struct Node { value: i32; next: Box?; } fn push(head: Box?, value: i32) -> Result, AllocError> => Box.create(Node { value: value, next: head }); fn build(n: i32) -> Result?, AllocError> { var head: Box? = null; for (i in 0..n) { head = push(move head, i)?; } return Ok(head); } fn main() { match (build(3)) { Ok(list) => { Console.writeLine(list!.value); } Err(AllocError.OutOfMemory) => { Console.writeLine("out of memory"); } } } ``` ```text 2 ``` The test suite runs every allocating program again with each allocation failing in turn, to check that every failure path cleans up after itself. ## Panics A panic prints the reason and the source location, and exits with status 134: ```nova fn main() { let values = [1, 2, 3]; var i: usize = 0; while (true) { Console.writeLine(values[i]); i += 1; } } ``` ```text 1 2 3 ``` Here the fourth read stops the program with `panic: index out of bounds` and the file and line of `values[i]`. Panics never run `drop` blocks or deferred statements: the program is in a state its author did not expect, so nothing more of it runs. ## I/O errors Files, sockets and processes report `IoError`, a built-in enum (`NotFound`, `PermissionDenied`, `AlreadyExists`, …, `Other(code)`), whose `message()` is the C library's text: ```nova fn main() -> i32 { match (File.open("/no/such/file")) { Ok(file) => { Console.writeLine("opened"); return 0; } Err(e) => { Console.writeLine(e.message()); return 1; } } } ``` ```text No such file or directory ``` ## Next [Memory and allocation](./memory). # Memory and allocation Nova has no hidden allocation. Values live on the stack unless the program asks for memory, and every request names where the memory comes from and returns an error when there is none. This chapter goes from the simplest owner of heap memory, `Box`, to arenas, pools and scratch frames, and ends with how the compiler moves boxes to the stack when it can prove that is safe. The rules are in [Memory model](/reference/memory-model) and the type system's sections on [boxes](/reference/type-system#_18-boxes-and-allocators-m3b) and [regions](/reference/type-system#_19-regions-m4). ## `Box` `Box` owns one `T` on the heap. It is one pointer, it moves (never copies), and destroying it destroys the `T` and frees the memory. Fields and methods are reached through it as if it were the value. ```nova struct Account { id: u64; balance: i64; fn deposit(borrow mut this, amount: i64) { this.balance += amount; } } fn open(id: u64) -> Result, AllocError> => Box.create(Account { id: id, balance: 0 }); fn main() { var account = open(7)!; account.deposit(100); // through the box Console.writeLine(account.balance); } // the Account is destroyed and its memory freed here ``` ```text 100 ``` Boxes are how recursive types are built, since a type cannot contain itself directly: ```nova enum Tree { Leaf, Node(Box, i32, Box), } fn sum(t: borrow Tree) -> i32 => match (t) { Tree.Leaf => 0, Tree.Node(left, value, right) => sum(left) + value + sum(right), }; fn leaf() -> Box => Box.create(Tree.Leaf)!; fn main() { let tree = Tree.Node(Box.create(Tree.Node(leaf(), 1, leaf()))!, 2, Box.create(Tree.Node(leaf(), 3, leaf()))!); Console.writeLine(sum(tree)); } ``` ```text 6 ``` A long list of boxes is destroyed with a loop, not recursion, so it cannot overflow the stack. ## Allocators `Box` is `Box`: memory from the platform's `malloc`. The allocator is part of the type, and `Box.createIn(allocator, value)` chooses another one: | Allocator | Gives | Frees | |---|---|---| | `SystemHeap` | the platform heap | each value when its owner is destroyed | | `Arena` | chunks of one region | everything at once, when the arena is destroyed or reset | | `FixedBufferAllocator` | a buffer you provide (an array on the stack, say) | nothing: the buffer is reused when the allocator ends | | `Scratch` frames | a runtime stack of chunks, last in first out | everything in a frame when the frame ends | | `Pool` | slots for up to *n* values of one type | each slot when its box is destroyed | | your own | any struct that implements `Allocator` | as you implement it | `FailingAllocator` is a `SystemHeap` that can be told to fail after *n* allocations, for testing failure paths. ## Arenas An arena hands out memory quickly and frees it all at once. Every allocation **borrows the arena**, so none can outlive it: the compiler rejects any attempt to keep one after the arena is reset or destroyed. ```nova fn histogram(input: Slice) -> Result { using arena = Arena.create(SystemHeap {}, 65536u64); let counts = arena.allocArray(256usize, 0u32)?; // a SliceMut in the arena for (b in input) { counts[b as usize] += 1; } var distinct: u64 = 0; for (c in counts) { if (c != 0) { distinct += 1; } } return Ok(distinct); // a number, not arena memory } // the arena releases everything here fn main() { Console.writeLine(histogram("hello".bytes())!); } ``` ```text 4 ``` Values in an arena must not need destruction (no `drop` blocks, no boxes inside), since the arena never runs their destructors. Returning `counts` itself would be a compile error: it borrows an arena that ends with the function. ## A fixed buffer ```nova struct Span { start: u32; length: u32; } fn main() { var storage = [0u8; 1024]; let fba = FixedBufferAllocator.over(mut storage); // borrows `storage` mutably let spans = fba.allocArray(16usize, Span { start: 0, length: 0 })!; spans[3].length = 5; Console.writeLine(spans[3].length); Console.writeLine(spans.length); } ``` ```text 5 16 ``` When the buffer is full, an allocation returns `Err(AllocError.OutOfMemory)`; nothing is ever taken from the heap. ## Scratch frames Scratch memory is a stack of frames: a function enters a frame, allocates temporaries from it, and they are all released when the frame ends. Nested frames borrow their parent, so frames always end in order. ```nova fn render(n: usize) -> Result { using frame = Scratch.enter(); let buffer = frame.allocArray(n, 0u8)?; { using inner = frame.enter(); let temporary = inner.allocArray(n * 2, 0u16)?; temporary[0] = 1; } // `inner`'s memory is released here return Ok(buffer.length); } // `frame`'s memory is released here fn main() { Console.writeLine(render(64)!); } ``` ```text 64 ``` ## Pools A pool holds up to a fixed number of values of one type. `take` returns a box-like `PoolBox` that returns its slot when destroyed, so pool values may have `drop` blocks. ```nova struct Particle { x: i32; v: i32; } fn simulate() -> Result { using pool: Pool = Pool.create(SystemHeap {}, 64u32); var a = pool.take(Particle { x: 0, v: 2 })?; let b = pool.take(Particle { x: 10, v: -1 })?; a.x += a.v; return Ok(a.x + b.x); } fn main() { Console.writeLine(simulate()!); } ``` ```text 12 ``` ## Your own allocator An allocator is a struct with no fields that implements `lib/core`'s `Allocator` interface: `allocate` returns memory for a `Layout` (size and alignment) or an `AllocError`, and `deallocate`, which is `unsafe` because it must be given exactly what `allocate` returned, gives it back. `Box.createIn(MyAllocator {}, value)` then uses it. The CosmoOS kernel's `KernelHeap` over `kmalloc` is written this way; see [Kernel code](./kernel) and the [lib/core reference](/reference/api/core). ## Placement: boxes on the stack The compiler looks for boxes that provably never outlive their function, or one loop iteration, and puts them on the stack instead. The program cannot tell the difference: destruction happens at the same points and in the same order, and allocation failure can no longer happen for those boxes. `novac build --explain-allocations` lists each allocation and the decision; `--no-placement` turns it off. The rules are in [Optimisations](/reference/optimisations). ## Next [Arrays, slices and collections](./collections). # Arrays, slices and collections ## Arrays `[T; N]` is `N` values of `T`, inline: on the stack, or inside the struct that holds it. Arrays are values (copied when assigned) and their element type must be Copy. ```nova fn main() { var squares = [0u32; 5]; // five zeros for (i in 0..squares.length) { squares[i] = truncate(i * i); // usize to u32 could lose bits: say so } let primes = [2, 3, 5, 7]; var sum = 0; for (p in primes) { sum += p; } Console.writeLine(squares[4]); Console.writeLine(sum); } ``` ```text 16 17 ``` Every index is checked: `a[i]` with `i >= a.length` stops the program. The compiler removes the check where it can prove the index is in range, as in both loops above. ## Slices `Slice` is a borrowed view of consecutive elements, and `SliceMut` a borrowed view that can change them. They are a pointer and a length, they follow every rule for references, and they are how functions take "some elements": ```nova fn total(values: Slice) -> i64 { var sum: i64 = 0; for (v in values) { sum += v; } return sum; } fn double(values: SliceMut) { for (i in 0..values.length) { values[i] *= 2; } } fn main() { var numbers = [1i64, 2, 3, 4, 5]; double(mut numbers[1..3]); // elements 1 and 2 Console.writeLine(total(numbers[..])); Console.writeLine(total(numbers[3..])); } ``` ```text 20 9 ``` `a[lo..hi]`, `a[lo..]`, `a[..hi]` and `a[..]` take elements `lo` up to `hi`; the range is checked. A slice borrows the whole array it came from, so the array cannot change while the slice is used. Slices also copy and fill in one checked operation (`d.copyFrom(s)`, `d.fill(v)`), swap elements, and sort and search with closures (see [Functions and closures](./closures)). ## Vectors `Vector` owns a growable list on the heap. Pushing can fail, so `push` returns a `Result`. ```nova fn main() { var v = Vector.create(); // allocates nothing yet for (i in 0..5) { v.push(i * 10)!; } Console.writeLine(v.length); Console.writeLine(v[2]); match (v.pop()) { // i32?: null when empty Some(last) => { Console.writeLine(last); } null => {} } v[0] = 99; var sum = 0; for (x in v) { sum += x; } Console.writeLine(sum); } ``` ```text 5 20 40 159 ``` A vector can be passed where a `Slice` is expected (it lends its elements), and `mut v` where a `SliceMut` is. `v.view()` and `v.viewMut()` give the slices explicitly. Other members: `capacity`, `clear()`, `truncate(n)`, `swap(i, j)`, and `Vector.withCapacity(n)`. **Invalidation is a compile error.** Growing a vector may move its elements, so while any element or view of it is borrowed, `push`, `pop`, `clear` and writes are rejected. This is the most common thing the borrow checker catches in code that would crash in C++. ## Text: `StringView` and `String` `StringView` is a read-only view of UTF-8 text: string literals are views, and so are slices of strings. `String` owns growable text. ```nova fn greet(name: StringView) -> Result { var s = String.withCapacity(name.length + 7)?; s.append("Hello, ")?; s.append(name)?; s.append("! You are visitor ")?; s.appendNumber(42)?; return Ok(s); } fn main() { let message = greet("Ada")!; Console.writeLine(message); let word: StringView = "Nova language"; Console.writeLine(word[0..4]); Console.writeLine(word.length); Console.writeLine(word == "Nova language"); } ``` ```text Hello, Ada! You are visitor 42 Nova 13 true ``` - `length` is in bytes; `s[i]` is a byte (`u8`); `s.bytes()` is a `Slice`. - Slicing (`s[lo..hi]`) checks that both ends fall on character boundaries. - A `String` can only receive valid UTF-8 (`append` takes a view, which always is), so its contents are always valid text. - `==` compares bytes. ## Stable references: `SlotMap` and `StableVector` Sometimes values must refer to each other, as in a graph. A reference stored in a value would borrow the collection for as long as the value lives. A `SlotMap` gives out **handles** instead: plain values that name an element, can be stored anywhere, and are checked on every use, so a handle to a removed element finds nothing instead of something else. ```nova struct Node { value: i32; next: Handle?; } fn main() { var nodes = SlotMap.create(); let a = nodes.insert(Node { value: 1, next: null })!; let b = nodes.insert(Node { value: 2, next: a })!; match (nodes.get(b)) { Some(node) => { Console.writeLine(node.value); } null => {} } let removed = nodes.remove(a); Console.writeLine(nodes.get(a) == null); // the handle is stale now Console.writeLine(nodes.length); } ``` ```text 2 true 1 ``` `StableVector` is a grow-only sequence whose elements never move, so an element can stay borrowed while more are pushed. ## Next [Generics and interfaces](./generics). # Generics and interfaces ## Generic functions and types Functions, structs and enums can take type parameters. Each use with different types compiles to its own copy (monomorphisation), so generic code is as fast as hand-written code for each type. ```nova struct Pair { first: T; second: T; fn swapped(this) -> Pair => Pair { first: this.second, second: this.first }; } enum Maybe { Nothing, Just(T), } fn firstOf(pair: Pair) -> T => pair.first; fn main() { let p = Pair { first: 1, second: 2 }; // Pair, inferred let q = p.swapped(); Console.writeLine(firstOf(q)); // T = i32, inferred let names = Pair { first: "x", second: "y" }; Console.writeLine(firstOf(names)); // written out let m: Maybe = Maybe.Just(7); match (m) { Maybe.Just(v) => { Console.writeLine(v); } Maybe.Nothing => {} } } ``` ```text 2 x 7 ``` Type arguments are inferred from the arguments where possible, and written otherwise: `Pair { … }`, `Vector.create()`, `firstOf(names)`. **Generic code is checked once,** for every possible type argument. Inside it, a value of type `T` can be moved, borrowed, stored and returned, but nothing else is known about it: no arithmetic, no fields, no `==`. What it can do comes from its **bounds**. ## Interfaces An interface lists methods. A type implements it with an `impl` block, and a generic function bound by it can call those methods. ```nova interface Shape { fn area(borrow this) -> u64; fn name(borrow this) -> StringView; } struct Square { side: u64; } struct Rect { w: u64; h: u64; } impl Shape for Square { fn area(borrow this) -> u64 => this.side * this.side; fn name(borrow this) -> StringView => "square"; } impl Shape for Rect { fn area(borrow this) -> u64 => this.w * this.h; fn name(borrow this) -> StringView => "rect"; } fn total(shapes: Slice) -> u64 { var sum: u64 = 0; for (shape in shapes) { sum += shape.area(); } return sum; } fn describe(shape: borrow S) { Console.write(shape.name()); Console.write(": "); Console.writeLine(shape.area()); } fn main() { let squares = [Square { side: 2 }, Square { side: 3 }]; var rects = Vector.create(); rects.push(Rect { w: 4, h: 5 })!; Console.writeLine(total(squares[..]) + total(rects.view())); describe(squares[1]); } ``` ```text 33 square: 9 ``` - Calls through a bound are resolved at compile time: there is no virtual dispatch and no cost. - `Copy` is a built-in bound: a `T: Copy` can be used twice and stored in arrays. - Several bounds are joined with `+`: ``. - An interface can itself be generic: `interface Comparable { fn compare(borrow this, other: borrow T) -> i32; }`. ## Dynamic dispatch A bound is resolved at compile time, so a `Slice` holds shapes of one type. For a collection of different types that implement the same interface, use `dyn Shape`: a value of some type implementing `Shape`, whose methods are called through a table at run time. Its size is not known, so it lives behind a pointer: `borrow dyn Shape`, `borrow mut dyn Shape`, or an owning `Box`. ```nova interface Shape { fn area(borrow this) -> u64; fn name(borrow this) -> StringView; } struct Square { side: u64; } struct Rect { w: u64; h: u64; } impl Shape for Square { fn area(borrow this) -> u64 => this.side * this.side; fn name(borrow this) -> StringView => "square"; } impl Shape for Rect { fn area(borrow this) -> u64 => this.w * this.h; fn name(borrow this) -> StringView => "rect"; } fn describe(shape: borrow dyn Shape) { Console.write(shape.name()); Console.write(": "); Console.writeLine(shape.area()); } fn main() { var shapes = Vector>.create(); shapes.push(Box.create(Square { side: 3 })!)!; // a Box becomes a Box shapes.push(Box.create(Rect { w: 2, h: 5 })!)!; for (shape in shapes) { describe(shape); } let sq = Square { side: 4 }; describe(sq); // a Square, lent as a borrow dyn Shape } ``` ```text square: 9 rect: 10 square: 16 ``` - A `borrow T` or a `Box` converts to the `dyn` type where one is expected, if `T` implements the interface. There is no way back to `T`. - Every method of the interface must take `borrow this` or `borrow mut this`. - A call through `dyn` cannot be inlined. Prefer a bound when one type is enough. - An interface method may declare its effects, `@effects() fn visit(borrow mut this, x: u64);`, which every `impl` must keep: kernel code in an atomic context can then call it through `dyn`. ## Next [Functions and closures](./closures). # Functions and closures ## Functions are values A function can be stored in a variable and passed as an argument. Its type is written `fn(parameter types) -> result`: ```nova fn double(x: i32) -> i32 => x * 2; fn negate(x: i32) -> i32 => -x; fn applyTwice(f: fn(i32) -> i32, value: i32) -> i32 => f(f(value)); fn main() { Console.writeLine(applyTwice(double, 3)); var op: fn(i32) -> i32 = double; op = negate; Console.writeLine(op(5)); } ``` ```text 12 -5 ``` A function value is one pointer, and calling it is an indirect call. ## Closures A closure is a function written inline, `(parameters) => body`, that can use the variables around it. It borrows what it captures, so it is a kind of reference: its type is `borrow fn(…) -> R` if it only reads what it captures, and `borrow mut fn(…) -> R` if it changes something. ```nova fn countIf(values: Slice, keep: borrow fn(i32) -> bool) -> i32 { var n = 0; for (v in values) { if (keep(v)) { n += 1; } } return n; } fn forEach(values: Slice, 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)); // captures `limit` var total = 0; forEach(values[..], (v) => { total += v; }); // changes `total` Console.writeLine(total); let twice = (x: i32) => x * 2; // captures nothing: a plain fn Console.writeLine(twice(21)); } ``` ```text 2 25 42 ``` - Parameter types can be left out where the expected type gives them, as in the calls above. - The body is an expression, or a block: `(v) => { total += v; }`. - A closure that captures nothing is an ordinary function value, and can be stored anywhere a function can. ### Closures do not outlive their block Because a closure borrows its captures, the borrow checker treats it like any reference: it cannot be returned from the function that made it, stored in a struct, or kept after a captured variable changes. That makes closures free of allocation and safe to use in kernels. ### Owning closures A `move` literal moves what it captures into the closure, so the closure can outlive the function that made it. It is boxed, with an allocation you can see and handle, as a `Box R>`. A closure that changes what it captured is a `Box R>`, and calling it needs mutable access. ```nova fn adder(n: u64) -> Box u64> { return Box.create(move (x) => x + n)!; } fn counter() -> Box u64> { var count: u64 = 0; return Box.create(move () => { count += 1; return count; })!; } fn main() { let add2 = adder(2); Console.writeLine(add2(3)); var next = counter(); next(); Console.writeLine(next()); var steps = Vector u64>>.create(); steps.push(adder(1))!; steps.push(adder(10))!; var x: u64 = 0; for (step in steps) { x = step(x); } Console.writeLine(x); } ``` ```text 5 2 11 ``` - The literal goes straight into `Box.create(…)` where a `Box` is expected; its parameter types come from there. - A captured variable is moved in, and cannot be used afterwards (a Copy one is copied). It cannot hold a reference. - The body changes the closure's own copy of what it captured, and cannot move it out. ## Methods that take closures Slices, arrays (through `a[..]`) and vectors have `sortBy`, `forEach`, `any`, `all`, and vectors `retain`: ```nova struct Reading { sensor: u32; celsius: f64; } fn main() { var readings = Vector.create(); for (i in 0..8u32) { readings.push(Reading { sensor: i % 3, celsius: 18.5 + (i as f64) * 0.75 })!; } let limit = 20.0; readings.retain((r) => r.celsius > limit); readings.sortBy((a, b) => if (a.celsius > b.celsius) -1 else if (a.celsius < b.celsius) 1 else 0); var sum = 0.0; readings.forEach((r) => { sum += r.celsius; }); Console.writeLine(readings.length); Console.writeLine(readings[0].celsius); Console.writeLine(readings.any((r) => r.sensor == 0)); } ``` ```text 5 23.75 true ``` `sortBy` is an introsort: it allocates nothing and runs in O(n log n), but it is not stable. Its comparator returns a negative number, zero or a positive number. These methods are written in Nova, in `lib/core/slices.nova`. ## Effect lists In kernel code, a callback may run where sleeping or allocating is forbidden. A function type can say what its functions may do: `@effects() fn(i32)` may neither sleep nor allocate, and `@effects(alloc) fn(i32)` may allocate but not sleep. The compiler checks every function and closure given for it. See [Kernel code](./kernel). ```nova fn onEach(values: Slice, f: borrow mut @effects() fn(u32)) { for (v in values) { f(v); } } fn main() { var largest: u32 = 0; let values = [4u32, 11, 7]; onEach(values[..], (v) => { if (v > largest) { largest = v; } }); Console.writeLine(largest); } ``` ```text 11 ``` ## Floats `f32` and `f64` follow IEEE 754: no operation on them panics, and NaN and the infinities behave as specified. They print as the shortest text that reads back as the same value. ```nova fn mean(values: Slice) -> f64 { var sum = 0.0; for (v in values) { sum += v; } return sum / round(values.length); } fn main() { let samples = [1.5, 2.25, 4.0]; let m = mean(samples[..]); Console.writeLine(m); Console.writeLine(m.sqrt()); Console.writeLine(truncate(m * 1000.0)); Console.writeLine(0.1 + 0.2); } ``` ```text 2.5833333333333335 1.6072751268321592 2583 0.30000000000000004 ``` Integers become floats with `as` when exact (`i32 as f64`) or `round(n)`; floats become integers with `truncate(x)` (toward zero; out of range stops the program), `checkedTruncate` or `saturatingTruncate`. The math methods are `sqrt`, `abs`, `floor`, `ceil`, `min` and `max`. Kernel code compiles with `--no-float`, which rejects float types altogether. ## Next [Threads](./threads). # Unsafe code and C Safe Nova cannot express everything a systems program needs: talking to hardware, calling C, implementing an allocator. For those there is `unsafe`, kept small and visible. The contract unsafe code must keep is in [Unsafe code](/reference/unsafe), and the C boundary in the type system's [C interoperability](/reference/type-system#_28-c-interoperability-m8b) section. ## Raw pointers `*T` and `*mut T` are raw pointers: plain addresses, Copy, with no borrow attached. **Making** a pointer is safe; **using** one (`read`, `write`, `add`, turning it back into a reference) is allowed only inside `unsafe`. ```nova /// 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() { var values = [1u32, 2, 3, 4]; let p = borrow mut values[0] as *mut u32; unsafe { p.add(3).write(10); } Console.writeLine(unsafe { sum(p, values.length) }); } ``` ```text 16 ``` ## The `Safety:` rule Every `unsafe fn`, and every function that contains an `unsafe` block, must say in its doc comment why it is sound, on a line starting with `Safety:`. The compiler enforces it: ```nova fn first(p: *u8) -> u8 { return unsafe { p.read() }; } ``` ```text main.nova:1:4: error[N0283]: function 'first' uses 'unsafe' and needs a '/// Safety:' doc comment saying why it is sound (type-system.md §27.4) | 1 | fn first(p: *u8) -> u8 { | ^^^^^ ``` For an `unsafe fn`, the line states what callers must guarantee; for a safe function with an `unsafe` block inside, why the block cannot go wrong. Reviewers read these lines; the compiler checks that they exist. `unsafe` permits only the pointer operations, calls of unsafe functions and C functions, and volatile accesses. Everything else in an unsafe block is checked exactly as in safe code. ## Calling C C functions are declared in an `extern "C"` block and called inside `unsafe`, since Nova cannot check what C does. Their types are C's: integers, `bool`, raw pointers, `*CChar` for C strings, and `@repr(C)` structs. ```nova extern "C" { fn strlen(s: *CChar) -> usize; fn puts(s: *CChar) -> i32; } /// 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(unsafe { strlen(p) }); } ``` C files are built with the program: `novac build main.nova helpers.c -o program`, or the `c` list of a package's manifest. ## Being called from C `@export` makes a Nova function callable from C, and `@repr(C)` gives a struct C's layout. `novac` writes `nova_exports.h` with their declarations for the C side to include. ```nova @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 => abs(p.x) + abs(p.y); fn abs(v: i32) -> i32 => if (v < 0) -v else v; fn main() { Console.writeLine(length(Point { x: 3, y: -4 })); } ``` `@callback` gives a function a C address without exporting a symbol, for C APIs that take a function pointer and a `void *` argument, as kernels' timer and thread APIs do. `extern "C" fn(…) -> R` is the type of such a pointer. ## C structs from the C code's headers A C codebase's own structs are not re-declared in Nova, where the two copies could drift apart. They are imported from its headers: Nova uses them by pointer or embedded by value, names only the fields it uses, and the C compiler checks each one at build time. ```nova @header("kernel/timer.h") extern "C" { @ctype("struct timer") struct timer; @effects() fn timer_start(t: *mut timer, delay_ns: u64); } ``` See [C structs from headers](/reference/decisions/0008-c-struct-bindings-from-headers) and [Kernel code](./kernel). ### Arrays, keyword names and tables A C array member is declared as a Nova array (`uint8_t uuid[16]` is `uuid: [u8; 16]`) and used like one: indexed with bounds checks, sliced, copied. A member whose name is a Nova keyword takes `@cname`. An array of C structs is indexed in place. A constant table can be exported to C as data with `@export`. ```nova @header("kernel/gpt.h") extern "C" { struct gpt_part { first_lba: u64; @cname("type") kind: [u8; 16]; uuid: [u8; 16]; } struct gpt_table { nparts: u32; parts: [gpt_part; 32]; } } /// `const uint8_t gpt_type_esp[16]` in C. @export("gpt_type_esp") const TYPE_ESP: [u8; 16] = [0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b]; /// Is partition `i` an EFI system partition? fn isEsp(table: borrow gpt_table, i: usize) -> bool { let kind = table.parts[i].kind; // a copy of the C array for (k in 0..kind.length) { if (kind[k] != TYPE_ESP[k]) { return false; } } return true; } ``` This is how CosmoOS's GPT parser is written in Nova ([the C9 report](/reference/cosmoos/c9-report); the rules are [ADR 0014](/reference/decisions/0014-c9-c-arrays-and-exported-constants)). ## Next [Kernel code](./kernel). # Kernel code Nova is designed to run inside an operating system kernel as well as in user programs. Its kernel support is developed against [CosmoOS](https://github.com/vkuttyp/CosmoOS), a kernel written in C, where Nova code runs as kernel components and as loadable modules on x86-64 and AArch64. Nothing in it is specific to CosmoOS: the same mechanisms fit any C kernel or firmware. ## The freestanding profile `--profile freestanding` (or `profile = "freestanding"` in a package's manifest) compiles for an environment without a C library or an operating system: - there is no `main`: the entry points are `@export` functions that the environment calls; - everything that needs a hosted runtime is unavailable, and naming it is an error: `Console`, files, sockets, `SystemHeap` and the collections built on it; - memory comes from an allocator the environment provides, written in Nova over the kernel's C functions; - `--no-float` (or `float = false`) rejects floating point, for code compiled without FP registers. `novac build --emit obj` then writes one relocatable object, with only the undefined symbols the environment provides, ready for the kernel's own build to link. ```nova /// A CRC-32C over `n` bytes at `data`, for C callers. /// Safety: `data` points to `n` readable bytes, as the C prototype's contract says. @export("crc32c_sum") unsafe fn crc32c(data: *u8, n: usize) -> u32 { return checksum(Slice.fromRaw(data, n)); } fn checksum(bytes: Slice) -> u32 { var crc: u32 = 0xFFFFFFFF; for (b in bytes) { crc = crc ^ (b as u32); for (bit in 0..8) { let mask = if ((crc & 1) != 0) 0x82F63B78u32 else 0u32; crc = (crc >> 1) ^ mask; } } return crc ^ 0xFFFFFFFF; } ``` The unsafe part is confined to the boundary: the exported function turns C's pointer and length into a slice under a stated contract, and everything behind it is safe, checked Nova. ## Context effects Kernel code runs in contexts with rules: while a spin lock is held it must not sleep; in an interrupt handler it must not sleep or allocate. Nova checks these rules at compile time, across function calls: - an operation's **effects** are `sleep` (it may block) and `alloc` (it may allocate), inferred for every function from what it does and calls, its destructors included; - `@effects(…)` declares what a C function may do, or bounds what a Nova function may do; - `@context(atomic)` on a function forbids `sleep` in it; `@context(irq)` forbids `sleep` and `alloc`; - `@context(atomic)` on a **struct** makes holding one an atomic context: a spin-lock guard. ```nova @header("kernel/sched.h") extern "C" { @effects(sleep) fn cosmo_sleep_ms(ms: u32); } /// Holding a spin-lock guard makes the code that runs meanwhile atomic: it must not sleep. @context(atomic) struct SpinGuard { cpu: u32; drop {} } /// Safety: cosmo_sleep_ms has no preconditions; it may sleep. fn backOff() { unsafe { cosmo_sleep_ms(1); } } fn update(counter: borrow mut u64) { let guard = SpinGuard { cpu: 0 }; counter += 1; backOff(); // sleeps while the guard is held } ``` ```text main.nova:17:5: error[N0295]: a 'SpinGuard' is live here, which makes this an atomic context (type-system.md §32.3), but this may sleep: it calls 'backOff', which calls 'cosmo_sleep_ms', declared @effects(sleep) | 17 | backOff(); // sleeps while the guard is held | ^^^^^^^^^^ ``` This proves that Nova code does nothing its context forbids, given that the `@effects` of C functions are declared correctly. It does not prove the absence of deadlocks or lock-order inversions: the kernel's own lockdep stays the check for those. ## Hardware registers `lib/core` provides `Volatile`, a cell read and written only by volatile accesses, and `MmioPtr`, the address of memory-mapped registers. Making an `MmioPtr` is the one unsafe step; register access through it is safe. ```nova @repr(C) struct UartRegs { data: Volatile; status: Volatile; } struct Uart { regs: MmioPtr; /// Safety: `base` is the mapped address of a UART that no other `Uart` aliases. public unsafe fn attach(base: usize) -> Uart => Uart { regs: MmioPtr.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); } } ``` ## Kernel allocators, locks and objects The CosmoOS integration (in the repository's `integrations/cosmoos/`) builds the rest from these pieces, in Nova: - `KernelHeap`, an allocator over `kmalloc`/`kfree`, so `Box.createIn(KernelHeap {}, value)` allocates kernel memory and returns `Err` when the kernel has none; - `SpinLock`, `IrqSpinLock` and `Mutex` that hold the data they protect: the data is reachable only through a guard, the guard unlocks when it is destroyed, and a spin guard is an atomic context; - `KRef`, a reference-counted kernel object whose final release may sleep, so dropping one under a spin lock does not compile; - C kernel state (lists, locks, kernel objects) embedded by value in Nova structs, built in place and never moved; - a loadable module whose state, `init` and teardown are Nova. How these were built and tested inside CosmoOS, including a stress test under lockdep, is in the [CosmoOS reports](/reference/cosmoos/c7-report) and the [profiles](/reference/cosmoos/profiles). ## Next [The novac command](./novac). # The novac command `novac` is the compiler and every tool around it. `novac --help` prints the same summary as this page. Inside a [package](./packages), commands take no file names: they use the nearest `nova.toml` in the current directory or above. Outside one, give the files. ## Commands | Command | Does | |---|---| | `novac new [--lib] [--agent ] [--no-agent]` | make a package: `nova.toml`, `src/main.nova` (or `src/lib.nova`), and `AGENTS.md` for [AI agents](./for-agents) | | `novac agents [--agent ]` | write or refresh the package's agent instructions | | `novac fetch [--offline]` | resolve the package's dependencies and write `nova.lock` | | `novac check` | parse, type-check and borrow-check; the fastest way to find errors | | `novac build` | compile to C, then with the C compiler to `out/` (or `-o`) | | `novac run --interp` | run the program in the reference interpreter | | `novac parse` | parse only, reporting syntax errors | | `novac fmt [--check \| --stdout]` | format in the canonical style ([Formatting](./formatting-and-docs)) | | `novac doc [-o file] [--title t] [--missing]` | a Markdown page of the public items ([Documentation](./formatting-and-docs#documentation)) | | `novac lsp` | the language server, for [editors](./editors) | | `novac fix [--dry-run]` | apply the fixes the compiler is sure of, and check again until none applies | | `novac explain ` | what a diagnostic code (`N0249`) means | | `novac mcp` | the MCP server, for [AI agents](./for-agents) | ## Building | Option | Does | |---|---| | `-o ` | the output file (or directory, with `--emit c`) | | `--emit exe` | an executable (the default) | | `--emit c` | the generated C sources, to look at or to build yourself | | `--emit obj` | one relocatable object, for a kernel or another program's link (`--ld ld.lld` joins objects for ELF cross builds) | | `--cc ` | the C compiler; default `$CC`, else `cc`, or on Windows clang, gcc or Visual Studio's `cl`, found and set up ([tooling](/reference/tooling) §8) | | `--cflag ` | an extra C compiler flag (repeatable); the default is `-O2` | | `-g` | a debug build: `#line` directives and `-g -O0` (see [Debugging](./debugging)) | | `--line-directives` | `#line` directives without changing the optimisation level | | `--profile hosted \| freestanding` | the [profile](./kernel#the-freestanding-profile) | | `--no-float` | reject every float type and literal | | `--offline` | in a package: never run `git` | C files given with the Nova files (`novac build main.nova shim.c`) are compiled and linked with the program. ## Seeing what the compiler decided | Option | Prints | |---|---| | `--explain-allocations` | each allocation site, and whether its box lives on the stack or the heap, and why | | `--explain-bounds-checks` | each index check, and whether it was removed | | `--explain-overflow-checks` | each checked `+`, `-`, `*` and negation, and whether its check was removed | | `--no-placement`, `--no-bce`, `--no-oce` | turn off box placement, bounds-check elimination, overflow-check elimination | | `--dump-ast`, `--dump-hir`, `--dump-nir` | the compiler's intermediate forms (`parse`, `check`, `build`) | The optimisations never remove a check that could fail; their preconditions are in [Optimisations](/reference/optimisations). ## Exit status and errors `novac` exits with 0 on success, 1 when the program has errors, and 2 for a usage error. Diagnostics go to standard error as `file:line:column: error[code]: message`, followed by the source line, notes, and a hint where there is one. The codes are listed in [Diagnostics](/reference/diagnostics). With `--diagnostics-format=json` (on `parse`, `check`, `build`, `run`, `fmt` and `fix`), they go to standard output instead, as one JSON document with each diagnostic's position, message and fixes, for programs and [AI agents](./for-agents) ([tooling](/reference/tooling) §5). ## The interpreter `novac run --interp` runs a program without a C compiler, in an interpreter that executes the compiler's own intermediate form. It stops with an exit status of 70 on any use of freed, out-of-bounds or uninitialised memory, or a bad free, which the C build would not detect. The test suite runs every program both ways and compares the results. # Packages A package is a directory with a `nova.toml`: a program or a library, its sources, and the packages it depends on. Dependencies come from local paths or git repositories, pinned to exact commits in `nova.lock`. The full rules are in [Tooling §4](/reference/tooling#_4-packages-t6-adr-0013) and [ADR 0013](/reference/decisions/0013-m10-modules-and-packages). ## The manifest ```toml [package] name = "app" # an identifier: what other packages call this one version = "0.1.0" kind = "executable" # "executable", "library", or "object" (one .o for a kernel's build) profile = "hosted" # or "freestanding" float = true # false is --no-float sources = ["src"] # the default c = ["src/shim.c"] # C files built with an executable [dependencies] geometry = { path = "../geometry" } lz4 = { git = "https://example.org/lz4-nova.git", tag = "v1.2.0" } # or rev = "", or branch = "main" ``` Only `name` is required. A dependency's key must be the name its own manifest gives it, and a dependency must be a library. ## Using a dependency All files of one package share one scope. Another package's items are reached through its name, and only its `public` items are visible: ```nova // geometry/src/lib.nova public struct Point { public x: i64; public y: i64; public fn create(x: i64, y: i64) -> Point => Point { x: x, y: y }; public fn norm(this) -> i64 => this.x * this.x + this.y * this.y; } public fn origin() -> Point => Point.create(0, 0); fn helper() -> i64 => 1; // internal: only geometry can call it ``` ```nova // app/src/main.nova import geometry.Point; // one item; `import geometry;` brings all its public items fn main() { let p = Point.create(3, 4); // imported let q = geometry.origin(); // qualified Console.writeLine(p.norm() + q.norm()); } ``` - `geometry.X` names a dependency's public item, in types, expressions and patterns. - `import geometry.X;` brings one item into the file's scope, and `import geometry;` all its public items. Imports apply to their file only. A name two imports provide is an error where it is used. - Within a package, the name of a dependency is reserved: nothing else in the package may be called `geometry`. - A public function may not expose a type its users cannot see (a public function returning an internal struct). - Two packages may declare the same name: in the generated C, a dependency's items carry its package's name. ## The lock file ```sh novac fetch # resolves dependencies and writes nova.lock novac build # uses nova.lock; fails if it does not match nova.toml ``` `nova.lock` records every dependency, direct and indirect: where it comes from, and for a git dependency the exact commit and a SHA-256 of its files. Commit it with `nova.toml`, so that every build of the package uses the same code. - A `tag` or `branch` is resolved to a commit by `novac fetch` and stays that commit until the next fetch, even if the branch moves. - Every build checks each git dependency's files against the lock's hash, so a checkout changed behind your back is an error. - Git repositories and checkouts are cached in `~/.nova` (or `$NOVA_HOME`). `--offline` builds never touch the network. - Nothing runs during a fetch or a build except `git` and the C compiler: packages have no build scripts. - Two path or git dependencies that need the same package must agree on its source and commit. ## The registry A dependency can also be a version requirement, looked up in the package index: ```toml [dependencies] shapes = "2.1" # any 2.x from 2.1.0 on geometry = "=1.4.0" # exactly json = { version = "0.9", index = "https://example.org/my-index.git" } ``` `novac fetch` chooses the newest version of each package that satisfies every requirement on it, and records its commit and hash in `nova.lock`, so builds stay pinned until the next fetch. A version already locked is kept while it still fits; `novac fetch --update` (or `--update shapes`) chooses afresh. When no choice works, the error says which requirements clash and where each came from. To publish a library, give its manifest a `version`, commit and push it, and run `novac publish`. It checks the package and prints the entry to add to the index by pull request. ## Building ```sh novac check # the package and its dependencies, checked together novac build # out/ novac run --interp novac fmt # the package's own sources novac doc -o api.md # its public items ``` A build compiles the package and all its dependencies together into one C program, so generic code from a dependency is specialised for its users and checked like the rest. ## Files without a package Everything also works on files: `novac build a.nova b.nova -o tool`. Files given together are one package with no dependencies. # Formatting and documentation ## Formatting `novac fmt` rewrites Nova source in one canonical style. It has no options, so code formatted by anyone looks the same. The style is specified in [Formatting](/reference/formatting). ```sh novac fmt # the package's sources (or: novac fmt file.nova dir/) novac fmt --check # change nothing; list the files that would change; exit 1 if any novac fmt --stdout file.nova # print one file formatted ``` The style, briefly: - 4-space indentation, no tabs, at most one blank line in a row, lines up to 120 columns; - a block or list you wrote on one line stays on one line if it fits; one you broke across lines is laid out one element per line, with a trailing comma; - a line that is too long is broken at the outermost place that helps: an expression body, then a list, then an initialiser, then an operator; - `} else {` on one line, and the blocks of an `if`/`else` chain are broken together; - trailing comments on consecutive lines are aligned. It is safe by construction: the formatter only changes whitespace and trailing commas, and before returning it parses its own output and checks that the program and its comments are unchanged. A file with a syntax error is left alone. ## Documentation `novac doc` writes one Markdown page for a package's public items: each item's signature, as the formatter prints it, and its `///` doc comment, which is Markdown. ```sh novac doc -o api.md --title "geometry" novac doc --missing # list the public items without a doc comment; exit 1 if any ``` ```nova /// A point in the plane. /// - coordinates are exact integers; /// - `norm` is the squared length. public struct Point { public x: i64; public y: i64; /// The squared distance from the origin. public fn norm(this) -> i64 => this.x * this.x + this.y * this.y; } ``` becomes a section with `public struct Point { public x: i64; public y: i64; }`, the list, and a subsection for `Point.norm`. The [lib/core reference](/reference/api/core) on this site is generated this way. # Editors ## VS Code The extension in `editors/vscode` gives `.nova` files syntax highlighting, and through the language server: errors as you type, the type or signature on hover, go to definition (F12), the outline, and formatting (Shift+Alt+F). Build and install it with [Vite+](https://viteplus.dev/): ```sh cd editors/vscode vp install vp test # the grammar, and novac lsp through VS Code's own client library vp run package # makes nova-language-0.1.0.vsix code --install-extension nova-language-0.1.0.vsix ``` The extension starts `novac lsp`. It finds `novac` on your `PATH`, or where the setting `nova.server.path` says. ## Other editors `novac lsp` is a standard Language Server Protocol server on standard input and output, so any editor with an LSP client can use it: Neovim, Helix, Emacs, Zed, Sublime Text. Configure it to run `novac lsp` for `.nova` files. The TextMate grammar in `editors/vscode/syntaxes/nova.tmLanguage.json` works in editors that accept TextMate grammars. ## What the server does | | | |---|---| | Diagnostics | every error and note of `novac check`, as you type | | Hover | the type of an expression; `let`/`var` and the type of a variable; a function's signature; a field's type | | Go to definition | locals, parameters, functions, methods, fields, structs, enums and interfaces in the file | | Document symbols | the outline: functions, structs with their fields and methods, enums, interfaces, constants | | Formatting | the whole file, in `novac fmt`'s style | A file that belongs to a package is checked with the package and its locked dependencies; any other file is checked on its own. The details are in [Tooling §2](/reference/tooling#_2-language-server-t3). # Debugging A Nova program is debugged as the C it compiles to, with the C mapped back to Nova lines, so lldb and gdb work without knowing anything about Nova. ## A debug build ```sh novac build -g main.nova -o program lldb ./program # or: gdb ./program ``` `-g` writes `#line` directives into the C and compiles it with `-g -O0`. Breakpoints then go on Nova lines, and stepping moves through Nova statements. | | lldb | gdb | |---|---|---| | Stop at a line | `b main.nova:12` | `break main.nova:12` | | Stop when a check fails, before the program exits | `b nova_panic` | `break nova_panic` | | Where am I | `bt` | `bt` | ## Names The C names follow fixed rules, so a Nova name is easy to find: | Nova | C | |---|---| | function `length` | `nvF_length` | | method `Counter.bump` | `nvM_7Counter_bump` (the type's name, prefixed by its length) | | local or parameter `sum` | `v_sum_1` | | a temporary the compiler made | `t_3` | | field `x` | `f_x` | | struct `Point` | `nvT_Point` | | an item of a dependency `geometry` | `nvF_geometry_area`, `nvT_geometry_Point` | ## Nova names in the debugger `editors/debug` adds two commands that print with Nova names: ```sh lldb ./program -o "command script import editors/debug/nova_lldb.py" gdb ./program -ex "source editors/debug/nova_gdb.py" ``` - `nova-locals`: the current frame's locals and parameters, without the compiler's temporaries, with struct fields by their Nova names: `p = { x: 3, y: 4 }`. - `nova-bt`: the backtrace as `Counter.bump at main.nova:6`. ```text (lldb) b main.nova:12 (lldb) run (lldb) nova-locals p = { x: 3, y: 4 } sum = 7 (lldb) nova-bt length at main.nova:12 main at main.nova:18 ``` ## When the program panics A failed check prints the reason and the Nova location, and exits with status 134. Under a debugger, a breakpoint on `nova_panic` stops at the moment of the panic, with the whole call stack to inspect: ```text (lldb) b nova_panic (lldb) run (lldb) nova-bt nova_panic at nova_rt_core.c:207 nv_add_u8 at rt.h:190 grow at main.nova:2 main at main.nova:7 ``` For memory errors in `unsafe` code, `novac run --interp` is often faster: the interpreter stops at the first invalid access and names it. # Coming from another language Nova borrows deliberately from several languages, and each will feel partly familiar: - its **syntax** from C# and TypeScript: braces, `fn name(x: T) -> R`, `let` and `var`, generics in `<…>`; - its **ownership and borrowing** from Rust, with less annotation and a simpler checker; - its **explicit allocators** from Zig, with use-after-free made a compile error; - its **regions** from Cyclone, and its **compile-through-C** design and compiler-managed memory from Goose; - its **deterministic destruction** from C++'s RAII, without the undefined behaviour. These pages map what you know to Nova, show the same program side by side, and point out the differences that will trip you up. | If you know | Read | The biggest changes | |---|---|---| | Rust | [Nova for Rust programmers](./rust) | no lifetime syntax on plain borrows; allocators per allocation; checks in every build; no macros or traits-as-types | | Zig | [Nova for Zig programmers](./zig) | use-after-free and dangling references are compile errors; checks never turn off; generics instead of `comptime` | | C or C++ | [Nova for C and C++ programmers](./c-and-cpp) | the compiler tracks ownership and borrows; no undefined behaviour; no preprocessor | | C#, Java or TypeScript | [Nova for C#, Java and TypeScript programmers](./csharp-java-typescript) | no garbage collector, no classes or inheritance, no exceptions, no null references | | Go | [Nova for Go programmers](./go) | no garbage collector; errors propagate with `?`; interfaces are explicit and static; no goroutines yet | A comparison of the languages as a whole, in one table, is on the repository's [README](https://github.com/vkuttyp/Nova#how-nova-compares). # Nova for Rust programmers Nova's safety model is Rust's: single ownership, moves, shared and exclusive borrows, deterministic destruction, no garbage collector. What differs is how much you write down, where memory comes from, and how much the language does for you. This page assumes you know Rust well. ## At a glance | Rust | Nova | Notes | |---|---|---| | `let x = 1;` / `let mut x = 1;` | `let x = 1;` / `var x = 1;` | | | shadowing (`let x = x + 1;`) | not allowed | two locals in scope never share a name | | `&T`, `&mut T` | `borrow T`, `borrow mut T` | | | `f(&x)`, `f(&mut x)` | `f(x)`, `f(mut x)` | a shared borrow is implicit at the call; a mutable one is marked | | `fn f<'a>(a: &'a T, b: &T) -> &'a T` | `fn f(a: borrow T, b: borrow T) -> borrow(a) T` | the result names its sources; no lifetime parameters | | `struct S<'a> { s: &'a str }` | `struct S { s: StringView; }` | only types that store references declare regions | | `Box`, `Vec`, `String`, `&str` | `Box`, `Vector`, `String`, `StringView` | | | `Option`: `Some(x)`, `None` | `T?`: `Some(x)`, `null` | a `T` converts to `T?` where one is expected | | `Result`, `?` | `Result`, `?` | the same | | `.unwrap()` | `!` (postfix) | `r!`, `o!` | | `panic!` (unwinds by default) | panics always abort | no unwinding, no `catch_unwind` | | `impl Drop for S` | `drop { … }` inside the struct | | | `trait`, `impl Trait for T`, `T: Trait` | `interface`, `impl Interface for T`, `T: Interface` | bounds only: no `dyn` | | `#[derive(Clone, Copy)]` | automatic | a struct is Copy when all its fields are and it has no `drop` | | `x as u8` (truncates) | `truncate(x)` | `as` only converts where nothing can be lost | | `wrapping_add`, `checked_add`, `saturating_add` | `wrappingAdd`, `checkedAdd`, `saturatingAdd` | | | `\|x\| x + 1` | `(x) => x + 1` | closures borrow their captures and cannot escape | | `match x { A => …, }` | `match (x) { E.A => { … } }` | variants are qualified; statement arms are blocks | | `println!("{}", x)` | `Console.writeLine(x)` | no macros, no format strings yet | | `#![no_std]` | `--profile freestanding` | | | `unsafe { }` with a `// SAFETY:` convention | `unsafe { }` with a required `/// Safety:` doc line | the compiler enforces it | | Cargo, `Cargo.toml`, `Cargo.lock` | `novac`, `nova.toml`, `nova.lock` | path and git dependencies only; no registry | | `rustfmt`, `rustdoc` | `novac fmt`, `novac doc` | | ## The same program ```rust fn longest_word(text: &str) -> Option<&str> { let mut best: Option<&str> = None; for word in text.split(' ') { if best.map_or(true, |b| word.len() > b.len()) { best = Some(word); } } best } fn main() { match longest_word("the quick brownish fox") { Some(w) => println!("{}", w), None => println!("empty"), } } ``` ```nova fn longestWord(text: StringView) -> StringView? { var bestStart: usize = 0; var bestLength: usize = 0; var start: usize = 0; for (i in 0..text.length + 1) { if (i == text.length || text[i] == ' ') { if (i - start > bestLength) { bestStart = start; bestLength = i - start; } start = i + 1; } } if (bestLength == 0) { return null; } return text[bestStart..bestStart + bestLength]; } fn main() { match (longestWord("the quick brownish fox")) { Some(w) => { Console.writeLine(w); } null => { Console.writeLine("empty"); } } } ``` ```text brownish ``` The Nova version has no iterator adaptors (there is no `split` yet), returns explicitly, and its signature says the result views the input through the region `r`. With a single reference parameter, as here, the region could also be left out: `fn longestWord(text: StringView) -> StringView?` means the same. ## Ownership and borrowing: what is the same Moves, Copy types, "one writer or many readers", borrows ending at their last use, two-phase borrows for `v.push(v.length)`, references that cannot outlive their referent, and destruction at scope end in reverse order: all as in Rust. Error messages name the borrow, where it was made and where it is used later. ## What is different **Less annotation.** A function that returns a reference names the parameters it may borrow from, `-> borrow(a, b) T`, instead of declaring lifetime parameters. Types that store references declare `region` parameters, and so do functions that pass such types through; plain functions over references never do. Regions are erased: `Token` and `Token` are the same type. **A simpler checker.** Nova's borrow checker is intraprocedural and keeps one set of loans per value, where Rust reasons about lifetimes. Borrows end at their last use, as with NLL, but some programs Rust accepts are rejected; the usual fixes are to end a borrow earlier, copy a value out, or use an index or a slot-map handle. Reference locals are `let`, never `var`: assigning to a reference writes through it. **Allocators per allocation.** Every allocating type takes its allocator as a type parameter (`Box`, `Vector`), and every allocation returns a `Result`: there is no infallible `push`. Arenas, pools, fixed buffers and scratch frames are built in, and allocations from a region borrow it, so they cannot outlive it. **Checks in every build.** Integer overflow panics in release builds too; there is no `--release` that turns it off. Bounds and overflow checks the compiler proves unnecessary are removed, and `--explain-bounds-checks` shows which. **No traits as types.** Interfaces are bounds and nothing else: no `dyn`, no default methods, no associated types, no blanket `impl`. Heterogeneous collections are enums. **Closures do not escape.** A closure borrows what it captures and has a reference type (`borrow fn(i32) -> bool`, `borrow mut fn(i32)`), so it cannot be returned or stored. There is no `move` closure, `Fn`/`FnMut`/`FnOnce` trait or `impl Fn`. Calls through a closure are indirect. **No macros, no iterator chains.** `for` walks ranges, arrays, slices and vectors; slices and vectors have `sortBy`, `forEach`, `any`, `all` and `retain`. There is no `Iterator` trait yet. **Through C.** Nova compiles to C11, which any C toolchain or kernel build can take, including MSVC and compilers for targets LLVM does not support. The generated C is readable and maps back to Nova lines. **Kernel checking.** Context effects (`@context(atomic)`, `@effects(sleep)`) reject sleeping under a spin lock across calls, at compile time. Rust has no equivalent in the language. ## Things you will miss `Rc`/`Arc` and interior mutability (`Cell`, `RefCell`), threads and `Send`/`Sync` (there are no user threads yet), `dyn Trait`, iterators, pattern guards (`if` in a match arm), slice patterns, `impl` blocks on generic types, the standard library's breadth, and crates.io. Several are on the [roadmap](/reference/roadmap) or are [open decisions](/reference/open-decisions). # Nova for Zig programmers Zig and Nova agree on a lot: no hidden allocation, allocators as explicit values, errors as values, `defer`, no garbage collector, first-class C interop, and code that can run in a kernel. They differ on what guarantees safety. Zig checks bounds, overflow and null at run time in its safe build modes, and leaves use-after-free and dangling pointers to the programmer. Nova makes those compile errors, through ownership and borrowing, and keeps its run-time checks on in every build. ## At a glance | Zig | Nova | Notes | |---|---|---| | `const x = 1;` / `var x: u32 = 1;` | `let x = 1;` / `var x: u32 = 1;` | | | `[4]u8`, `[]u8`, `[]const u8` | `[u8; 4]`, `SliceMut`, `Slice` | slices are borrows: they cannot outlive their array | | `?T`, `null`, `orelse`, `.?` | `T?`, `null`, `match`, `!` | | | `!T`, `error.X`, `try`, `catch` | `Result`, `Err(E.X)`, `?`, `match` | errors are ordinary enums | | `catch unreachable` | `!` (postfix) | | | `defer`, `errdefer` | `defer` | locals are destroyed automatically: most `defer`s are unnecessary | | `allocator.create(T)`, `allocator.destroy(p)` | `Box.createIn(allocator, value)`; destroyed automatically | the allocator is part of the box's type | | `std.ArrayList(T)` with an allocator | `Vector` | | | `std.heap.ArenaAllocator` | `Arena` | allocations borrow the arena: none survives it | | `std.heap.FixedBufferAllocator` | `FixedBufferAllocator` | | | `+%`, `+\|`, `@addWithOverflow` | `wrappingAdd`, `saturatingAdd`, `checkedAdd` | | | `@intCast`, `@truncate` | `as` (lossless only), `truncate` | | | `comptime` generics | `` generics with interface bounds | no compile-time execution | | `@cImport` | `extern "C"` declarations; C structs imported by `@header` | | | `export fn`, `callconv(.C)` | `@export`, `@callback` | | | Debug / ReleaseSafe / ReleaseFast | one mode, always checked | proved checks are removed | | `std.debug.print` | `Console.writeLine` | | ## The same program ```zig // Zig 0.13 const std = @import("std"); fn sumAll(allocator: std.mem.Allocator, n: usize) !u64 { var list = std.ArrayList(u64).init(allocator); defer list.deinit(); var i: u64 = 0; while (i < n) : (i += 1) try list.append(i * i); var total: u64 = 0; for (list.items) |x| total += x; return total; } pub fn main() !void { const total = try sumAll(std.heap.page_allocator, 10); std.debug.print("{}\n", .{total}); } ``` ```nova fn sumAll(n: u64) -> Result { var list = Vector.create(); // freed at the end of the function, by its owner for (i in 0..n) { list.push(i * i)?; } var total: u64 = 0; for (x in list) { total += x; } return Ok(total); } fn main() { Console.writeLine(sumAll(10)!); } ``` ```text 285 ``` There is no `deinit`: the vector is destroyed when its owner goes out of scope, on every path, including the early return of `?`. Another allocator goes in the type, `Vector` for an allocator type `A`. An arena works the Zig way instead: `arena.allocArray(n, 0u64)` returns a slice that borrows the arena, and everything is freed with it. ## What changes **Lifetimes are checked.** Returning a slice of a local array, keeping a pointer into an `ArrayList` across an `append`, or using an arena allocation after `arena.deinit()`: each is a compile error in Nova, with a message naming the borrow. In Zig these compile and fail at run time, if they are noticed at all. **Destruction is automatic.** A struct with a `drop` block, or that owns a box, vector or file, is destroyed exactly once when its owner goes away. Ownership moves on assignment, so there is no double free to avoid by convention. **Errors carry types.** An error is any type, usually an enum with payloads (`ParseError.BadDigit(c)`), not a global error set. `?` requires the function's error type to match; there are no implicit error-set unions. **Generics, not `comptime`.** Nova's generics are type parameters checked once, for every type argument, against the bounds you declare. There is no compile-time evaluation of arbitrary code, no `@typeInfo` and no reflection. Constants are evaluated at compile time; functions are not. **Safety is not a build mode.** There is no ReleaseFast. Overflow and bounds checks are always on, and the compiler removes those it proves cannot fail. ## Things you will miss `comptime`, `@cImport` of whole headers, packed structs and arbitrary-width integers (`u3`), SIMD vectors, inline assembly, async, the cross-compiling toolchain in one binary, and Zig's build system. Nova relies on a C compiler for code generation and cross compilation. # Nova for C and C++ programmers Nova compiles to C, calls C directly, and is designed to live inside C codebases, kernels included. What it adds is a compiler that tracks who owns each piece of memory and who is looking at it, and rejects the programs that would use it wrongly. What it removes is undefined behaviour. ## At a glance | C / C++ | Nova | Notes | |---|---|---| | `int32_t x = 1;` | `let x: i32 = 1;` (or `var` to change it) | | | `T *`, `const T *` | `borrow mut T`, `borrow T` in safe code; `*mut T`, `*T` in `unsafe` | references are never null and never dangle | | `malloc` / `free`, `new` / `delete` | `Box.create(v)`; freed by its owner | allocation failure is a `Result`, not `NULL` or an exception | | `std::unique_ptr` | `Box` | | | `std::vector` | `Vector` | growing it while an element is referenced is a compile error | | `std::string`, `std::string_view` | `String`, `StringView` | UTF-8 only | | `std::optional` | `T?` | | | destructors, RAII | `drop { … }` blocks; destruction at scope end | moves leave nothing behind to destroy twice | | `std::move(x)` | assignment moves by default; `move x` to end a use early | using a moved value is a compile error | | exceptions, `errno`, return codes | `Result` and `?` | | | `assert`, `abort` | panics: bounds, overflow, unwrap | always on | | templates | generics with interface bounds | checked once, before use; no SFINAE | | `#include`, the preprocessor | packages and `import` | no macros, no conditional compilation | | `struct` layout | the same as C's, in declaration order | `@repr(C)` to cross the boundary | | `volatile` | `Volatile`, `p.readVolatile()` | | | `-fsanitize=address,undefined` | needed only for `unsafe` code; `novac run --interp` checks it | | ## The same program ```cpp #include #include int main() { std::vector v = {1, 2, 3}; int &first = v[0]; v.push_back(4); // may reallocate: `first` now dangles std::printf("%d\n", first); } ``` In C++ this compiles and has undefined behaviour. In Nova it does not compile: ```nova fn main() { var v = Vector.create(); v.push(1)!; let first = borrow v[0]; v.push(4)!; Console.writeLine(first); } ``` ```text main.nova:5:5: error[N0241]: cannot borrow 'v' as mutable while 'v[0]' is borrowed | 5 | v.push(4)!; | ^ = note: 'v[0]' is borrowed here (4:5) = note: the borrow is used later here (6:5) ``` The fixes are the ones you would reach for anyway: copy the value (`let first = v[0];`), or take the reference after the last push. ## No undefined behaviour Every operation that is undefined in C is defined in Nova, by a check or by a rule: | Undefined in C | In Nova | |---|---| | signed overflow | stops the program (`wrappingAdd` and friends where wrapping is meant) | | out-of-bounds access | stops the program | | use after free, double free | compile errors | | reading uninitialised memory | a compile error | | null dereference | there are no null references; optionals must be checked | | shift by the width or more | stops the program | | unsequenced modifications | evaluation order is left to right | | strict aliasing violations | the generated C never relies on them | The generated C is written so that it has no undefined behaviour itself, and is warning-free under strict flags, so the C compiler's optimiser has nothing to exploit. ## Working with an existing C codebase - Call C functions from an `extern "C"` block, inside `unsafe`. - Export Nova functions with `@export`; `novac` writes `nova_exports.h` for your C code to include. - Use your codebase's own structs from its headers (`@header`, `@ctype`) instead of re-declaring them: the C compiler checks every field Nova uses. - Build a freestanding object with `--profile freestanding --emit obj` and link it with your build. See [Unsafe code and C](../unsafe-and-c) and [Kernel code](../kernel). ## Things you will miss The preprocessor, inline assembly, unions (enums with payloads replace tagged unions), bit fields, pointer arithmetic in safe code, varargs, goto, and in C++, classes with inheritance and virtual functions, operator overloading, overloads, exceptions, and `constexpr` evaluation. # Nova for C#, Java and TypeScript programmers Nova's syntax was chosen to look like C# and TypeScript: braces, `fn name(x: T) -> R`, `let` and `var`, generics in angle brackets, `public` and `internal`, `using`, interfaces. Underneath, it works very differently: there is no garbage collector, no classes, no inheritance, no exceptions and no null references. Memory is managed at compile time, by tracking ownership. ## At a glance | C# / Java / TypeScript | Nova | Notes | |---|---|---| | `var x = 1;` / `const x = 1;` | `var x = 1;` / `let x = 1;` | | | `class` | `struct` with methods | no inheritance; values, not references | | `interface`, `class C : I` | `interface`, `impl I for C` | interfaces are generic bounds, not types | | `new Point(1, 2)` | `Point { x: 1, y: 2 }` | on the stack, or `Box.create(…)` on the heap | | garbage collection | ownership: destroyed when the owner goes away | deterministic, at a point the language defines | | `IDisposable`, `using` | `drop { … }`, `using` | every owner runs `drop`, not only `using` | | `null` references | `T?`, which must be checked | a plain `T` is never null | | exceptions, `try`/`catch` | `Result`, `?`, `match` | panics are bugs and cannot be caught | | `ref`, `out` | `borrow mut T` parameters, `mut x` at the call | | | `List`, `string` | `Vector`, `String` / `StringView` | | | LINQ, lambdas | `sortBy`, `forEach`, `any`, `all`, `retain`; `(x) => x > 2` | lambdas borrow and cannot escape | | `async` / `await`, threads | not yet | | | NuGet, Maven, npm | `nova.toml`, path and git dependencies | | ## The same program ```csharp class Account { public long Balance { get; private set; } public void Deposit(long amount) => Balance += amount; } var a = new Account(); a.Deposit(100); Console.WriteLine(a.Balance); ``` ```nova struct Account { private balance: i64; public fn create() -> Account => Account { balance: 0 }; public fn deposit(borrow mut this, amount: i64) { this.balance += amount; } public fn current(borrow this) -> i64 => this.balance; // a method cannot share a field's name } fn main() { var a = Account.create(); a.deposit(100); Console.writeLine(a.current()); } ``` ```text 100 ``` The receiver says how a method uses its object: `borrow this` reads it, `borrow mut this` changes it, and `this` takes it by value. Calling `deposit` needs `a` to be a `var`. ## What changes **Values, not references.** Assigning a struct copies it (if every field is copyable) or moves it, never shares it. Two variables never point to the same object unless one *borrows* the other, and a borrow is checked: while something is borrowed mutably, nothing else can use it. **Destruction is deterministic.** An object is destroyed when its owner goes out of scope, by assignment, or when the object that owns it is destroyed. A `drop` block runs then, exactly once: closing a file or releasing a lock does not depend on a collector or a `finally`. **Errors are return values.** A function that can fail returns `Result`; the caller matches it, or passes it on with `?`. Out of memory is an error value too. Bugs (an index out of range, an overflow) stop the program instead of throwing. **No null.** `StringView` can never be null; `StringView?` can, and the compiler requires a check before use. **Generics are compiled per type,** like C++ templates and unlike Java's erasure, and checked once, like C#'s. Interfaces constrain type parameters (``), but there is no `List` holding different shapes: use an enum. ## Things you will miss Classes and inheritance, interfaces as types, reflection, attributes on everything, properties, events, LINQ's query syntax, string interpolation, exceptions, `async`, threads, and a large standard library. # Nova for Go programmers Go and Nova share some taste: a small language, errors as values, `defer`, a canonical formatter, packages as directories, and fast feedback. They differ in how memory is managed. Go has a garbage collector and a runtime; Nova has neither, and tracks ownership at compile time instead, which is what lets it run in a kernel. ## At a glance | Go | Nova | Notes | |---|---|---| | `x := 1`, `var x int32` | `let x = 1;`, `var x: i32 = 0;` | `let` cannot be reassigned | | `func f(a int) (int, error)` | `fn f(a: i32) -> Result` | | | `if err != nil { return err }` | `?` | | | `panic`, `recover` | panics abort; no recovery | | | `defer` | `defer` | runs at the end of the block, not the function | | slices `[]T` (growable) | `Vector` (owns, grows) and `Slice` (borrows) | a slice cannot outlive what it views | | maps | not built in yet | `SlotMap` for handles | | `nil` | `null`, only for optionals (`T?`) | | | interfaces, satisfied implicitly | interfaces, implemented explicitly with `impl` | generic bounds, resolved at compile time | | generics `[T any]` | `` | | | goroutines, channels | not yet | | | `gofmt` | `novac fmt` | one canonical style, no options | | `go.mod`, `go.sum` | `nova.toml`, `nova.lock` | path and git dependencies, pinned by commit and hash | | `go vet`, the race detector | the compiler: ownership, borrows, context effects | | ## The same program ```go package main import ( "errors" "fmt" ) func divide(a, b int) (int, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } func average(xs []int) (int, error) { sum := 0 for _, x := range xs { sum += x } return divide(sum, len(xs)) } func main() { avg, err := average([]int{3, 5, 10}) if err != nil { fmt.Println(err) return } fmt.Println(avg) } ``` ```nova enum MathError { DivisionByZero, } fn divide(a: i64, b: i64) -> Result { if (b == 0) { return Err(MathError.DivisionByZero); } return Ok(a / b); } fn average(xs: Slice) -> Result { var sum: i64 = 0; for (x in xs) { sum += x; } return divide(sum, truncate(xs.length)); // usize to i64 could lose bits: say so } fn main() { let values = [3i64, 5, 10]; match (average(values[..])) { Ok(avg) => { Console.writeLine(avg); } Err(MathError.DivisionByZero) => { Console.writeLine("division by zero"); } } } ``` ```text 6 ``` `xs.length` is a `usize`, and Nova has no implicit conversions: `as` converts only where nothing can be lost, so a `usize` becomes an `i64` through `truncate`. ## What changes **No garbage collector.** Every value has one owner and is destroyed when the owner goes away. Where Go lets any number of pointers share an object and keeps it alive, Nova lets one owner keep it and others *borrow* it for a while, and checks that no borrow outlives the object. **Allocation is visible and fallible.** Values live on the stack unless the code asks for a box, a vector or a string, and each of those returns a `Result`: out of memory is an error, not a crash. **Errors propagate with `?`** instead of `if err != nil`. The error type is part of the signature, usually an enum. **Interfaces are explicit and static.** A type implements an interface with an `impl` block, and interfaces are used as generic bounds, resolved at compile time. There is no interface value holding "some type", and no type switch. **Overflow is checked.** Integer arithmetic that overflows stops the program; Go wraps silently. ## Things you will miss Goroutines, channels and `select`, maps, interface values and type switches, struct embedding, the standard library, and Go's fast tooling for large codebases. # Threads A program can run work on several threads. The compiler checks that safe code never has a data race: a value moves to another thread only if it is safe there, and threads share memory only through types made for it. ## Spawning and joining `Thread.spawn` starts a thread running a `move` closure, which owns what it captures. It returns a `Thread`, and `join` waits for the thread and gives back its result. ```nova fn main() { var numbers = Vector.create(); for (i in 1..101u64) { numbers.push(i)!; } let worker = Thread.spawn(move () => { var sum: u64 = 0; for (n in numbers[..]) { sum += n; } return sum; })!; Console.writeLine(worker.join()); } ``` ```text 5050 ``` - The vector is moved into the thread. The spawner cannot use it afterwards, so nothing is shared. - Spawning can fail (no memory, no thread): `Thread.spawn` returns a `Result`, here unwrapped with `!`. - A `Thread` that is dropped without `join` is joined then: every thread ends before `main` returns. - A panic in any thread ends the whole program. ## What may cross: `Send` A value may move to another thread if it is `Send`. Plain data, strings, vectors and boxes are. References are not, nor anything that holds one, nor raw pointers. The compiler infers it, and reports N0334 where it fails: ```nova fn main() { var n: u64 = 5; let address = borrow n as *u64; let t = Thread.spawn(move () => address.isNull())!; } ``` ```text main.nova:4:26: error[N0334]: this thread's closure captures 'address', a '*u64', which is not Send: it cannot move to another thread (type-system.md §38.1) | 4 | let t = Thread.spawn(move () => address.isNull())!; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` `Send` and `Share` can also bound a generic function: `fn run(value: T)`. ## Sharing: `Arc`, `Mutex` and atomics To share a value, give each thread an owner of it: `Arc` counts its owners, and the last one to go frees it. An `Arc` gives only shared access, so a value that threads change goes in a `Mutex`, whose guard lends it to one thread at a time. ```nova struct Tally { count: u64; } fn main() { let tally = Arc>.create(Mutex.create(Tally { count: 0 })!)!; var threads = Vector>.create(); for (t in 0..4u64) { let mine = tally.clone(); threads.push(Thread.spawn(move () => { for (k in 0..250u64) { var guard = mine.get().lock(); let data = guard.data(); data.count += 1; } })!)!; } threads.clear(); // dropping the threads joins them var guard = tally.get().lock(); Console.writeLine(guard.data().count); } ``` ```text 1000 ``` For a counter or a flag, an atomic is simpler: `AtomicU64`, `AtomicU32`, `AtomicUSize` and `AtomicBool` change through a shared reference with `load`, `store`, `swap`, `compareExchange`, `fetchAdd` and `fetchSub`. Those are sequentially consistent: every thread sees every atomic operation in one order. That is the simple rule, and on x86-64 it costs a full barrier on each store. When one thread publishes data and another reads it, release and acquire are enough, and `storeWith` and `loadWith` say so: ```nova struct Message { data: AtomicU64; ready: AtomicBool; } fn main() { let m = Message { data: AtomicU64.create(0), ready: AtomicBool.create(false) }; var got: u64 = 0; Thread.both(() => { while (!m.ready.loadWith(LoadOrdering.Acquire)) {} got = m.data.loadWith(LoadOrdering.Relaxed); }, () => { m.data.storeWith(42, StoreOrdering.Relaxed); m.ready.storeWith(true, StoreOrdering.Release); }); Console.writeLine(got); } ``` ```text 42 ``` The release store makes the data written before it visible to a thread whose acquire load reads `true`. The `…With` methods take C11's orderings (`Relaxed`, `Acquire`, `Release`, `AcquireRelease`, `SequentiallyConsistent`). A weaker ordering never makes a data race, but a wrong one can make a thread read a stale value, and neither `novac run --interp` nor ThreadSanitizer will notice. Use the plain methods unless a measurement says otherwise. `benchmarks/atomics` has one: a ring between two threads, 5 to 9 times faster on x86-64 with release and acquire. ## Borrowing: `Thread.both` A spawned thread cannot borrow, since it may outlive what it borrowed. `Thread.both(a, b)` can: it runs `a` on a new thread and `b` on this one, and returns only when both have finished. So the two closures may use the caller's data, for example the two halves of a slice: ```nova fn sum(values: Slice) -> u64 { var total: u64 = 0; for (v in values) { total = total + v; } return total; } fn main() { let values: [u64; 6] = [1, 2, 3, 4, 5, 6]; let halves = values[..].splitAt(3); var low: u64 = 0; var high: u64 = 0; Thread.both(() => { low = sum(halves.left); }, () => { high = sum(halves.right); }); Console.writeLine(low + high); } ``` ```text 21 ``` The usual borrowing rules hold between the two closures: both may read a value, but if one changes it the other may not use it. A value both read must be `Share`, and one that a closure changes must be `Send`; a reference counts as its referent here. `splitAtMut` gives two halves that each closure may change. If a thread cannot be started, `both` runs `a` and then `b` on this thread, so it never fails. ## Where threads run Threads are part of the hosted profile: Linux, macOS, Windows and CosmoOS user programs. Kernel code uses the kernel's own locks instead ([Kernel code](./kernel)). The interpreter (`novac run --interp`) runs threads one at a time, in a fixed order, so its output is always the same. ## Next [Unsafe code and C](./unsafe-and-c). # Nova Grammar — M1 subset Status: normative for Milestone 1 (increment 1a implements the lexer and parser for exactly this grammar). Later milestones extend it. Any syntax not listed here is rejected by the parser, even if `language-spec.md` shows it for a later milestone. Notation: EBNF. `'x'` is a literal token, `X?` optional, `X*` zero or more, `X+` one or more. ## 1. Lexical grammar ### 1.1 Source text - UTF-8. A leading BOM is ignored. Line terminators: `\n`, `\r\n`, `\r`. - Columns in diagnostics are 1-based and count UTF-16 code units. Lines are 1-based. - Non-ASCII characters may appear only inside comments, string literals and char literals. ### 1.2 Whitespace and comments - Whitespace: space, tab, line terminators, form feed. - `// ...` to end of line. `/* ... */` block comments do **not** nest. An unterminated block comment is error N0003. - Comments and whitespace are discarded by the parser, except doc comments (M8a). The lexer records every comment's span for the formatter (M10 T1, formatting.md). - **Doc comments** (M8a): a line comment that starts with exactly `///` (not `////`). A run of doc comment lines, with only whitespace between them, is attached to the declaration whose first token follows the run directly. Its text is each line after `///` with leading spaces removed. Doc comments elsewhere are ordinary comments. Used by the `Safety:` rule (type-system.md §27.4). ### 1.3 Identifiers and keywords ``` Identifier = IdentStart IdentContinue* (not a keyword) IdentStart = 'A'..'Z' | 'a'..'z' | '_' IdentContinue = IdentStart | '0'..'9' ``` A lone `_` is the token `Underscore` (reserved; no meaning in M1). Keywords (all reserved, whether or not M1 uses them): ``` as borrow break const continue defer drop else enum extern false fn for if impl import in interface internal let loop match move mut namespace null own private public return Self static struct this true type unsafe using var where while class async await yield macro ``` Primitive type names (`i32`, `u64`, `bool`, `void`, …) are **identifiers**, resolved by name resolution, not keywords. ### 1.4 Integer literals ``` IntLiteral = (Decimal | '0x' HexDigits | '0b' BinDigits | '0o' OctDigits) IntSuffix? IntSuffix = 'i8'|'i16'|'i32'|'i64'|'isize'|'u8'|'u16'|'u32'|'u64'|'usize' ``` - `_` separators are allowed only *between* digits (`1_000`, `0xFF_FF`). A separator at the start or end of the digit sequence, or doubled, is error N0010. - The value must fit in 64 bits (unsigned), otherwise N0004. Whether it fits the *type* is checked later (type checking). - A digit invalid for the radix is N0008. A missing digit after a prefix is N0008. An unknown suffix is N0009. - Decimal literals have no leading-zero restriction, and there is no octal-by-leading-zero: `010` is ten. ### 1.5 Float literals (typed since M11 G7: type-system.md §35.2) ``` FloatLiteral = Decimal '.' Decimal Exponent? FloatSuffix? | Decimal Exponent FloatSuffix? Exponent = ('e'|'E') ('+'|'-')? Decimal FloatSuffix = 'f32' | 'f64' ``` `1.` and `.5` are not float literals. `1..2` lexes as `1` `..` `2`. ### 1.6 String and char literals - `"..."` on one line. Escapes: `\n \r \t \\ \" \' \0 \u{H..H}` (1–6 hex digits, a Unicode scalar value). Other escapes are N0006. A newline or EOF before the closing quote is N0005. - `'c'`: exactly one Unicode scalar value or one escape. Otherwise N0007. Its value is the scalar value, as an integer literal (`type-system.md` §4, M11); it may also be a pattern. ### 1.7 Punctuation and operators ``` ( ) { } [ ] , ; : . .. @ ? -> => + - * / % ! & | ^ ~ && || << = == != < <= > >= += -= *= /= %= &= |= ^= <<= ``` The lexer **never** produces `>>` or `>>=`. It emits single `>` (and `>=`) tokens. The parser forms the shift operator `>>` from two *adjacent* `>` tokens, and `>>=` from `>` immediately followed by `>=`. That keeps nested generic arguments (`A>`) unambiguous in later milestones. Any other character is N0001 (unexpected character). ## 2. Syntactic grammar (M1) ``` CompilationUnit = NamespaceDecl? ImportDecl* Item* EOF NamespaceDecl = 'namespace' QualifiedName ';' (* retired by ADR 0013: accepted, no effect *) ImportDecl = 'import' Identifier ('.' Identifier)? ';' (* M10 T5: a dependency, or one of its items; type-system.md §2 *) QualifiedName = Identifier ('.' Identifier)* Item = Attribute* Visibility? (FunctionDecl | ConstDecl | StaticDecl | StructDecl | EnumDecl | ExternBlock) Attribute = '@' Identifier ('(' AttributeArg (',' AttributeArg)* ')')? (* M8b; type-system.md §28.1 *) AttributeArg = Identifier | StringLiteral | IntLiteral ExternBlock = 'extern' StringLiteral '{' (ExternFn | ExternStruct)* '}' (* M8b; §28.2 *) ExternStruct = Attribute* Visibility? 'struct' Identifier ('{' ExternField* '}' | ';') (* C6b; type-system.md §31 *) ExternField = Attribute* FieldDecl (* C9: @cname; type-system.md §31.1 *) ExternFn = Attribute* Visibility? 'fn' Identifier '(' (Param (',' Param)* ','?)? ')' ('->' Type)? ';' TypeParams = '<' TypeParam (',' TypeParam)* '>' (* M6a; type-system.md §20 *) InterfaceDecl = 'interface' Identifier TypeParams? '{' MethodSig* '}' (* M6b; §21 *) MethodSig = Attribute* 'fn' Identifier '(' Receiver (',' Param)* ')' ('->' Type)? ';' (* attributes: M13 G2, `@effects`; §37.3 *) ImplDecl = 'impl' Type 'for' Type '{' (Attribute* Visibility? FunctionDecl)* '}' (* M6b; §21; attributes C7a *) TypeParam = 'region' Identifier (* M6x: a region, before any type parameter; §25.1 *) | Identifier (':' Type ('+' Type)*)? (* bounds: M6b *) (* `region` is contextual: only `region` followed by a name declares a region. In a type, a region argument is a region name or `static`, written first in the argument list (`Token`, `StringView`, `Slice`) or in `borrow(r) T`; type-system.md §25.2. *) EnumDecl = 'enum' Identifier TypeParams? '{' (Variant (',' Variant)* ','?)? '}' (* M2d *) Variant = Identifier ('(' Type (',' Type)* ','? ')')? Visibility = 'public' | 'internal' | 'private' FunctionDecl = 'unsafe'? 'fn' Identifier TypeParams? '(' ParamList? ')' ('->' Type)? FunctionBody (* 'unsafe': M8a *) ParamList = (Receiver | Param) (',' Param)* ','? Receiver = ('borrow' 'mut'?)? 'this' (* M2a: by value; M2b: reference receivers *) Param = Identifier ':' Type StructDecl = 'struct' Identifier TypeParams? '{' Member* '}' (* M2a; TypeParams M6a *) Member = Visibility? FieldDecl | Attribute* Visibility? FunctionDecl | DropBlock (* method attributes: C7a, type-system.md §32 *) DropBlock = 'drop' Block (* M2c *) FieldDecl = Identifier ':' Type ';' FunctionBody = Block | '=>' Expr ';' ConstDecl = 'const' Identifier ':' Type '=' Expr ';' StaticDecl = 'static' 'mut'? Identifier ':' Type '=' Expr ';' (* M14 K1; type-system.md §39 *) Type = QualifiedName | 'borrow' 'mut'? Sources? Type (* M2b references; Sources only in return types *) | '[' Type ';' IntLiteral ']' (* M2d fixed array *) | ('Slice' | 'SliceMut') '<' Type '>' (* M2d slices *) | 'Result' '<' Type ',' Type '>' (* M3a; type-system.md §17 *) | Type '?' (* M6c: optional; `borrow T?` is an optional reference *) | '(' Type ')' (* M11 (G6): `borrow (T?)` is a reference to an optional *) | Attribute* 'fn' '(' (Type (',' Type)*)? ')' ('->' Type)? (* M11 (G8a): a function value, type-system.md §36.1; G8b′: `@effects(…)` before `fn`, §36.3 *) | Identifier '<' Type (',' Type)* '>' (* M6a: an instantiation of a generic struct or enum; also Box, Pool, … *) | '*' 'mut'? Type (* M8a: raw pointer; type-system.md §27 *) | 'dyn' Type (* M13 G2: an interface instantiation; only behind `borrow` or in `Box<…>`; type-system.md §37. `dyn` is a keyword only here *) | 'dyn' 'mut'? Attribute* 'fn' '(' (Type (',' Type)*)? ')' ('->' Type)? (* M13 G3: an owning closure's type, §36.5 *) Sources = '(' Identifier (',' Identifier)* ')' (* only if a type follows the ')'; else '(' Type ')' *) Block = '{' Statement* '}' Statement = Block | LocalDecl | IfStmt | 'while' '(' Expr ')' Block | 'loop' Block | 'break' ';' | 'continue' ';' | 'return' Expr? ';' | 'using' Identifier (':' Type)? '=' Expr ';' (* M2c *) | 'defer' Statement (* M2c *) | MatchStmt (* M2d; type-system.md §15.2 *) | 'for' '(' Identifier 'in' Expr ('..' Expr)? ')' Block (* M2d; type-system.md §16.4 *) | 'unsafe' Block (* M8a; type-system.md §27.4 *) | Expr AssignOp Expr ';' (* assignment: a statement, not an expression *) | Expr ';' LocalDecl = ('let' | 'var') Identifier (':' Type)? ('=' Expr)? ';' IfStmt = 'if' '(' Expr ')' Block ('else' (IfStmt | Block))? AssignOp = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=' ``` Notes: - Braces are **mandatory** after `if`, `else`, `while` and `loop`, so there is no dangling-else ambiguity. - Assignment is a statement, so `if (x = 1)` is a syntax error. The assignment target must be a name or member access (N0104 otherwise). Whether it is *assignable* (a `var`) is checked later. - A `LocalDecl` without an initialiser must have a type annotation (enforced by the checker, not the parser). ### 2.1 Expressions and precedence From lowest to highest binding. All binary levels are left-associative except comparisons, which are **non-associative**: `a < b < c` is error N0103. | Level | Operators | Notes | |---|---|---| | 1 | `\|\|` | short-circuit | | 2 | `&&` | short-circuit | | 3 | `== != < <= > >=` | non-associative | | 4 | `\|` | | | 5 | `^` | | | 6 | `&` | | | 7 | `<< >>` | | | 8 | `+ -` | | | 9 | `* / %` | | | 10 | `as` | `Expr 'as' Type`, left-assoc (`x as u32 as u64`) | | 11 | unary `- !`, `borrow`, `borrow mut`, `move` | prefix, right-assoc (`- -x`) | | 12 | postfix: call `(...)`, member `.name` | | The ordering puts bitwise operators **above** comparisons (as in Rust and Swift), so `x & 1 == 0` parses as `(x & 1) == 0`. This avoids a well-known C/C# pitfall. ``` Primary = IntLiteral | FloatLiteral | StringLiteral | CharLiteral | 'true' | 'false' | Identifier GenericArgs? | '(' Expr ')' | 'this' | 'unsafe' '{' Expr '}' (* M8a: an unsafe expression *) | IfExpr | MatchExpr (* M11 (G5): type-system.md §15.3 *) | '(' (ClosureParam (',' ClosureParam)*)? ')' '=>' (Block | Expr) (* M11 (G8b): a closure, §36.2 *) | 'move' '(' (ClosureParam (',' ClosureParam)*)? ')' '=>' (Block | Expr) (* M13 G3: a move literal, §36.5 *) | Identifier GenericArgs? '{' FieldInits? '}' (* struct literal, M2a; GenericArgs M6a *) | '[' Expr (',' Expr)* ','? ']' | '[' Expr ';' IntLiteral ']' (* array literals, M2d *) ClosureParam = Identifier (':' Type)? (* a '(' … ')' followed by '=>' is a closure *) IfExpr = 'if' '(' Expr ')' Branch 'else' (IfExpr | Branch) MatchExpr = 'match' '(' Expr ')' '{' (ValueArm (',' ValueArm)* ','?)? '}' ValueArm = Pattern '=>' Branch Branch = Expr | Block (* a Block branch must not complete *) FieldInits = FieldInit (',' FieldInit)* ','? FieldInit = Identifier ':' Expr GenericArgs = '<' Type (',' Type)* '>' (* only when followed by '(', '.' or '{' — see below *) Postfix = Primary ( '(' ArgList? ')' | '.' Identifier | '[' IndexOrRange ']' | '?' | '!' )* (* M3a: propagate / unwrap a Result *) IndexOrRange = Expr | Expr? '..' Expr? (* M2d: element or slice *) ArgList = Arg (',' Arg)* ','? Arg = 'mut' Expr | Expr (* `mut x` for a `borrow mut` parameter, M2b *) ``` **Generic arguments in expressions** (`truncate(x)`, and since M6a `id(x)`, `Pair.make()` and `Pair { … }`): after an identifier, `<` starts generic arguments if the tokens up to the matching `>` can only form types (identifiers, `.`, `,`, `;`, `*` (M8a), `?` (M9a: optional types, as in `Vector?>.create()`), integer literals, `borrow`, `mut`, and balanced `<…>` and `[…]`) and the `>` is followed by `(`, `.` or `{`, or (since C7d, for a generic function's instance as a value, `release`, type-system §34.1) by `)`, `,`, `;` or `]`, which cannot follow a comparison's `>` (it needs an operand). Otherwise `<` is the less-than operator. Write `(f < T) > (x)` to mean the comparison. **Struct literals** (M2a): in expression position, an identifier *immediately followed by `{`* is a struct literal. This is unambiguous in Nova because every `{` that opens a block follows a keyword (`else`, `loop`), a `)` (conditions are parenthesised) or a type (`-> T {`), never an expression. A `Receiver` (`this`) is only meaningful as the first parameter of a function declared inside a `struct`. The parser accepts it as any first parameter; the checker rejects it elsewhere (N0232). ## 3. Error recovery (normative for diagnostics tests) - Expected-token errors (N0100) are reported at the current token. When the missing token is `;`, they are reported at the end of the previous token instead. The missing token is *not* consumed. - After an error, the parser suppresses further diagnostics until it re-synchronises: - in a block: at the next `;` (consumed), `}` (not consumed), or statement keyword (`let var if while loop return break continue`). Balanced `{ }` groups are skipped whole. An item keyword (`fn`, visibility) ends the block, which reports the missing `}`; - inside a `struct` body: at the next member start (identifier followed by `:`, `fn`, or a visibility keyword) or the closing `}`; - at the top level: at the next declaration keyword (`fn const public internal private namespace struct class enum interface impl let var static extern import using type`) outside any skipped `{ }` group, or at EOF. Each misplaced declaration is therefore reported once (N0101). - Every parse loop consumes at least one token per iteration or terminates, so parsing always terminates. # 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. `namespace` declarations 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`; - in an expression, `geometry.area(p)`, `geometry.LIMIT`, `geometry.Shape.Circle(2)`, a struct literal `geometry.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.make()`), since `geometry.Pair.make()` does not parse (grammar.md §2.1: in an expression, generic arguments follow a simple name only). - **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`, with `geometry` a 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 dependency `geometry` into 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 `public` items are visible to other packages, and of a public struct only its `public` fields and methods; `internal`, the default, keeps an item within its package, and `private` within 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. An `impl I for T` applies wherever `I` and `T` are 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/core`** is 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. For `a.b`: if `a` is a local or an item of the package or of `lib/core`, `.b` is a member as before; otherwise, `a` being a dependency (and so not a local or item, N0315), `a.b` is that dependency's item `b`. - **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 `void` is an error (N0224). - Parameters and `const` items are immutable. Assigning to them, or to a `let`, 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 `-128i8` and `-9223372036854775808` (as `i64`) 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, so `c == 'k'` with `c: u8` compares bytes, and `'0'..'9'` is a range. With no integer expectation it is a `u32`. In an 8-bit type it must be ASCII (at most `0x7F`), 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 no `char` type: 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; `return` value → the return type; `const` value → its declared type; `if`/`while` condition → `bool`; each value branch of an `if` or `match` expression → 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 an `if` or `match` expression whose expression branches are all flexible (§15.3). Since M11 (G7), unsuffixed float literals combined the same way are flexible too; they take an expected `f32` or `f64` instead (§35.2). For a binary operator whose operands must have the same type: 1. if the operator's result type is the operand type and an integer type is expected, both operands are checked against it; 2. 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; 3. otherwise both are checked without expectation (flexible ones default to `i32`). So `x + 1` with `x: u8` types `1` as `u8`, and `let y: u64 = 2 * 3;` types both literals as `u64`. ## 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(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 T` is allowed only when **every** value of the source type is representable in `T` (N0208 otherwise): - identity; - `uN → uM` and `iN → iM` for M ≥ N; - `uN → iM` for M > N; - `bool →` any integer type (`false`→0, `true`→1). `isize`/`usize` use the target width. Floats (M11, §35.4): `f32 → f64`, and the integers that a float type holds exactly. - `truncate(e)`: `e` of any integer type, `T` any integer type. The result is `e` modulo 2^bits(T), reinterpreted in `T` (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)` and `Console.writeLine(x)`: `x` is an integer, a `bool`, or a string literal. `writeLine()` with no argument writes a newline. Both return `void`. - `Process.exit(code: i32)`: has type *never*. - `truncate(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`/`continue` must be inside `while` or `loop` (N0214). - `return;` only in a `void` function. `return e;` only in a non-`void` function (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`) reads `x`. 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`, `break` or `continue`, or a call of type *never*. It also may not complete if it is a block containing such a statement, an `if` with an `else` whose two branches both cannot complete, or a `loop` with no `break` that targets it. A `while` loop may always complete, whatever its condition: write `loop` for infinite loops. An `if` or `match` expression (§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-`void` function body may be reached, that is N0213. ## 10. Constants - `const N: T = e;` where `e` is a constant expression: literals, other constants, and unary, binary, `as` and `truncate` applied 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): `T` may be `[E; N]` with `E` an integer type or `bool`, and `e` an 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. - `@export` on 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) ```nova 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-`void` M1 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 function `f` of `S`. - **Copy.** In M2a every struct is a Copy type: its fields are Copy and `drop` does not exist yet. Assignment, argument passing and return copy the value. OD-7's rule ("Copy iff all fields Copy and no `drop`") 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. Each `ei` is checked against the field's type. Initialisers are **evaluated in source order** (`intermediate-representation.md`), not in declaration order. - **Field access.** `e.f` where `e` has struct type `S` and `f` is a field of `S` has `f`'s type (unknown field: N0230; `e` is not a struct: N0234). An access chain `a.b.c` is a *place* if `a` is a local or `this`. - **Field assignment.** `a.f = e` and `a.f op= e` require the root local `a` to be a `var` (N0211). `this` is 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 `this` is a method. `this` has the struct's type and is an immutable binding (a copy for Copy structs). It is called as `e.m(args)`, where `e` is evaluated first and passed as `this`. A static function (no `this`) is called as `S.f(args)`. Calling a method as `S.m()`, or a static function as `e.f()`, is N0233. `this` anywhere 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`). `public` and `internal` fields, and the default, stay visible throughout the package. - **Operators.** No operator applies to struct values in M2a (`==` included: N0207). `Console.write` cannot 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 from `TargetInfo`. 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) and `borrow mut T` (exclusive), where `T` is any non-reference, non-`void` type. `borrow borrow T` is rejected (N0246). At runtime a reference is a non-null pointer. - **Where references may appear (v0):** - parameter types; - `let` locals with an initialiser; - return types (with a source list, §13.5). They may **not** appear in struct fields (E3), constants, or `var` locals (N0246). Struct fields of reference type need lifetime parameters (OD-3). - `borrow T` is Copy. A `borrow mut T` value is never copied: every use reborrows it (§13.3). ### 13.2 Creating references - `borrow p` / `borrow mut p`, where `p` is a **place**: a local, `this`, a field path, or a path through a reference such as `r.f` (`(*r).f`). Borrowing anything else is N0247; bind the value to a local first. - `borrow mut p` requires `p` to be mutable: rooted at a `var` local, or reached through a `borrow mut` reference (N0248). A `let` binding, a parameter, a by-value `this`, and anything reached through a shared reference are immutable. That holds through a `borrow mut` or `SliceMut` stored where a shared reference leads: in `h.v` with `h: borrow Holder` and `v: borrow mut Vector`, the vector cannot be changed, as with Rust's `& &mut T`. - **Call sites.** - A `borrow T` parameter accepts a place or value of type `T`, borrowed implicitly. A value becomes a temporary that lives until the end of the statement. - A `borrow T` parameter also accepts `borrow T` or `borrow mut T`; the latter is coerced by a shared reborrow. - A `borrow mut T` parameter accepts `mut p`, where `p` is a mutable place of type `T`, or a `borrow mut T` value (reborrowed). The `mut` marker is required for a plain place and is rejected where the parameter is not `borrow mut` (N0249). It makes mutation visible at the call site, as C#'s `ref` does. - **Receivers.** `fn m(borrow this)` and `fn m(borrow mut this)` declare reference receivers; `this` then has type `borrow S` / `borrow mut S`. In `e.m(…)` the receiver is borrowed implicitly, with no marker. For `borrow mut this`, `e` must be a mutable place or a `borrow mut S` value (N0248). ### 13.3 Using references - **Auto-deref.** Where a value of type `T` is needed (an operand, a by-value argument, an initialiser of type `T`, a return of type `T`, `Console.write`), a `borrow T` or `borrow mut T` operand is read through: `*r`. Field access `r.f` and method calls `r.m()` also look through references. - **Writes through references.** `r = v`, `r op= v` and `r.f = v` write to the referent. This requires `borrow mut` (N0211: "cannot assign through a shared reference"). A reference local itself can never be re-pointed. - **Reborrow of `borrow mut`.** Using a `borrow mut T` value 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 type `borrow mut T` or `SliceMut` (`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 T` with 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 a `borrow mut` result naming a shared parameter. After `borrow`, 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), so `borrow (T?)` is a reference to an optional and `borrow(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`, and `borrow T`. A struct is Copy iff it has no `drop` block and every field is Copy. - **Move types:** structs with a `drop` block or a move-type field, and `borrow 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 `drop` block, 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 ```nova struct File { fd: i32; drop { Sys.close(this.fd); } // `this` is `borrow mut File` } ``` - At most one `drop` block per struct (N0228). Inside it, `this` has type `borrow mut S`. The block cannot be called explicitly. It runs exactly once when a value of `S` is 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 `drop` block is N0252: the `drop` block needs the whole value. Moving out of `this` inside `drop` is 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 `this` receiver; - a struct-literal field; - a `return` value. 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 p`** explicitly moves out of the place `p`, even for a Copy type. Afterwards `p` is uninitialised, which ends a value's use early. - **Partial moves** (O2): moving `p.f` out of a local is allowed if no struct on the path from `p` to `f` has a `drop` block. `p` is then partially initialised. `p.f` may be reassigned, after which `p` is whole again. Reading `p` as 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 `this` in `borrow`/`borrow mut` methods; - moving out of a `using` binding (N0253). - **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.f` requires the rest of `p` to be initialised (no piecewise construction of an uninitialised struct). Re-initialising a moved-out field is allowed. - If some enclosing struct of `p.f` has a `drop` block, that whole struct must be initialised, `f` included: 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 is `f` (found by the mutation suite, M2d). ### 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`/`continue` leaving the block, or a `return`; - the end of its statement, for a temporary; - being **overwritten** by an assignment, in which case the old value is destroyed first. - **Order:** locals in reverse declaration order; then a struct's `drop` block; 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.exit` and 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`/`continue` out 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) ```nova 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-`void` type (E3 applies to payloads too). An enum must not contain itself by value (N0227). - **Values.** `E.V` for a variant without payload; `E.V(e1, …)` with exactly the payload arity (N0209). Each `ei` is 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 `drop` block (v0). - **Layout.** - A tag (`u8` for ≤ 256 variants, else `u16`/`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). ### 15.2 `match` ```text 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 `match` is 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 `_`. `bool` matches 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 a `borrow T` / `borrow mut T` to the payload, and the borrow checker treats it like any reborrow of the scrutinee. - `_` binds nothing. - 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 `match` statement. - **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 or `bool` payload (`Some(3)`), or `null` on an optional payload (`Ok(null)`). Each is checked against the payload's type exactly as a whole `match` is 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 separate `match` on 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`/`Some` and `Ok`/`Err` as the constructors. Integer literals never cover their type: an integer position needs a binding or `_` somewhere. ### 15.3 `if` and `match` expressions (M11, G5) ```nova 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; }, }; ``` ```text 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 `return` value, …). An `if` or `match` that begins a statement is the statement form (§9, §15.2), as before. An `if` expression needs its `else` (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`, `continue` or 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 + 3` is `if (c) 1 else (2 + 3)`. Write parentheses to use an `if` expression 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 `null` is 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: `f64` if any is a float literal (§35.2), else `i32`. 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. `match` expressions 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 `if` statement (§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 of `N` elements. `N` is an integer literal (no suffix or `usize`) with 1 ≤ N ≤ 2^48 − 1, and the array is at most 2^31 − 1 bytes. It is laid out as `N` consecutive `T`s: size `N × size(T)`, the alignment of `T`. - **v0 restriction:** `T` must 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). - `Slice` / `SliceMut` are **borrowed views** `(pointer, length)` of a run of `T`s in an array or another slice. They follow every rule for references (§13), with the same scoping: - only in parameters, `let` locals with an initialiser, and return types with a source list; - never in fields, payloads, array elements, constants or `var` locals (N0246); - `Slice` is Copy, and each use of a `SliceMut` reborrows it; `SliceMut` coerces to `Slice`; - 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. - Internally, `Slice` is `borrow [T]` and `SliceMut` is `borrow 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 of `v`. - `a.length` is a `usize`: the constant `N` for arrays, the stored length for slices. For an array, `a` must be a place (a variable, field or element), because the constant would skip evaluating it (N0216). ### 16.3 Indexing and slicing - `a[i]`: - `a` is an array or slice place, or a reference to an array; - `i` is a `usize` (a flexible literal becomes `usize`); - `i ≥ a.length` **panics** ("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 a `var` or reached through `borrow mut`, or a `SliceMut`), and borrowed. - `a[lo..hi]`, `a[lo..]`, `a[..hi]`, `a[..]` give a `Slice` of elements `lo` up to but not including `hi` (default `0` and `a.length`). `lo ≤ hi ≤ a.length` is checked; otherwise the program **panics**. - `borrow mut a[lo..hi]` gives a `SliceMut` and needs `a` to be mutable. As a call argument for a `SliceMut` 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`, then `hi`. In an assignment `a[i] = v`, the target's indexes are evaluated and checked before `v`. - The borrow checker treats all elements of an array as **overlapping** (no index reasoning in v0). Borrowing `a[0]` and then writing `a[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) { … }`: `i` takes each value from `lo` up to but not including `hi`. Both bounds are evaluated once, have the same integer type (flexible literals adapt), and `i` is immutable in the body. - `for (x in seq) { … }`: `seq` is an array place, a slice, or a reference to an array. `x` is 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 `T` is not Copy, `x` is a `borrow T` to the element, in place, instead of a copy (which could not be made: N0251). This is the rule of a `match` on a reference (§15.2). The borrow checker treats it as a shared borrow of that element for as long as `x` is used. While it lives, the sequence cannot be changed (§13.4, §23.1). A Copy element is still copied, as before. - `break` and `continue` behave as in `while`. `continue` proceeds 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. ```nova fn copy(out: SliceMut, to: usize, src: Slice, from: usize, n: usize) { out[to..to + n].copyFrom(src[from..from + n]); // two range checks, one length check, one copy } ``` - `d.copyFrom(s)`, with `d: SliceMut` and `s: Slice` (a `SliceMut` coerces): if `s.length != d.length` the program **panics** ("slice lengths differ", reported at the call) and writes nothing. Otherwise each `d[i]` becomes `s[i]`, as if `s` were first copied to a temporary. In safe code the two cannot overlap, because `s`'s loan would conflict with `d`'s (N0241, N0242); if `unsafe` code makes them overlap (§27.3, `Slice.fromRaw`), the result is still defined. - `d.fill(v)`, with `v: T`: every element of `d` becomes `v`. Nothing is checked. - `T` must be Copy (N0259), because both methods copy elements. - The receiver is a `SliceMut` (a parameter, a local or a call result; reborrowed, like any use of a `SliceMut`), or a slicing expression `a[lo..hi]` in any of its forms, which is then taken as `borrow mut a[lo..hi]`. That needs `a` to be mutable (N0248), as `mut a[lo..hi]` does for a `SliceMut` argument. A `Slice` 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 this` methods of a place). So the argument cannot read the destination: `s.fill(s[0])` is N0242, and `let 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), with `i, j: usize`: exchanges elements `i` and `j` of a `SliceMut` (or of a vector, which lends its elements mutably). `T` may 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 == j` changes 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) ```nova enum ParseError { Empty, BadDigit(u8), Overflow } fn digit(c: u8) -> Result { if (c < 48u8 || c > 57u8) { return Err(ParseError.BadDigit(c)); } return Ok((c - 48u8) as u32); } fn twoDigits(a: u8, b: u8) -> Result { 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 a `SliceHalves` whose `left` is `s[..mid]` and `right` is `s[mid..]`. `s.splitAtMut(mid)`, on a `SliceMut` or a vector, gives a `SliceHalvesMut` of two `SliceMut` 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") if `mid` is past the end; the panic names lib/core's line, not the caller's. The halves borrow `s` for as long as either lives. They are lib/core code (`SliceSplitting`, `lib/core/slices.nova`). ### 17.1 Types - `Result` is a built-in enum with two variants, in this order: `Ok` with payload `T`, and `Err` with payload `E`. Everything in §15.1 applies: layout, Copy iff `T` and `E` are Copy, needs drop iff either does, and only the active variant's payload is destroyed. - `T` may be `void`, in which case `Ok` has no payload. `E` must not be `void`. Neither may be a reference or slice (N0246, E3); arrays, structs, enums and results are fine. - A type contains `T` and `E` by value: `struct S { r: Result; }` is recursive (N0227). - `AllocError` is a built-in enum `{ OutOfMemory }` (Copy). The names `Result`, `Ok`, `Err` and `AllocError` cannot be redeclared as items (N0201). - Two `Result` types are equal iff their `T` and their `E` are equal. ### 17.2 Values - `Ok(e)`, `Ok()` (for `T = void`) and `Err(e)` need an **expected `Result` type** that fixes `T` and `E`. 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 `T` or `E` like a call argument against its parameter (§5). ### 17.3 Matching - Patterns `Ok(x)`, `Ok(_)`, `Ok` (for `T = void`), `Err(x)` and `Err(_)` are written without a type prefix. The bindings and exhaustiveness rules of §15.2 apply unchanged: a non-Copy payload is bound by matching on `borrow r` (N0258 otherwise). ### 17.4 `e?` and `e!` ```text Postfix = Primary ( … | '?' | '!' )* -- '!' is postfix only when it follows an operand ``` - **`e?` (propagate).** - `e` must have a `Result` type (N0262). The enclosing function must return `Result` with the same `E` (N0261); `From` conversions come in M6. - If `e` is `Ok(v)`, the value of `e?` is `v` (of type `T`; `void` if `T` is `void`). - If `e` is `Err(x)`, the function returns `Err(x)` at once, exactly like a `return` statement. The statement's temporaries end, then every scope is left (drops and deferred statements), innermost first. Like `return`, `?` is not allowed in a deferred statement (N0254). - **`e!` (unwrap).** `e` must have a `Result` type (N0262). The value is `v` for `Ok(v)`; `Err(_)` **panics** with "unwrapped an Err value". Panic is abort, so nothing is destroyed. - In both cases `e` is evaluated once and consumed. The payload moves out of it (or is copied); nothing else of `e` remains to be destroyed. - As an expression statement: `f()?;` and `f()!;` are allowed when the operand is a call, like calls themselves (N0225 otherwise). - Precedence: postfix, like a call or member access: `parse(a)?.value` and `-x?` work as expected (`-` applies to `x?`). `!` directly followed by `=` lexes as `!=`. Write `r! == 1` with a space. ### 17.5 Discarded values (correction to §14.5) - An expression statement whose value needs drop (for example a call `make();` returning a `Res`) 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) ```nova struct Account { id: u64; balance: i64; fn deposit(borrow mut this, amount: i64) { this.balance += amount; } } fn open(id: u64) -> Result, 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 freed ``` `Box`, `SystemHeap` and `FailingAllocator` are built in (ADR 0003). ### 18.1 Allocator types - `SystemHeap` and `FailingAllocator` are 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 {}`. - `SystemHeap` is the platform heap (`malloc`/`free` in hosted and CosmoOS userland builds). - `FailingAllocator` allocates from the platform heap, but can be told to fail, for testing error paths in the language itself: - `FailingAllocator.failAfter(n: u64)`: the next `n` allocations through `FailingAllocator` succeed, 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` - `Box` owns exactly one value of type `T` in memory obtained from allocator type `A` (H1). `Box` means `Box`. `A` must be an allocator type (N0263). `T` is any value type except `void` and 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 to `A`. - A type does **not** contain the target of a `Box` by value, so recursive types are built with boxes: `enum List { Nil, Cons(i32, Box) }` is allowed (no N0227). - **Creation** (built-in functions): - `Box.create(v)`: `Result, AllocError>`; - `Box.createIn(a, v)` with `a` a value of allocator type `A`: `Result, AllocError>`. `T` is the type of `v`. An expected type `Result, _>` provides the type hint for `v` (so `Box.create(5)` can make a `Box`). `v` is moved into the call. If the allocation fails, `v` is destroyed and the result is `Err(AllocError.OutOfMemory)`. - **Access through a box** works like access through a reference (§13.3), with the box's own mutability: - fields `b.f`, elements `b[i]` and method calls `b.m()` reach `T` (auto-deref); - the contents are mutable iff the box is: the box variable is `var`, or the box itself is reached through `borrow mut` or another mutable place. `b.f = 1` needs a `var b`; - a `Box` argument for a `borrow T` parameter borrows the contents, and `mut b` lends them for a `borrow mut T` parameter; - where a Copy `T` is expected, a `Box` is read through (auto-deref); - a `borrow Box` converts to `borrow T`, and a `borrow mut Box` to `borrow mut T` or `borrow T`: a reborrow of the contents (for example, a payload bound by reference in a match); - `borrow b` borrows the box itself (`borrow Box`). - **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 `Box` is 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 `T` with a `drop` block 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, and `T` has no `drop` block by the rule above. - There is no way to move the whole contents out in v0 (`T` would need an unbox operation, M6). ### 18.4 Moving payloads out in `match` (M3c; replaces N0258) ```text Binding = Identifier | 'var' Identifier | '_' ``` - `var x` makes a mutable binding; `x` an 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`; a `using` binding 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.V` without a list, a variant without payload), the value is destroyed when the match statement ends. - 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 `T` destroys 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 a `Box` again. If there is one, `Box`'s glue becomes a loop: - destroy everything of the current `T` except 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. - 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 `drop` block runs in the order §14.5 defines. - Consequence: lists (`Cons(x, Box)`) 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` and `PoolBox`, 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` whose `T` or `E` is scoped (a *scoped result*); - the scoped region values: `FixedBufferAllocator` (§19.4), `ScratchFrame` (§19.5) and `PoolBox` (§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) ```nova fn process(input: Slice) -> Result { using arena = Arena.create(SystemHeap {}, 65536u64); let counts = arena.allocArray(256usize, 0u32)?; // SliceMut, 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 here ``` - `Arena` is 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)`, with `a` an allocator value (`SystemHeap {}`, `FailingAllocator {}`) and `chunkSize: u64`, makes an empty arena. It cannot fail: memory is obtained in chunks of `chunkSize` bytes when needed. An allocation larger than a chunk gets a chunk of its own. - **Allocation** (R1). Both methods take `borrow this` and return a scoped result that carries a **shared loan of the arena**: - `arena.alloc(v)`: `Result`, with `v` moved into fresh arena memory; - `arena.allocArray(n, fill)` with `n: usize`: `Result, AllocError>`, `n` copies of `fill`. `T` must be Copy. `n` may be 0 (an empty slice); `n × size(T)` beyond the address space is `Err(OutOfMemory)`. The returned references are mutable: each allocation is fresh memory that nothing else refers to. - **R2:** `T` must not need drop, and must not be scoped (no references or slices in arena memory, E3). Both are N0264. - `arena.reset()` takes `borrow mut this` and 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) ```nova fn countWords(text: Slice) -> Result { 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. `s` is a mutable place of type `[u8; N]`, or a `SliceMut`. 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. - `FixedBufferAllocator` is **scoped** (§19.1): a `let` local, a parameter or a return type with sources, never stored. It is a move type without drop; nothing is freed. - `alloc(v)` and `allocArray(n, fill)` work as for `Arena` (§19.2), including R2 (N0264). Each allocation is aligned within the storage. When the remaining bytes are not enough, the result is `Err(AllocError.OutOfMemory)`; the storage is never grown. There is no `reset`: 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) ```nova fn render(n: usize) -> Result { 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 here ``` - Scratch 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`/`allocArray` call 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. - `ScratchFrame` is 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)` and `allocArray(n, fill)` work as for `Arena`, including R2. A failure to obtain a chunk is `Err(AllocError.OutOfMemory)`. - Frames are bound with `using` (R3; §14.6): nesting borrows the frame mutably, and a scoped value cannot be a `var`. Since frames are scoped, they cannot be returned from the function that entered them. ### 19.6 `Pool` and `PoolBox` (M4c) ```nova struct Particle { x: i32; v: i32; } fn simulate() -> Result { using pool: Pool = Pool.create(SystemHeap {}, 64u32); var a = pool.take(Particle { x: 0, v: 2 })?; // PoolBox: 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 destroyed ``` - `Pool` is a built-in allocator for up to `capacity` values of one type `T`. - `Pool.create(a, capacity)`, with `a` an allocator value and `capacity: u32`, makes an empty pool. `T` comes from the expected type (`let pool: Pool = …`); without one it is N0260. - It cannot fail: the slots are allocated from `a` at the first `take`. - `Pool` is a move type that needs drop; destroying it returns the slots to `a`. It is not scoped. - `pool.take(v)` takes `borrow this` and returns `Result, AllocError>`: `v` moves into a free slot. The result is `Err(AllocError.OutOfMemory)` when all slots are taken, or when the slots cannot be allocated; `v` is then destroyed. - `PoolBox` owns one slot and its value, like a `Box` (§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, so **`T` may 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); - `T` must not be scoped (E3; N0264). - Destroying the pool while a `PoolBox` from it is live is rejected (B2), so every slot is free when the pool is destroyed. ## 20. Generics (M6a) ```nova struct Pair { first: T; second: T; fn swap(this) -> Pair => Pair { first: this.second, second: this.first }; } enum Maybe { Nothing, Just(T) } fn largest(p: borrow Pair, pick: bool) -> borrow T { if (pick) { return borrow p.first; } return borrow p.second; } fn main() { let p = Pair { first: 1, second: 2 }; // Pair, inferred from the fields let q = p.swap(); let m: Maybe = Maybe.Just(7u8); let r = largest(p, true); // T = i32, inferred from the argument let s = largest(q, false); // explicit type argument } ``` ### 20.1 Declarations ```text 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`, `Maybe>`. A generic struct or enum must always be given all its type arguments in a type (N0204). - **Calls:** `largest(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 is `i32` unless 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 }` infers `T` from the field values, or takes it from the expected type. `Pair { … }` gives it. Likewise `Maybe.Just(7)` infers from the payload, and `Maybe.Nothing` needs an expected type or explicit arguments: `Maybe.Nothing`. - **Static functions:** `Pair.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 `T` twice is N0250). - Generic declarations that are never used produce no C code. ### 20.4 Instantiation (monomorphisation) - Every distinct use (`largest`, `Pair`) 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` is Copy; `Pair` 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` calling `f>`) is N0266. Diagnostics: N0265 (type arguments that cannot be inferred), N0266 (instantiation too deep). ## 21. Interfaces (M6b) ```nova 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(shapes: Slice) -> i64 { var t = 0i64; for (i in 0..shapes.length) { t += shapes[i].area(); } return t; } ``` ### 21.1 Declarations ```text 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`). Its methods cannot have type parameters of their own, and v0 has no `Self` type. - **`impl I for T { … }`** implements interface `I` (with its type arguments, `I`) for a concrete type `T`: a struct, an enum, or a built-in type such as `i32`. `T` must not be generic in v0 (`impl I for Pair` 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 `impl` block's methods are ordinary methods of `T`. `x.area()` works on any `Square`, and their names must not clash with `T`'s other methods (N0228). - Interfaces are not types: an interface is a bound (§21.2), and since M13 also the `I` of a `dyn I` type 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 every `impl` of it (§37.3). ### 21.2 Bounds - A type parameter may have bounds: ``, ` + Copy>`. - **Inside generic code**, a value whose type is `T` (or a reference to one) may call the methods of `T`'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). - **`Copy`** is a built-in bound: a `T: Copy` is 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 for `Copy`. Otherwise it is N0268, reported where the instantiation is written. - Dispatch is static: an instantiation calls `T`'s `impl` method directly (monomorphisation). ### 21.3 Slices of move types (correction to §16.1) - `Slice` and `SliceMut` 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?` ```nova fn find(xs: Slice, 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) -> 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 optional `T`, for any non-`void` `T`. It is a built-in enum with two variants, in this order: `null` (no payload) and `Some(T)`. Everything in §15.1 applies: it is Copy iff `T` is, needs drop iff `T` does, and is scoped iff `T` is (`borrow T?`). - **Optional references and references to optionals** (M11, G6). `?` applies to the whole type before it, so `borrow T?` is an *optional reference*, `(borrow T)?`. A *reference to an optional* is written with parentheses: `borrow (T?)`, `borrow mut (Box?)`. A type may be parenthesised anywhere a type is written (grammar.md §2). Diagnostics print the two as written: `borrow i32?` and `borrow (i32?)`. A reference to an optional is a reference like any other (§13): a match on it binds references to the payload, `r == null` reads the referent, and `r = v` through a `borrow mut (T?)` replaces the optional, as in ```nova fn append(slot: borrow mut (Box?), value: i32) { match (slot) { Some(node) => { append(mut node.next, value); } null => { slot = Some(Box.create(Node { value: value, next: null })!); } } } ``` - **Values.** - `null` needs an expected optional type (N0260). - `Some(e)` makes a present value; with an expected `T?`, `e` is checked against `T`, otherwise `T` is `e`'s type. - Where a `T?` is expected (an initialiser, an argument, a return value, a field), a value of type `T` converts to `Some(value)` implicitly. - **Matching.** Patterns `null`, `Some(x)` and `Some(_)` are written without a type prefix; the rules of §15.2 and §18.4 apply. - **Comparisons.** `o == null` and `o != null` test for absence, for any `T` (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 returns `null` from the enclosing function, which must return an optional (N0261). `?` on an optional in a function returning a `Result` is N0261 in v0 (`language-spec.md` Ex11). - **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 is `null`, and any other value is `Some`. That covers `Box?`, except a pool box (two pointers), and `borrow T?` and `borrow 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_alloc` gives 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??`) 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?` is 8 bytes rather than 16 on a 64-bit target. ### 22.2 `StringView` ```nova 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 } ``` - `StringView` is a **shared, read-only view of UTF-8 text** `(pointer, length in bytes)`. It is scoped like `Slice` (§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 a `StringView` without scoped parameters (like `greeting` above). The return check still rejects any view that borrows a local (N0244). - **Operations.** - `s.length` is the length in bytes (`usize`); - `s[i]` is the byte at `i` (`u8`, bounds-checked); - `s.bytes()` is the same bytes as a `Slice`; - `s[lo..hi]` (any bound may be omitted) is a `StringView` of bytes `lo` up to `hi`. 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)` and `Console.writeLine(s)` print it. - `for (b in s)` is not defined: iterate over `s.bytes()` (characters come with `String` methods later). Diagnostics: none new. `null` without an expected type is N0260; `?` and `!` on non-optional, non-result values are N0262. ## 23. `Vector` and `String` (M6d) ### 23.1 `Vector` ```nova fn sum(xs: Slice) -> i32 { var total = 0; for (x in xs) { total += x; } return total; } fn main() -> i32 { var v = Vector.create(); // no allocation until the first push v.push(1)!; // Result 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` owns a growable, contiguous buffer of `T` values obtained from allocator type `A` (H2). `Vector` means `Vector`. `A` must be an allocator type (N0263). `T` is any value type except `void` and 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` or `Result, _>`; N0260 otherwise): - `Vector.create()`: an empty vector. It allocates nothing; - `Vector.withCapacity(n)` (`n: usize`): `Result, AllocError>`, an empty vector with room for `n` elements (`n == 0` allocates nothing). - **Members:** - `v.length` and `v.capacity` (`usize`); - `v[i]` is element `i`, bounds-checked (panic "index out of bounds"). It is a place, like an element of a slice: it can be read (Copy `T`), borrowed (`borrow v[i]`, `borrow mut v[i]`) and assigned (`v[i] = x` destroys the old element). Writing needs a mutable vector: a `var`, or one reached through `borrow mut`. Moving an element out is rejected (N0251), as for a slice; - `v.push(x)`: `Result`. It appends `x`, growing the buffer when it is full (new capacity: twice the old, at least 4). If the allocation fails, `x` is destroyed, the vector is unchanged, and the result is `Err(AllocError.OutOfMemory)`; - `v.pop()`: `T?`, the last element moved out, or `null` if the vector is empty; - `v.clear()`: destroys every element; the capacity stays; - `v.truncate(n)` (M11 G8c, `n: usize`): if `n < v.length`, destroys the elements from index `n` up, lowest first, and the length becomes `n`; 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 a `Slice`, and `v.viewMut()` as a `SliceMut`; - a `Vector` argument for a `Slice` parameter is borrowed as `v.view()`, and `mut v` for a `SliceMut` parameter as `v.viewMut()` (like a box's contents, §18.2); - `for (x in v)` is `for (x in v.view())` (§16.4). - **Calls take `this` like methods:** `push`, `pop`, `clear` and `truncate` take `borrow mut this`, and their arguments are evaluated before the mutable loan starts (two-phase, B5, `lifetimes.md` §5). `length`, `capacity`, `view` and element reads take `borrow 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 `==`, no `Copy`. - **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` ```nova fn greet(name: StringView) -> Result { 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` owns UTF-8 text in a growable buffer (`String` means `String`). It is a move type that needs drop, like `Vector`, but a distinct type. - **UTF-8 invariant:** text only enters a string through `append` of a `StringView`, 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] = b` is rejected (N0211), and `borrow mut s[i]` and mutable slices of a string (N0248). - **Creation:** `String.create()` (no allocation) and `String.withCapacity(n)`: `Result, AllocError>`; `String.create()` for another allocator. - **Members:** - `s.append(text)` (`text: StringView`): `Result`, two-phase like `push`. Growth is as for vectors. On failure the string is unchanged; - `s.appendNumber(n)` (M11, G4c; `n` of any integer type, or a reference to one): appends `n` in decimal, with a `-` before a negative value and no leading zeros or `+`: `0`, `-128`, `18446744073709551615`. It is `Result`, two-phase and unchanged on failure, as for `append`. 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 a `StringView`, a loan of `s`; a `String` argument for a `StringView` parameter is borrowed the same way; - read-only access as for a view: `s[i]` (`u8`), `s[lo..hi]` (a `StringView`, 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 of `s` when the call's mutable loan starts. - **Layout (v0):** as `Vector`. Diagnostics: none new. The errors reuse N0205, N0207, N0209, N0211, N0240–N0251, N0260 and N0263. ## 24. `SlotMap`, `Handle` and `StableVector` (M6e) ### 24.1 `SlotMap` and `Handle` ```nova struct Node { value: i32; next: Handle?; } fn main() { var nodes = SlotMap.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` owns values of `T` in reusable slots from allocator type `A` (`SlotMap` means `SlotMap`; N0263). `T` is any value type except `void` and 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` 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 by `insert`. - **Operations:** - `SlotMap.create()`: an empty map; it allocates nothing (type arguments as for vectors, §23.1); - `m.insert(v)`: `Result, AllocError>`, taking `borrow mut this` (two-phase, B5). If the storage cannot grow, `v` is destroyed and the result is `Err(AllocError.OutOfMemory)`; - `m.get(h)`: `borrow T?` (a loan of `m`), `null` if `h` does not name a live value; - `m.getMut(h)`: `borrow mut T?` (a mutable loan of `m`; needs a mutable map); - `m.remove(h)`: `T?`, the value moved out (or `null`), taking `borrow 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 `T` is memory-safe: it yields that map's value in that slot, or `null` (a logic error, as with any index). - **Invalidation:** references from `get`/`getMut` are loans of the map, so `insert` and `remove` are 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` is `{ u32 index; u32 generation }`. ### 24.2 `StableVector` ```nova fn main() { let names = StableVector.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` 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 for `Vector` (§23.1). - `sv.push(x)`: `Result` (the new element's index). It takes **`borrow 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 failure `x` is destroyed and the vector is unchanged. - `sv.length` (`usize`) and `sv[i]` (a bounds-checked place, as for vectors: read, `borrow`, `borrow mut` and assignment, the last two needing a mutable vector). - No `pop`, `clear`, views or `for` (the elements are not contiguous): iterate with `for (i in 0..sv.length)`. A `borrow mut sv[i]` is a mutable loan of `sv`, so `push` is rejected while one is live (N0241). - Not `Sync` (concurrency is later work). **Layout (v0):** `{ void *chunks[32]; u64 length }` (264 bytes); element `i` is in chunk `⌊log2(i + 8)⌋ − 3`. At most `8 · (2³² − 1)` elements; beyond that `push` fails with `OutOfMemory`. Diagnostics: none new. The errors reuse N0205, N0209, N0240–N0251, N0260 and N0263. ## 25. Region parameters (M6x; OD-3, ADR 0006) ```nova enum Kind { Word, Number } struct Token { kind: Kind; text: StringView; } struct Lexer { input: StringView; position: usize; } fn next(lexer: borrow mut Lexer) -> Token? { … } // the token views the input fn split(line: StringView) -> Result>, AllocError> { … } ``` ### 25.1 Declaring regions - Structs, enums and functions may declare **region parameters** before their type parameters: `struct Lexer`, `fn f(…)`. In a parameter list, `region` followed by a name declares a region; `region` alone is still an ordinary name. - A method may use its struct's regions. `static` is 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`, `Pair`. The built-in scoped types take one region optionally: `StringView`, `Slice`, `SliceMut`, and references `borrow(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` 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 of `T`, …), the instantiation is rejected where it is written: N0274. - **Collections:** `Vector`, `StableVector` and `SlotMap` elements may be scoped; so may the value passed to a region's `alloc`, 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`) 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` and `Token` 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 contributes `p`'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 `i`** iff they share a flow name: the result may carry `i`'s loans (this generalises v0's return sources, §13.5); - **parameter `i` ← parameter `j`**, for `i` of type `borrow mut T` whose `T` shares a flow name with `j`: the callee may store `j`'s loans into what `i` refers 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: 1. Writing a scoped value into a place of a local (a field, an element, a payload) adds its loans to that local. 2. 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 mut` parameter `p` of the current function, the value's loans must all come from parameters that flow into `p` (§25.6): otherwise N0272. 3. `push`, `insert` and `append` write their argument through their receiver (rule 2). 4. At a call, an argument that flows into a `borrow mut` parameter adds its loans to what that argument refers to. 5. In generic code, values whose type mentions a type parameter carry loans like scoped values. 6. Destroying a value that is scoped and needs drop is a use of its loans: its `drop` may read them, so they must still be live (N0243 otherwise). Diagnostics: N0270, N0272, N0273, N0274 (`diagnostics.md`). ## 26. Program arguments, files and sockets (C3) ```nova 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?`, argument `i` as a view of static text (no loans), or `null` if `i` is out of range or the argument's bytes are not valid UTF-8 (the `StringView` invariant, §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 - `File` owns 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` for reading; `File.create(path)`: for writing, created or truncated; `File.remove(path)`: `Result`. `path` is a `StringView`; a path that contains a zero byte or is longer than 4095 bytes is `InvalidInput`. - `f.read(mut buffer)` (`buffer: SliceMut`, `f` taken as `borrow mut this`): `Result`, the number of bytes read, `0` at the end of the file. Interrupted reads are retried. - `f.write(bytes)` (`Slice`): `Result`; it writes all the bytes or fails. - `Console.read(mut buffer)`: reads standard input like `f.read`. `Console.writeBytes(bytes)` writes raw bytes to standard output, in order with `Console.write`. - `Console.writeError(text)` and `Console.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`, `0` for any free port): `Result`, listening. `listener.port()`: `Result`, the port it is bound to. `listener.accept()`: `Result`. - `TcpStream.connect(address, port)`: `Result`. `s.read(mut buffer)` and `s.write(bytes)` as for files; `s.shutdownWrite()`: `Result`, 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 on `PATH`; `arguments: borrow Vector`, passed after the program name): `Result`. 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 is `InvalidInput`; 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. ```nova /// 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** to `T`. `T` is any value type, or `void` for an untyped pointer (`*void`, C's `void *`). It may not be a reference, slice, `StringView` or other scoped type (§19.1, §25.3), or `never` (N0281). Since M8d it may be a type parameter: an instantiation with a scoped type argument is then N0274, as for a box of `T`. - 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 T` coerces to `*T` wherever a `*T` is expected (like `SliceMut` to `Slice`). - `==` and `!=` compare two pointers of the same type (after that coercion) by address. Ordering comparisons are not defined on pointers (N0207); compare `p as usize` instead. ### 27.2 Making pointers (safe) Making and converting a pointer is safe; only using one is not (§27.3). - `r as *T` for a reference `r: borrow T` or `borrow mut T`, and `r as *mut T` for `r: borrow mut T`. `borrow x as *T` parses as `(borrow x) as *T` (grammar §2.1). The borrow ends at once: the pointer holds no loan, so later uses of `x` are not checked against it. - `s.ptr` for a slice `s`: `*T` for `Slice`, `*mut T` for `SliceMut`; for a `StringView`, `*u8`. It is the address of element 0 (of where it would be, for an empty slice). - `p as *U` and `p as *mut U` for any raw pointer `p`, whatever its pointee and mutability. - `p as usize` for any raw pointer, and `n as *T` / `n as *mut T` for `n: usize` (also `*void`). An integer literal converts too: `0 as *mut T` is the **null pointer** (there is no `null` pointer literal: `null` is 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()`: the `T` at `p`, moved out (copied, for a Copy `T`). Afterwards the memory holds no `T`: 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.md` U2). A type holding C state (§33) is N0282, since C state never moves. Since ADR 0027; before it, `T` had to be Copy. - `p.write(v)`: moves `v` to `p` (copies, for a Copy `T`) without reading or destroying what was there. `p` must be a `*mut T` (N0282). It returns `void`. For a type with `@placed` methods (§33.3) the store is a placement: they run at `p` afterwards, as for `Box.create`, and `v` may be the struct literal without its C state that `Box.create` takes (§33.2). - `p.readVolatile()` and `p.writeVolatile(v)` (§29.1) still need a Copy `T`. - `p.add(n)` and `p.sub(n)` (`n: usize`) and `p.offset(n)` (`n: isize`): the pointer `n` elements (`n × size(T)` bytes) after (`sub`: before) `p`, with the type of `p`. The arithmetic wraps modulo 2^64; it never panics. - `Ptr.toBorrow(p)`: a `borrow T` to the `T` at `p`; `Ptr.toBorrowMut(p)`: a `borrow mut T`, for `p: *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)`: a `Slice` of the `n` elements from `p`; `SliceMut.fromRaw(p, n)`: a `SliceMut`, for `p: *mut T`. Like `Ptr.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 is `e`'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. `main` may not be `unsafe` (N0219). Methods may be `unsafe`; since C5, interface methods may be too (§30.1), and an `impl` must match (N0267). - Everything lexically inside an unsafe block, an unsafe expression or the body of an `unsafe fn` is in an unsafe context, including deferred statements. A nested `unsafe` is allowed. - **The Safety rule** (U2): an `unsafe fn`, and every function whose body contains an `unsafe` block or expression, must carry a doc comment (grammar §1.2) with a line that starts with `Safety:`. It states the contract: what callers must guarantee (for an `unsafe 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()` and `p.write(v)` are a copy from, and an assignment to, the place `(*t)` of a temporary `t` holding `p`: the same `Deref` projection as references, through a pointer. `Ptr.toBorrow`, `Ptr.toBorrowMut` and the pointer casts are `Cast`s; the arithmetic, `isNull`, `.ptr` and `fromRaw` are 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 on `uintptr_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 calls `Ptr.toBorrow`/`toBorrowMut` or `fromRaw` keeps 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) ```nova 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(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 with `nova_`, `nv_`, `NV_`, or `nv` and 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 - `@export` makes a top-level, non-generic, non-method function callable from C, with the C types of §28.3 (N0291 otherwise). `main` cannot 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 `@repr` enum parameter that is not one of its variants panics (`invalid enum value from C`) before the body runs. - **Exported constants** (C9, ADR 0014). `@export` or `@export("symbol")` on a constant (§10) of integer or `bool` type, 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;` or `const CE symbol[N] = { … };`, declared in `nova_exports.h` as `extern 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 is `struct S` with members named as in Nova, of the types Nova uses inside its own C (`usize` is `uint64_t`, pointers have no `const`), so that the generated code and C code that includes `nova_exports.h` declare 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 build` accepts 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 its `main` in C instead of in Nova; without a Nova `main`, 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 `@export` functions or `@repr` types, novac also writes `nova_exports.h`: the `@repr` structs and enums, and a prototype for each export. C files include it as `#include "nova_exports.h"`. - `run --interp` runs 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) ```nova @repr(C) struct UartRegs { data: Volatile; status: Volatile; } struct Uart { regs: MmioPtr; /// 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.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` | 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` | The address of a memory-mapped `T` (typically a `@repr(C)` struct of `Volatile` registers). `unsafe MmioPtr.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.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 (`pause` on x86-64, `yield` on 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()` and `Target.isAArch64()`: `bool`, the architecture the generated C is compiled for. They are decided by the C compiler, not by `novac` (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) -> R` is the type of a pointer to a C function (grammar §2). Its parameter and result types must be FFI-safe (§28.3) and not `@repr` enums (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 `@export` function or a `@callback` function (§34.1) is a value of that type where such a type is expected (a typed `let`, 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(…) -> R` is the null pointer, `f.isNull()` tests for it, and `f as usize` is its address. - **Calls.** `f(args)`, with `f` a 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) ```nova 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, 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 least `layout.size` bytes aligned to `layout.align`, or `Err` (never a null `Ok`); - `unsafe fn deallocate(borrow this, p: *mut u8, layout: Layout)`: returns a block. Its contract: `p` came from `allocate` of this allocator type with the same layout, and is returned once. - Interface methods may be `unsafe` (lifting §27.4's earlier rule); an `impl` must match the interface's `unsafe` (N0267). - **`layoutOf()`** (ADR 0027): `T`'s size and alignment as a `Layout`, from `TargetInfo`. For a type holding C state (§33), whose size only the C compiler knows (§31.2), it is C's `sizeof`/`_Alignof` of `T`'s C type, so `novac run --interp` cannot evaluate it. It takes one type argument and no arguments (N0209), and is a static initialiser (§39.4). ADR 0027 named it `Layout.of()`; Nova has no type arguments on a member call, so it is a function, like `truncate`. ### 30.2 Allocator types - A **user-defined allocator** is a non-generic struct **with no fields** that implements `Allocator`. It may be the `A` of `Box`, and `Box.createIn(a, v)` takes a value of it. Having no fields, it has no state that a box would need to carry: a `Box` 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 `SystemHeap` and `FailingAllocator`. `Vector`, `String`, `SlotMap`, `StableVector`, `Arena` and `Pool` take 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 `Allocator` is N0263. ### 30.3 Semantics - `Box.createIn(a, v)` with a user allocator calls `allocate(Layout { size: size(T), align: align(T) })` once. On `Err`, it destroys `v` and returns `Err(AllocError.OutOfMemory)`; on `Ok(p)`, it moves `v` to `p`. Destroying the box destroys the contents, then calls `deallocate(p, the same layout)` once (H1). The compiler only calls `deallocate` with blocks `allocate` returned, so the contract holds for every safe program. - A `size(T)` of 0 is never requested: such boxes still allocate one byte (as `SystemHeap` does). - 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): `Box` with 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) ```nova @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's `ExternStruct`. - `@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 `-I` paths, 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 is `cname` instead, for a C typedef (`typedef struct {…} cname;`). The name, the `@ctype` name 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, `bool` or imported types, for C array members `CE name[N]`; otherwise N0291. An embedded member is checked with `_Generic` only, 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 (`u32` is `uint32_t`, `usize` is `size_t`, `*CChar` is `const char *`, `*mut T` is `T *`, `[u8; 16]` is `uint8_t[16]`). This is checked when the C is compiled (§31.3), not by novac. (A plain `char` array cannot be declared: `CChar` is 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). `@cname` anywhere else is N0290. - An imported struct has no methods, `drop` block, 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 `T` appears 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.f` is a place of the declared field type, as for any struct (§12): read it, assign it through `borrow mut`, or borrow it. The borrow checker treats it like any field place. A field of raw pointer type, `usize` or `isize` can 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 against `N`, `r.f[a..b]` is a slice (mutable through `borrow mut`), and when the element is an integer or `bool` the 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.toBorrowMut` make references from them (unsafe, §27.3). `add`, `sub` and `offset` (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`, and `Slice.fromRaw` / `SliceMut.fromRaw`. - **Across the boundary**, `*T` and `*mut T` are FFI-safe (§28.3): `struct name const *` and `struct name *` (or the `@ctype` name) in prototypes. The type itself is not, by value (N0291). ### 31.3 Lowering - The generated C includes every `@header`, after `nova/rt.h`, once each, in the order the blocks appear (files in command-line order); `nova_exports.h` includes 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 through `uintptr_t` when read and written, as at exports (c-backend.md §2.3), so that `const` in the header's member type causes no warning. - For every declared field `f` of type `F` (C type `CF`), the generated C asserts both `_Generic(((S *)0)->f, CF: 1, default: 0)` and `sizeof(((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 `_Generic` of the member itself would see a decayed pointer, and, for integer and `bool` elements, with the size of Nova's array. Its place is `(*(A *)(uintptr_t)&(*p).f)` (through `uintptr_t`, as `p` may be a `const` pointer), where `A` is 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) ```nova 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 allocate ``` ### 32.1 Effects - There are two effects: **`sleep`** (the code may block) and **`alloc`** (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, `drop` blocks, 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 each `e` either `sleep` or `alloc`, and `@effects()` for none: - on an `extern` function, 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). - An unknown effect name, or `@effects` twice, is N0290. ### 32.3 Contexts - `@context(atomic)` on a function forbids `sleep` in its body. `@context(irq)`, code that may run in a hardware interrupt, forbids `sleep` and `alloc` (`cosmoos/kernel-memory-and-lifetimes.md` KQ-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, `sleep` is 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) ```nova @effectsOf(test) fn count(values: Slice, 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, 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 holds `p` (copied, cast or reborrowed from it and assigned nothing else), is not one of the function's own effects. So is passing `p` as the argument for another function's `@effectsOf` parameter. Every other use is as before: `p` passed 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 `@effectsOf` parameters, 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 `@effectsOf` parameter, or a local that holds it | none: 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 `@effectsOf` function (§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 through `p` during a call runs the argument the caller passed, whose effects the caller counted. Anything made from `p` other than a copy or reborrow is counted by its type, where it is called. - lib/core's `sortBy`, `forEach`, `any`, `all` and `retain` (§36.4) are `@effectsOf` their closure, and so are `sortBy`'s helpers. ## 33. C state embedded by value (C7b; ADR 0009 §2) ```nova @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 { 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, KernelHeap>, AllocError> => Box.createIn(KernelHeap {}, SpinLock { name: name, data: 0 }); // `raw` is left out ``` ### 33.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.create` or `Box.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`, through `Ptr.toBorrow` of 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 - `@placed` on a method `fn 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.createIn` has placed a C-laid-out value in its box, and before it returns, the compiler calls the `@placed` methods 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 `@placed` method runs C's initialisers (`spinlock_init`, `mutex_init`, `kobject_init`) at the value's final address. - A box whose allocation fails runs no `@placed` method: the value is dropped, as in §18.2. - The effects of `Box.create`/`Box.createIn` (§32.1) include those of the `@placed` methods 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) ```nova @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` made by `KRef.create`, and this runs once. @callback unsafe fn release(obj: *mut kobject) { let object: Box, KernelHeap> = Box.fromRaw(obj as *mut KObject); } // dropping `object` drops the data and frees it // … this.ktype.release = release; in KObject's @placed method ``` ### 34.1 `@callback`: C addresses of Nova functions - `@callback` on a top-level function gives it a C address, as `@export` does (§29.5), without a symbol. Where an `extern "C" fn` type 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` inside generic code, `release` 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`: its `Safety:` line states what the C caller guarantees. `@callback` with `@export` is N0290. - Generated C gives each instance used as a value a `static` function 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 the `sleep` and `alloc` effects (§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 box `b: Box` 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) turns `p: *mut T` back into a box. The box type, and with it the allocator, comes from the expected type (`let b: Box = unsafe { Box.fromRaw(p) }`); without one it is N0205. Contract: `p` came from `Box.intoRaw` of 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) ```nova fn mean(values: Slice) -> f64 { var sum = 0.0; for (v in values) { sum += v; } return sum / round(values.length); } fn main() { let samples = [1.5, 2.25, 4.0]; let m = mean(samples[..]); // 2.5833… Console.writeLine(truncate(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 - `f32` and `f64` are IEEE 754 binary32 and binary64. Their layout comes from `TargetInfo` (size and alignment 4 and 8 on both supported targets). They are Copy, need no drop, hold no loans, and are FFI-safe (C `float` and `double`, §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 type `f32` or `f64`. Its value is the decimal value rounded to nearest (ties to even) in that type, once: `0.1f32` is the `f32` nearest 0.1, not the `f64` nearest 0.1 rounded again. - With a suffix, the type is the suffix's. Without one, the literal is *flexible* (§5): it takes an expected `f32` or `f64` (looking through an optional, as integer literals do) and otherwise defaults to `f64`. 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 (write `1.0`, or `round(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.0` is 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 `match` scrutinees 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 - **`as`** stays 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`, `u8` and `u16` as `f32` or `f64`; - `i32` and `u32` as `f64`. Any other `as` from or to a float type is N0208. - **`round(e)`**: `e` of any integer or float type, `T` a float type. The result is `e` rounded to nearest (ties to even) in `T`. It is always defined: an `f64` too large for `f32` becomes ±infinity, and NaN stays NaN. - **`truncate(e)`** with `e` of a float type and `T` an integer type rounds toward zero. It **panics** with "float out of range" if `e` is NaN or ±infinity, or if the rounded value does not fit in `T`. (`truncate` between integer types is unchanged, §7.) A float `T` is N0207: use `round`. - **`checkedTruncate(e)`**: as `truncate`, but the result is `T?`, `null` where `truncate` would panic. - **`saturatingTruncate(e)`**: as `truncate`, but a value below `T`'s range gives its minimum and one above gives its maximum. NaN gives 0. It never panics. - `checkedTruncate` and `saturatingTruncate` need a float `e` and an integer `T`, and `round` a float `T` (N0207 otherwise). All four take exactly one type argument (N0216). ### 35.5 Representation - `f.bits()` is the IEEE representation of `f`: a `u32` for `f32`, a `u64` for `f64`. `f32.fromBits(u)` (`u: u32`) and `f64.fromBits(u)` (`u: u64`) make a float from a representation; every pattern is a value, some of them NaNs. - The names `bits` and `fromBits` are reserved on float types, as the overflow methods are on integers (§6). ### 35.6 C and the interpreter - `f32` and `f64` are C `float` and `double`. 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.h` requires `FLT_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 generated `main.c` asks for this with `#define NV_FLOATS` when 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, and `FLT_EVAL_METHOD` is not 0. gcc ignores the standard pragma, but does not contract under `-std=c11`, which Nova's builds use. `novac` also passes `-ffp-contract=off` to 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: - `truncate` and its variants compare the value with the target's exact bounds before converting (C11 6.3.1.4); - `round` of an `f64` outside `f32`'s range gives the infinity directly, not through a C conversion (C11 6.3.1.5). - The interpreter computes in .NET's `float` and `double`, 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.writeLine` and `String.appendNumber` accept `f32` and `f64` (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 `f32` prints with `f32`'s shortest digits: `0.1f32` prints as `0.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`. - `NaN`, `Infinity` and `-Infinity`. Both zeros print as `0`, 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 gives `4.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's `repr` and Schubfach give `2.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`, `min` and `max` are exact portable C over integers and bits in `rt.h`. On baseline x86-64 there is no instruction for `floor` or `ceil`; `roundsd` needs SSE4.1. - `sqrt` is the hardware square root, which is correctly rounded on both targets. It is reached through `__builtin_elementwise_sqrt` with clang and `__builtin_sqrt` with gcc, built with `-fno-math-errno` (novac passes it), so it never sets `errno` or calls `sqrt`. MSVC uses its C runtime's `sqrt`. - The interpreter uses .NET's `Math`/`MathF` functions of the same names, which follow the same IEEE operations. - The eight names `bits`, `fromBits`, `sqrt`, `abs`, `floor`, `ceil`, `min` and `max` are 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-float` and `novac check --no-float` reject every float type, float literal and `f32.fromBits`/ `f64.fromBits` with 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.py` to `c8.py`), and `check-fpregs.sh` remains the backstop. Floats in the generic freestanding profile are otherwise allowed (`cosmoos/profiles.md` §1). - In the corpus, a line `// option: no-float` checks 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) ```nova 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; // a generic function, named with its type arguments Console.writeLine(id(7)); } fn identity(x: T) -> T => x; ``` - **The type** `fn(T1, …, Tn) -> R` (`fn(T1, …, Tn)` when `R` is `void`) 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 stay `extern "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`), 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): - `extern` and `unsafe` functions, 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`. - **Calls.** `f(args)`, with `f` a 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), else `sleep` and `alloc` (§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) ```nova fn countIf(values: Slice, keep: borrow fn(i32) -> bool) -> i32 { var n = 0; for (v in values) { if (keep(v)) { n += 1; } } return n; } fn forEach(values: Slice, 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) => expression` or `(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 `void` for 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, `this` included, 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 a `borrow mut this` method 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. - **Types.** - A literal that captures nothing is a plain function value, `fn(…) -> R` (§36.1). - One that captures is a **closure**: `borrow fn(…) -> R` if every capture is shared, `borrow mut fn(…) -> R` if any is mutable. - A closure type is a reference (§13): `borrow fn` is Copy, and `borrow mut fn` is unique and reborrowed at each call. Both are scoped, and allowed where references are (§13.1, §25): parameters, `let` locals, 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. - **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 fn` converts to a `borrow mut fn`: calling a shared closure changes nothing. - A `borrow mut fn` where a `borrow fn` is 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 plain `fn` is expected. - **Calls** `f(args)` are as for function values (§36.1): safe, with the effects of the type's effect list (§36.3), else `sleep` and `alloc`. - **In the body**, `return` returns from the closure. `break` and `continue` cannot 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 a `static` function taking `const void *env` first. A call is `(*f)(f, args…)`. ### 36.3 Effect lists on function and closure types (G8b′; ADR 0012 §3.7) ```nova 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, 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) { 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 before `fn` in a function type, with each `e` `sleep` or `alloc` as in §32.2: `@effects() fn(i32) -> bool`, and for closures `borrow @effects() fn(i32)` and `borrow mut @effects(alloc) fn(i32)`. No other attribute is allowed there (N0290), and only `fn` may 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)` and `fn(i32)` are different types. A type without a list may have both effects, so `fn(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 `sleep` and `alloc`. - **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 `@effects` is 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 to `fn(i32)`, and `borrow @effects() fn(i32)` to `borrow 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 a `fn(i32) -> bool`, which cannot be passed where an `@effects() fn(i32) -> bool` is 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) ```nova 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.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`, or a vector (lending its elements mutably) | `borrow mut fn(borrow T, borrow T) -> i32` | `void` | | `s.forEach(visit)` | `Slice` (a `SliceMut` 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` (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)`. - **`sortBy`** orders the elements so that `cmp(borrow s[i], borrow s[i + 1]) <= 0` for each `i`, when `cmp` is 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 `cmp` O(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 `cmp` is 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. - **`forEach`** calls `visit` on each element in index order. **`any`** calls `test` in index order until it returns `true` (then `true`; `false` for an empty slice), and **`all`** until it returns `false` (then `false`; `true` for an empty slice). - **`retain`** calls `keep` once on each element in index order. It keeps the elements for which it returned `true`, 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`, `swap` and `retain`), so the closure cannot capture it: `v.retain((x) => x < v.length)` is N0241, as for any other conflicting loan. - **Effects.** The methods are `@effectsOf` their closure (§32.5): a call has the closure's effects, plus `retain`'s destruction of the elements it removes. So `s.sortBy((a, b) => a - b)` may be used in `@context(atomic)` and `@context(irq)` code, and `s.sortBy(compare)` may too if `compare` neither sleeps nor allocates. - **Implementation.** The methods are Nova code in `lib/core/slices.nova`: static functions of the generic structs `SliceAlgorithms` and `VectorAlgorithms`, which the method calls name (so `SliceAlgorithms.sortBy(s, cmp)` is the same call). They use only safe Nova and `swap` (§16.5). ### 36.5 Owning closures: `move` literals and `dyn fn` (M13 G3; ADR 0015 §3.6) ```nova fn adder(n: u64) -> Box u64> { return Box.create(move (x: u64) => x + n)!; // n is moved (here, copied) into the closure } fn counter() -> Box 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.create(); names.push(String.create())!; let size: Box usize> = Box.create(move () => names.length)!; // a closure owning the vector: Box usize> Console.writeLine(size()); // 1 } ``` - **Types.** `dyn fn(T…) -> R` and `dyn mut fn(T…) -> R` (with an optional effect list, `dyn @effects() fn(…)`) are `dyn` types (§37): unsized, so they appear only behind a reference or in a box. `Box 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 written `borrow dyn fn`. - **Move literals.** `move (x: T, …) => body` is 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`, `break` and `?` are those of §36.2. - **Where.** A move literal may only be the value argument of `Box.create(…)` or `Box.createIn(a, …)` whose expected result is a `Box` or a `Box`. 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 a `dyn fn` is 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)` where `f` is a place of type `Box`, `borrow dyn fn…`, or a reference to such a box, calls the closure. A `dyn mut fn` needs mutable access: a `var` box, or a `borrow mut`, else N0248. A call has the effects of the type's effect list, else `sleep` and `alloc` (§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 `dyn` pair (§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) ```nova 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>) -> u64 { var t: u64 = 0; for (s in shapes) { t += s.area(); } // a call through the table return t; } fn main() { var shapes = Vector>.create(); shapes.push(Box.create(Square { side: 3 })!)!; // Box coerces to Box 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`), **`dyn I`** is the type of "a value of some type that implements `I`". Its size is not known, so, like `[T]` (§16), it is **unsized**: it appears only behind a pointer. - `borrow dyn I` and `borrow mut dyn I` are references (§13). They are scoped, follow every reference rule, and `borrow dyn I` is Copy. - `Box` (and `Box` for `SystemHeap`) owns its value (§18.2). It is a move type that needs drop. - Anywhere else, `dyn I` is **N0330**: a local, parameter or result of type `dyn I`, a field, an array or vector element, an optional, a type argument (`Vector`; `Vector>` is fine), or an element of a slice type. - **Dyn-compatible interfaces.** `dyn I` is allowed only if every method of `I` takes `borrow this` or `borrow mut this`. A method that takes `this` by value makes the interface incompatible: `dyn I` is then **N0331**, which names that method. A generic interface is checked once its type arguments are given. - `dyn` is a keyword in types only (grammar.md §2). `dyn I` binds tighter than `borrow`, so `borrow dyn I` is a reference to a `dyn I`. ### 37.2 Coercions - A value converts to a `dyn` type only where that type is **expected**: an annotated `let`, an argument, a field initialiser, a `return`, or an argument of a collection method such as `push`. Inference never invents a `dyn` type. - `borrow T` → `borrow dyn I`, and `borrow mut T` → `borrow mut dyn I` (or `borrow dyn I`); - `Box` → `Box`: a move of the box, with no new allocation. The allocator type must be the same. - `T` must implement `I` (§21.1), else **N0268**, as for an unsatisfied bound. - `T` is never scoped (§19.1): no references, closures, views, slices or region parameters, since only an unscoped type can implement an interface (§21.1). So a `dyn` value carries no loans of its own: `borrow dyn I` carries only the loan of the reference it came from, and `Box` carries none. (Scoped `dyn(r) I` values are OD-10a.) - `T` must be a sized type: a struct, an enum, or a built-in type such as `i32`. A `dyn I` does not coerce to another `dyn` type, and there is no conversion back to `T` (no downcasting; OD-10b). ### 37.3 Calls - `x.m(args)`, where `x` is a `borrow dyn I`, a `borrow mut dyn I`, a `Box`, or a place of one of these types, calls the method `m` of `I` through the value's table. The method is that of `T`'s `impl I for T`. - The receiver rules are those of a reference to `T` (§13, §21.1): - a `borrow this` method may be called through any of the three; - a `borrow mut this` method needs mutable access: a `borrow mut dyn I`, or a mutable place of `Box`. Through a `borrow dyn I` it is N0248, as for any value behind a shared reference. - Only `I`'s methods can be called. `T`'s other methods and fields are not visible through `dyn I` (N0234). - **Effects** (§32). An interface method may carry an effect list, which bounds every `impl` of it: ```nova interface Visitor { @effects() fn visit(borrow mut this, value: u64); } ``` An `impl` method whose effects exceed its interface's list is N0295, reported at the `impl` method. A call through `dyn I` has the effects of `I`'s list, or `sleep` and `alloc` without one, as for a closure without a list (§36.3). A call on a generic `T: I` is unchanged: it has the effects of the instantiated method (§21.2). - **Drop.** Dropping a `Box` destroys its value with `T`'s drop glue, then frees it with `A`. ### 37.4 Representation (c-backend.md §2) - A `borrow dyn I`, a `borrow mut dyn I` and a `Box` with a zero-sized `A` are each two words: `{ data, vt }`. `data` points at the value, and `vt` points at the table for (`T`, `I`). - The table is a `static const` struct per pair (`T`, `I`) that a coercion names. It holds the drop glue, the value's size and alignment (for a user allocator's `deallocate`), then one function per method of `I`, in declaration order. Each function is a generated adapter taking `void *` as its receiver, so no function is called through a pointer of another type. - `dyn I` values have no address identity beyond `data`. Two `borrow dyn I` values 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) ```nova struct Counter { n: u64; } fn main() { let counter = Arc>.create(Mutex.create(Counter { n: 0 })!)!; var workers = Vector>.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>.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): ``, ``. - **`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 a `Send` element; - `Box`, `Vector`, `String`, `SlotMap`, `StableVector` and `Handle`, with `T: Send` and the system heap (or `FailingAllocator`); - `File`, `TcpListener`, `TcpStream`; - `Box` and `Box R + Send>` (§38.4); - the library's thread-safe types (§38.3): `Arc`, `Mutex`, the atomics. - It fails for: - references, slices, views and every other scoped type (§19.1); - closures (`borrow fn`), function values aside (`fn(…)` is `Send`: a code address); - raw pointers and imported C types; - a box or collection with a user allocator; - regions and pools; - `Thread` (§38.2); - `Box` and `Box` without `+ Send`. - **`Share`**: shared references to a value may be used from several threads at once. It holds for every `Send` type, since no v1 type can be changed through a shared reference except the library's, and those are `Share` by design. The exception is `Thread`. - Inference is structural and recursive. A recursive type is `Send` unless a path through it reaches a type that is not. Generic code knows only its bounds: a `T` is `Send` only under a `T: Send` bound. ### 38.2 Spawning and joining - **`Thread.spawn(move () => body)`** starts a thread that runs the move literal's body (§36.5) and returns `Result, SpawnError>`, where `T` is the body's result type. `SpawnError` is the enum `SpawnError.Resources` (the thread or its closure could not be allocated). - The literal takes no parameters. Its captures must be `Send` (**N0334**), and so must `T` (**N0334**). - `Thread.spawn` also takes a `Box T + Send>`. - A move literal is allowed here as well as in `Box.create` (amending §36.5). - **`Thread`** is a move type that needs drop. `t.join()` consumes it, waits for the thread to finish, and returns its `T`. Dropping a `Thread` that was not joined waits for it and destroys its result. There are no detached threads: every thread a program starts has ended before `main` returns. - A panic in any thread aborts the process (OD-5). - **Effects** (§32): `spawn` has `sleep` and `alloc`. `join`, and dropping a `Thread`, have `sleep`. ### 38.3 Shared state (library types: `lib/std/sync.nova`, and the atomics in `lib/core/atomics.nova`) - **`Arc`**: shared ownership. - `Arc.create(v)` returns `Result, AllocError>`. `a.clone()` returns another owner of the same value, and `a.get()` returns a `borrow T` (shared only). - The count is atomic. The last owner's drop destroys the value and frees it. - **`Mutex`**: mutual exclusion. - `Mutex.create(v)` returns `Result, AllocError>`; the platform lock is allocated. - `m.lock()` waits for the lock and returns a `MutexGuard`, scoped to `m` like C7's guards. Its `data()` lends `borrow mut T`, and its drop unlocks. - `lock` has the `sleep` effect. - **Atomics**: `AtomicU32`, `AtomicU64`, `AtomicUSize` and `AtomicBool`, 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)`, then `load()`, `store(v)`, `swap(v)` and `compareExchange(expected, new)` (returns `Result`, `Err` with the current value). - The integer types also have `fetchAdd(v)` and `fetchSub(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)`, and `swapWith`, `compareExchangeWith`, `fetchAddWith`, `fetchSubWith` taking `o: Ordering` (`AtomicBool` has the first four). The enums are `LoadOrdering { Relaxed, Acquire, SequentiallyConsistent }`, `StoreOrdering { Relaxed, Release, SequentiallyConsistent }` and `Ordering { 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: `AcquireRelease` as `Acquire`, `Release` as `Relaxed`. - 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 `Send` and `Share` by the library's declaration (`@threadSafe`, accepted only in the library's own files). Their `unsafe` internals are argued in `ownership.md` T1–T3. ### 38.4 `dyn … + Send` - `dyn I + Send` and `dyn fn(…) -> R + Send` (and with `mut`) are `dyn` types (§37, §36.5) whose boxes are `Send`. A coercion to one needs `T: Send`, which for a move literal means `Send` captures (N0334). A `dyn … + Send` converts to the same `dyn` type without `+ Send` (the same value: they share their C representation and tables), never the other way. `Thread.spawn(f)` with `f: Box T + Send>` runs a closure made elsewhere. ### 38.5 Profiles - Threads, `Arc` and `Mutex` are in the **hosted profile**: hosts with pthreads or Win32 threads, and CosmoOS userland over `cosmo_thread_*` and `cosmo_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 in `lib/core`, in every profile (ADR 0026). - `lib/std/` is the hosted library: compiled into hosted packages only. Like `lib/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)`** runs `a` on a new thread and `b` on the calling thread, waits until both have finished, and returns `void`. It is in the hosted profile, with `Thread` (§38.5). - Each argument is a closure literal with no parameters and a `void` result (§36.2), or a function value of type `fn()` (§36.1). A closure that is not a literal (a variable or a parameter of type `borrow 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); });` - **Captures.** `a` runs on another thread while `b` runs 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 **`Share`** type, where, for this rule only, `borrow T`, `Slice` and `StringView` are `Share` when `T` is; - a capture used **mutably** needs a **`Send`** type, where, for this rule only, `borrow mut T` and `SliceMut` are `Send` when `T` is; - a captured closure is refused; - otherwise **N0334**, naming the capture and its type. These extended rules apply here only: outside `Thread.both` references are neither `Send` nor `Share` (§38.1). - **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 `both` returns. - **No failure.** If the new thread cannot be started (its runtime record cannot be allocated, or the platform refuses), `both` runs `a` and then `b` on the calling thread. The result is the same either way, so `both` returns `void` and a program cannot tell whether `a` ran in parallel. - A panic in either closure aborts the process (OD-5). - **Effects** (§32): `sleep` and `alloc`. 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) ```nova 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;` and `static mut NAME: T = e;` at package level, with an optional visibility, like `const` (§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 `T` with a `drop` is allowed, and its drop never runs. ### 39.2 `static` - `T` must be `Share` (§38.1), because every thread may use a static (N0337). - `NAME` in an expression denotes the place `*S`, where `S` is a reference to the static of type `borrow(static) T`. `static` is 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 `T` is `Copy`, borrowed (`borrow NAME`, `borrow NAME.field`), and its methods taking `borrow this` called; - 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 `static` region. It may be returned from a function whose return type names `static` (`borrow(static) T`, §13.5), stored, and sent to another thread. - Mutation goes through `T`'s own synchronisation: an atomic's operations, a lock's guard. ### 39.3 `static mut` - `T` need not be `Share`. - `NAME` denotes the place `*S` with `S: 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.md` U3). ### 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, `bool` or 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 `@constant` constructors (below); - an array literal or repeat (`[e; N]`) of static initialisers; - a call of a `@constant` function whose arguments are static initialisers; - a call of a C constant macro whose arguments are static initialisers or `here` (below); - `layoutOf()` (§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 = Mutex
.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 `@constant` function'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_` (with the package's name for a dependency's, as in §2.1 of `c-backend.md`), `static` unless `--split` places it in `nova_instances.c` and renames it (c-backend §2.4). It is not `const` in 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 a `StringView`. `here` is `(&nvS_.…)`, an address constant (C11 §6.6p9), and `layoutOf()` of a C-laid-out `T` is `{ .f_size = sizeof(…), .f_align = _Alignof(…) }`. - `S` is `&nvS_`. - The interpreter evaluates every initialiser before `main` into 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). # Diagnostics Status: M1 increment 1a. Lists every diagnostic code the compiler can emit. Codes are stable: a retired code is never reused. Source of truth in code: `DiagnosticCodes` (`src/Nova.Compiler/Diagnostics/Diagnostic.cs`). ## Format ```text ::: error[N0100]: expected ';', found 'let' | 2 | let x = 1 | ^ ``` - `` is the path as given on the command line (never made absolute), so output is reproducible. - Line and column are 1-based. Columns count UTF-16 code units (grammar.md §1.1). - Diagnostics are emitted in the order they are found. The lexer finishes before the parser starts, so all lexical errors of a file come before its syntax errors. - `--diagnostics-format=json` gives the same diagnostics as one JSON document, with fixes, and `novac explain ` prints a code's row (M12; `tooling.md` §5). ## Recovery policy The parser reports **one** diagnostic per broken statement or item, then re-synchronises (grammar.md §3). The parse-fail corpus enforces this: each test lists exactly the expected diagnostics, so any cascade fails the test. ## Codes ### Lexer (N0001–N0099) | Code | Meaning | Example | |---|---|---| | N0001 | Unexpected character (including non-ASCII outside literals and comments) | `let a = 1 # 2;` | | N0002 | *(unassigned)* | | | N0003 | Unterminated block comment | `/* ...` at EOF | | N0004 | Integer literal does not fit in 64 bits | `18446744073709551616` | | N0005 | Unterminated string literal (newline or EOF before `"`) | `"abc` | | N0006 | Invalid escape sequence, or `\u{...}` that is not a Unicode scalar value | `"\q"`, `"\u{D800}"` | | N0007 | Invalid char literal: empty, more than one character, or unterminated | `''`, `'ab'` | | N0008 | Invalid digit for the radix, or no digits after a radix prefix | `0b102`, `0x` | | N0009 | Unknown literal suffix | `12abc`, `1f32` (float suffix needs a float literal) | | N0010 | Misplaced digit separator `_` (must sit between two digits) | `1__0`, `0x_FF`, `1_.5` | ### Parser (N0100–N0199) | Code | Meaning | Example | |---|---|---| | N0100 | Expected a specific token | missing `;`, `(` after `if`, `}` before `fn` | | N0101 | Expected an item (`fn` or `const`) at the top level | `let x = 1;`, `struct S {}` (not in M1) | | N0102 | Expected an expression | `let a = ;` | | N0103 | Comparison operators chained | `a < b < c` | | N0104 | Invalid assignment target | `1 = 2;`, `f() = 3;` | | N0105 | Expected a type | `fn f(x: ) {}` | | N0106 | Namespace declaration not first in the file | `fn a() {}` then `namespace X;` | | N0107 | Expected a field or `fn` inside a struct body | `struct A { 42; }` | | N0108 | Expected a `match` pattern (`_`, a literal, or `E.V(…)`) | `match (n) { + => { } }` | | N0109 | An `import` declaration after an item | `fn a() {}` then `import geometry;` | ### Names, types and flow (N0200–N0299) Semantic checks run only when every file parsed without errors. | Code | Meaning | Example | |---|---|---| | N0200 | Name not found (variable, function, or built-in member) | `missing`, `Console.print(1)` | | N0201 | Item defined twice in the package | two `fn twice()` | | N0202 | Parameter declared twice | `fn f(a: i32, a: i32)` | | N0203 | Local redeclares a local or parameter in scope (no shadowing of locals) | `let x = 1; let x = 2;` | | N0204 | Unknown type name | `fn f(a: int)` | | N0205 | Type mismatch against the expected type | `let a: bool = 1;`, `if (n) {}` | | N0206 | Integer literal out of range for its type | `256u8`, `let c: i8 = 128;` | | N0207 | Operator not applicable to the operand types (includes mixed types, signed bitwise ops, negating unsigned values) | `a + b` with `u8`/`i32`, `s & s` with `i32` | | N0208 | `as` conversion that can lose information | `a as u32` with `a: i32` | | N0209 | Wrong number of arguments | `two(1)` | | N0210 | Not callable, or a function or built-in used as a value | `local()`, `let f = two;` | | N0211 | Assignment to an immutable binding (`let`, parameter, constant, function) | `param = 3;` | | N0212 | Local read before it is definitely assigned | `var x: i32; return x;` | | N0213 | End of a non-`void` function is reachable | `fn f() -> i32 { }` | | N0214 | `break`/`continue` outside a loop | `break;` at function level | | N0215 | `return` with or without a value contradicts the return type | `return 1;` in a `void` function | | N0216 | Construct not supported in M1 (string values, member access, qualified types, generic arguments other than `truncate`; floats and `char` until M11) | `truncate(x)` | | N0217 | Not a constant expression, or constant evaluation would panic | `const C: u8 = 200 + 100;` | | N0218 | Constant depends on itself | `const A: i32 = A + 1;` | | N0219 | Invalid `main` signature | `fn main(args: i32)` | | N0220 | `let` without initializer | `let a: i32;` | | N0221 | `var` without type annotation or initializer | `var c;` | | N0222, N0223 | *(unassigned)* | | | N0224 | Value of type `void` used as a value | `let d = nothing();` | | N0225 | Expression statement that is not a call | `1 + 2;` | | N0226 | `void` used other than as a return type | `var e: void;` | | N0227 | Struct contains itself by value (infinite size); reported at the field that closes the cycle | `struct Node { next: Node; }` | | N0228 | Duplicate member (field or function) in a struct | `struct D { a: i32; a: i64; }` | | N0229 | Struct literal does not initialise every field | `Point { x: 1 }` | | N0230 | Unknown field (in a literal or an access), or a function named where a field was expected | `p.z`, `p.sum` | | N0231 | Field initialised twice in a literal | `Point { x: 1, x: 2, y: 3 }` | | N0232 | `this` outside a method, or a `this` parameter outside a struct | `fn f() -> i32 => this;` | | N0233 | Method called as a static function, or a static function called on a value | `Point.sum()`, `p.origin()` | | N0234 | Field access, method call or literal on something that is not a struct | `n.x` with `n: i32` | | N0235 | A block branch of an `if` or `match` expression can complete, so it has no value (type-system.md §15.3) | `let a = if (c) { f(); } else 2;` | | N0236 | A closure whose type cannot be inferred: a parameter without a type, or a block body returning a value, where no function or closure type is expected (type-system.md §36.2) | `let f = (x) => x + 1;` | ### References and borrows (N0240–N0249; `lifetimes.md`) N0240–N0245 come from the NIR borrow checker. It runs only on programs that type-check, and its errors carry notes ("borrowed here", "the borrow is used later here"). | Code | Meaning | Example | |---|---|---| | N0240 | Assignment to a place while it is borrowed (shared or mutable) | `let r = borrow x; x = 2; use(r)` | | N0241 | Borrow conflicting with a live borrow | two live `borrow mut x`, or `borrow mut x` while `borrow x` is live | | N0242 | Read of a place while it is mutably borrowed | `let m = borrow mut x; print(x); m = 1;` | | N0243 | A place or temporary goes out of scope while borrowed | `let r = same(five()); use(r)` | | N0244 | Returning a reference to a local or temporary (E1) | `return borrow x;` with `x` local | | N0245 | Return-source errors (E2): missing or invalid `borrow(...)` list, or a result derived from an unlisted parameter | `fn f(a: borrow T, b: borrow T) -> borrow T` | | N0246 | Reference in a disallowed position: struct field, constant, `var` local, reference to reference, source list outside a return type. Since M4a this covers every scoped type, scoped results included (`type-system.md` §19.1) | `struct S { r: borrow i64; }` | | N0247 | `borrow` of something that is not a place | `borrow 5`, `take(mut five())` | | N0248 | `borrow mut` (or a `borrow mut this` call) on an immutable place | `borrow mut param`, `shared.grow()` | | N0249 | `mut` argument marker missing or misplaced | `take(x)` for `borrow mut`, `look(mut x)` for `borrow` | ### Moves and destruction (N0250–N0259; `type-system.md` §14) N0212 and N0250–N0253 come from the NIR move checker (since M2c), which also runs only on programs that type-check. A move while borrowed is reported by the borrow checker as N0240. | Code | Meaning | Example | |---|---|---| | N0250 | Use of a value that was moved (on at least one path), including reading a partially moved struct as a whole | `consume(r); consume(r);` | | N0251 | Move out of a place reached through a reference | `let a = r.a;` with `r: borrow Pair` and `Pair.a` a move type | | N0252 | Move of a field out of a value whose type has a `drop` block | `consume(o.res)` with `struct Owner { res: Res; drop {} }` | | N0253 | Move out of a `using` binding | `using u = make(); consume(u);` | | N0254 | `return`, `break`, `continue` or `defer` inside a deferred statement | `defer return 1;` | | N0256 | `match` that does not cover every value (add the missing variants or `_`) | `match (s) { Shape.Empty => { } }` | | N0257 | Unreachable `match` arm: a duplicate pattern, or any arm after `_` | `1 => { } 1 => { }` | | N0258 | *Retired in M3c:* a non-Copy payload bound from a value scrutinee now moves out, and the match consumes the scrutinee (`type-system.md` §18.4) | — | | N0259 | Invalid array (`type-system.md` §16): a length that is not an integer literal from 1 to 2^48 − 1, an element type that is not Copy, or an array larger than 2^31 − 1 bytes; since M9b also `copyFrom` or `fill` on a slice whose element type is not Copy (§16.5) | `[Res; 2]`, `[i32; 0]` | Arrays and slices otherwise reuse codes: indexing or slicing a non-array, a non-`usize` index, or a bad `for` sequence or range is N0205; writing an element of a `let` array or through a `Slice` is N0211; a slice in a field, a `var` local or an array element is N0246; `borrow mut a[..]` of an immutable array is N0248; slicing a non-place is N0247. Since M9b, `copyFrom` or `fill` on a `Slice` is N0211, on a slice of an immutable array N0248, and a source that is not a slice of the element type N0205. Slices are references, so the borrow checker reports their conflicts as N0240–N0245. ### Region parameters (N0270–N0279; `type-system.md` §25, M6x) | Code | Meaning | Example | |---|---|---| | N0270 | A scoped field or payload in a struct or enum without region parameters | `struct S { v: StringView; }` | | N0272 | Storing a value through a `borrow mut` parameter whose loans the signature does not let flow into it | `fn put(v: borrow mut Vector, s: StringView) { v.push(s)!; }` | | N0273 | An unknown region name, region arguments on a type without region parameters, or the wrong number of them | `StringView`, `Plain` | | N0274 | A generic used with a scoped type argument where it needs an unscoped one (it boxes `T`, makes an array of `T`, …) | `boxed(s)` with `fn boxed(v: T) -> Box` | N0271 is unassigned: destroying a value that holds loans while their referents are gone reports N0243. ### Unsafe and raw pointers (N0280–N0289; `type-system.md` §27, `unsafe.md`, M8a) | Code | Meaning | Example | |---|---|---| | N0280 | An unsafe operation outside an unsafe context: a read, write or arithmetic through a raw pointer, `Ptr.toBorrow`/`toBorrowMut`, `Slice.fromRaw`/`SliceMut.fromRaw`, a call to an `unsafe fn`, or a use of a `static mut` (U1, type-system.md §39.3) | `let v = p.read();` in a safe function | | N0281 | An invalid raw-pointer pointee: a reference, slice, `StringView` or other scoped type, or `never` (a type parameter is allowed since M8d; a scoped argument for it is N0274) | `*borrow i32`, `*Slice` | | N0282 | A pointer operation that the pointer or its pointee does not allow: reading or writing a non-Copy type, writing through a `*T`, `toBorrowMut` or `SliceMut.fromRaw` from a `*T`, any access through `*void` | `p.write(1)` with `p: *i32`; `q.read()` with `q: *void` | | N0283 | An `unsafe fn`, or a function containing `unsafe`, without a doc comment with a `Safety:` line (U2) | `unsafe fn f() {}` with no `/// Safety: …` above it | ### C interoperability and profiles (N0290–N0299; `type-system.md` §28, M8b; `cosmoos/profiles.md` §4, M8c) | Code | Meaning | Example | |---|---|---| | N0290 | An unknown attribute, an attribute in the wrong place or with wrong arguments, a repeated attribute, an ABI other than `"C"`, or an extern declaration with a body, receiver or type parameters; `@export` on `main` | `@inline fn f() {}`, `@repr(C) enum E { A }`, `extern "Rust" { … }` | | N0291 | A type that cannot cross the C boundary in an extern declaration, an export, or a `@repr(C)` struct's field | `fn f(s: Slice);` in `extern "C"`, `@export fn g() -> Box` | | N0292 | A C name that is not a valid C identifier, is a C keyword (or `bool`, `true`, `false`, `NULL`, `main`), is reserved by C, or starts with `nova_`, `nv_`, `NV_` or `nv` and an uppercase letter | `@export("int")`, `fn nova_alloc();` in `extern "C"`, `@repr(C) struct S { default: i32; }` | | N0293 | A hosted-only name (`Console`, `Process`, `File`, the heap allocators and every collection or box that needs them, …) or a `main` function in a package built with `--profile freestanding` (`cosmoos/profiles.md` §4.1) | `Console.writeLine(1);`, `let b = Box.create(5)!;` | | N0294 | An imported C type (type-system.md §31), or a C-laid-out type (§33), used where Nova would need its size or would move it: as a local, parameter, result, field, element, optional or type argument, in a struct literal, or with `read`, `write`, `add`, `sub`, `offset` or `fromRaw` through a pointer; or a borrow of a pointer, `usize` or `isize` field of one | `fn f(i: cosmoboot_info) {}`, `p.add(1)` with `p: *cosmoboot_info` | | N0295 | An operation whose effect (`sleep`, `alloc`) the context forbids: in an `@context(atomic)` or `@context(irq)` function, while an `@context(atomic)` value (a spin-lock guard) is live, or beyond a Nova function's `@effects` bound (type-system.md §32); a closure body beyond its type's effect list, or a named function with effects its expected function type's list lacks (§36.3) | `mutex_lock(m)` while a `SpinGuard` is live | | N0296 | A `private` field named outside the source file that declares its struct: read, assigned, borrowed or given in a literal (type-system.md §12) | `lock.data` outside `sync.nova` | | N0297 | A float type, literal or `fromBits` in a build with `--no-float` (type-system.md §35.9) | `let x = 1.5;` under `novac build --no-float` | | N0298 | A function that cannot be a function value: an `extern` or `unsafe` function, one with region parameters, or `main` (type-system.md §36.1) | `let f = puts;` with `puts` declared in an `extern "C"` block | | N0299 | A closure of the wrong kind: a `borrow mut fn` where a `borrow fn` is expected, or a capturing closure where a plain `fn` is expected (type-system.md §36.2); a function or closure value whose type's effects are not in the expected type's effect list (§36.3) | `let f: fn(i32) -> i32 = (x) => x + k;` | Calling an extern function outside an unsafe context is N0280; two extern declarations or exports with one C name are N0201; `CChar` as a value type is N0281. Raw pointers reuse codes: an invalid pointer cast is N0208, ordering comparisons on pointers are N0207, a constant of pointer type is N0216, and an `unsafe main` is N0219. (An `unsafe` interface method was N0216 until C5.) ### Results and the heap (N0260–N0269; `type-system.md` §17–§18) | Code | Meaning | Example | |---|---|---| | N0260 | `Ok(...)` / `Err(...)` (or, since M4c, `Pool.create(...)`; since M6c, `null`) without an expected type that fixes its type arguments, or where another type is expected | `let a = Ok(1);`, `let p = Pool.create(SystemHeap {}, 4u32);`, `let n = null;` | | N0261 | `?` in a function that does not return a `Result` with the same error type; `?` on an optional in a function that does not return an optional (M6c) | `?` on `Result` in a function returning `Result` | | N0262 | `?` or `!` on something that is not a `Result` or optional value (a reference to one included) | `n?` with `n: i32` | | N0263 | An allocator argument that is not an allocator type (`SystemHeap`, `FailingAllocator`, or for `Box` since C5 a struct without fields implementing `Allocator`) | `Box`, `Box.createIn(point, 1)` | | N0265 | Type arguments that cannot be inferred: a generic call, struct literal or enum value whose arguments, fields or expected type do not determine every type parameter | `let d = Maybe.Nothing;`, `let e = make();` | | N0266 | Generic instantiation nested more than 64 deep (polymorphic recursion) | `fn deep(x: Maybe) { deep(Maybe.Just(x)); }` | | N0267 | An `impl` that does not match its interface (receiver, parameter or return type, a missing or extra method), a duplicate `impl`, an `impl` for a generic or scoped type, an interface method without a receiver, or a method call that two bounds both provide | `impl Shape for Square { fn area(this) -> i64 … }` | | N0268 | A type argument that does not satisfy a bound of its type parameter (an interface it does not implement, or `Copy`) | `total(plain)` with `fn total(…)` | | N0264 | A type a region cannot hold (R2): one that needs drop, a scoped type, or a non-Copy fill value for `allocArray` | `arena.alloc(Res { id: 1 })`, `arena.alloc(someReference)` | Generic code (M6a) reuses codes: - an operator on a type parameter is N0207, a field or method on one is N0234, and a literal for one is N0205; - a generic type without its arguments, or with the wrong number of them, is N0204; - a duplicate or shadowing type parameter is N0201; - a method with type parameters of its own is N0216; - a generic `main` is N0219; - naming something that is not an interface as a bound or in an `impl` is N0204; - a method on a type parameter that no bound declares is N0234; - an `impl` method clashing with a type's own method is N0228. Region values reuse codes: - assigning a `using` binding as a whole is N0211; - nesting a frame that is not a `using` binding is N0248; - a method call on a frame that is not a local is N0247; - a storage that is not `[u8; N]` or `SliceMut` is N0205; - returning a frame or fixed-buffer allocator without sources is N0245. `?` inside a deferred statement is N0254, and `Ok`/`Err` with the wrong number of values is N0209. The names `Result`, `Ok`, `Err`, `AllocError`, `Box`, `SystemHeap` and `FailingAllocator` cannot be declared (N0201). Moving out of a box's contents is N0251 until M3c. ### Build (N0300–N0399) | Code | Meaning | Example | |---|---|---| | N0300 | `novac build --emit exe` without a `main` function | a library-only file | Names across packages (N0310–N0319; `type-system.md` §2, ADR 0013): | Code | Meaning | Example | |---|---|---| | N0310 | The package named in an import, or in a struct literal `p.T { … }`, is not a dependency of the package (elsewhere an unknown `p` in `p.x` is N0200) | `import geometri;` | | N0311 | A dependency has no item of that name | `geometry.Pointt` | | N0312 | The item exists but is not `public`, or a field or method of a public struct is not | `geometry.helper()` with `fn helper` internal | | N0313 | A simple name is provided by two imports | `import a; import b;` both with `Point`, then `Point` | | N0314 | A public item's signature names a type that other packages cannot see | `public fn make() -> Hidden` with `struct Hidden` internal | | N0315 | An item, parameter, local, loop variable or pattern binding has the name of one of the package's dependencies | `let geometry = 1;` | Packages (N0320–N0329; `tooling.md` §4, ADR 0013): | Code | Meaning | Example | |---|---|---| | N0320 | `nova.toml` or `nova.lock` is not valid in the TOML subset | `name = app` (no quotes) | | N0321 | A manifest field is missing, unknown or invalid, or a dependency is an executable | `kind = "binary"` | | N0322 | A path dependency's directory or manifest is missing, or its name differs from its key | `geometry = { path = "../geo" }` with no `../geo/nova.toml` | | N0323 | A git dependency cannot be fetched or resolved, or is absent with `--offline` | an unknown tag | | N0324 | The lock file is missing or does not match the manifest | a dependency added without `novac fetch` | | N0325 | A git checkout's manifest and sources do not match the lock's hash | a file edited in the cache | | N0326 | One package name reached through two different sources | two paths to two `geometry` packages | | N0327 | A dependency cycle | `a` depends on `b`, `b` on `a` | | N0328 | An invalid version, version requirement, or index entry (`tooling.md` §4.6) | `shapes = "1.x"` | | N0329 | No versions satisfy the requirements: the conflicting requirements and where each came from (§4.7) | `^2.1 (from geometry 1.4.0) and ^1.0 (from plot 0.3.0)` | | N0335 | `novac publish`: the package cannot be published as it is (§4.8) | a path dependency, or uncommitted changes | Dynamic dispatch and owning closures (N0330–N0339; `type-system.md` §37, §36.5, ADR 0015): | Code | Meaning | Example | |---|---|---| | N0330 | `dyn I` where it is not behind a reference or a `Box`: a local, parameter, field, element, optional or type argument | `fn f(s: dyn Shape)`, `Vector` | | N0331 | `dyn I` for an interface that is not dyn-compatible: one of its methods takes `this` by value | `interface Sink { fn close(this); }` then `borrow dyn Sink` | | N0332 | A `move` literal captures a scoped value: a reference, view, slice, closure, or a value with region parameters (`type-system.md` §36.5) | `let v = s.view(); Box.create(move () => v.length)` | | N0333 | A `move` literal anywhere but as the value of `Box.create`/`createIn` with an expected `Box` | `let f = move (x: u64) => x;` | | N0334 | A value that is not `Send` where it must cross to another thread: a spawned closure's capture or result, a `dyn … + Send` coercion's source, a `Send` bound's argument, or, for `Thread.both`, a capture that may not be used from two threads at once or an argument that is not a closure literal or `fn()` value (`type-system.md` §38, §38.6) | `Thread.spawn(move () => view.length)` with `view: StringView` | | N0336 | `@effectsOf` that names no parameter, or a parameter that is not of a function or closure type, or has no names (`type-system.md` §32.5) | `@effectsOf(n)` with `n: i32`; `@effectsOf()` | | N0337 | A static that cannot be one: a `static` whose type is not `Share`, or a generic or local one (`type-system.md` §39) | `static C: Cell = …` with `Cell` not `Share` | | N0338 | Not a static initialiser: an initialiser or `@constant` body outside §39.4, or a C constant macro called elsewhere | `static N: u64 = compute();` | # Unsafe Code — Contract and Checks Status: **normative** from M8a (2026-10-24). Rules U1–U3 are in `ownership.md`; the typing rules are in `type-system.md` §27; the decisions are in ADR 0007. Safe Nova code cannot cause undefined behaviour (master prompt §6, `ownership.md` §1). Unsafe code can. This document says what unsafe code must guarantee, so that safe code keeps that property, and what the toolchain checks. ## 1. What unsafe permits Exactly the operations of `type-system.md` §27.3, and calls to `extern "C"` functions (§28.2; M8b). Later increments add MMIO accesses: - reading and writing through a raw pointer; - pointer arithmetic (`add`, `sub`, `offset`); - making references and slices from raw parts (`Ptr.toBorrow`, `Ptr.toBorrowMut`, `Slice.fromRaw`, `SliceMut.fromRaw`); - calling an `unsafe fn`. Making, copying, comparing and casting pointers is safe (§27.2): a pointer that is never used cannot do harm. An unsafe context relaxes nothing else (`type-system.md` §27.4). ## 2. The contract (U2) Every unsafe operation must be **valid** when it runs: | Operation | Requirement | |---|---| | `p.read()` | `p` is non-null, aligned for `T`, and points to a live, initialised `T` inside one object. For a `T` that is not Copy, the value is moved out (ADR 0027): until the next `p.write`, nothing may read, drop or borrow the `T` at `p`, or the value would have two owners (O1) | | `p.write(v)` | `p` is non-null, aligned for `T`, and points to live storage for a `T` inside one object, which nothing currently borrows (no live loan of it, shared or mutable). The old contents are not dropped: if they held a `T` that needs drop, it leaks unless it was read out first | | `p.add/sub/offset(n)` | always defined (the address wraps); the *result* must meet the requirements above before it is used | | `Ptr.toBorrow(p)` | as `read`, for as long as the reference is used; nothing writes the `T` meanwhile (except through the reference itself, for `toBorrowMut`), and for `toBorrowMut` nothing else reads it either | | `Slice.fromRaw(p, n)` | as `Ptr.toBorrow`, for each of the `n` elements; `n × size(T)` ≤ 2^63 − 1 | | calling an `unsafe fn` | the function's own `Safety:` contract | | calling an `extern "C"` function | the C function's documented contract, and the declaration's types match its definition; pointers it is given are valid for what it does with them, for as long as it keeps them; values it returns meet Nova's type invariants (`@repr` enums are checked) | **Pointers made from references** (M9c, ADR 0010): a pointer made from a reference (`r as *T`, `r as *mut T`, or `s.ptr` of a slice) may be used to **write** its target, directly or through a reference made from it by `Ptr.toBorrowMut` or `SliceMut.fromRaw`, only while the reference's loan could still be live. That is, only until the target is next used by anything other than that pointer: read, written, borrowed, moved, or its storage ended. After that, such a write is not valid even though the storage is live and nothing borrows it. Optimisations rely on this (`optimisations.md` §2): a vector's length, read directly, stays what it was until the next `borrow mut` of the vector. "Live" means the object's storage has not ended: the local is in scope (between its `StorageLive` and `StorageDead`), the box or vector buffer has not been freed, the region has not been reset or destroyed. "Inside one object" means within one local, one heap block or one region allocation, as laid out by `TargetInfo`. In addition, unsafe code must leave every **safe-code invariant** intact *as observed by safe code* (U2): ownership (O1–O5: a value has one owner, is destroyed once), borrowing (B1–B5: no write to a place while a shared loan of it is live, no second access while a mutable loan is live), regions (R1–R4), the heap (H1–H3), and the type invariants (a `bool` is 0 or 1, an enum's tag is one of its variants, a `StringView` is UTF-8). For example, writing `2` through a `*mut u8` that points at a `bool` breaks safe code later, even though the write itself is valid. **Unconstrained references** (U3): a reference made by `Ptr.toBorrow`, `Ptr.toBorrowMut` or `fromRaw` holds no loans, so the borrow checker cannot see what it refers to. The unsafe code must bound it. The usual way is a safe function whose signature ties the result to a parameter, so that safe callers are checked against that parameter: ```nova struct Buffer { data: *mut u8; length: usize; /// Safety: `data` points to `length` live bytes owned by this buffer (the constructor's contract). public fn bytes(borrow this) -> Slice => unsafe { Slice.fromRaw(this.data, this.length) }; } ``` A caller's slice from `b.bytes()` borrows `b`, so `b` cannot be changed or destroyed while it is used. Without such a bound, the reference could outlive its target, and nothing would report it. ## 3. The `Safety:` rule Each `unsafe fn` and each function containing `unsafe` carries a doc comment with a `Safety:` line (N0283; `type-system.md` §27.4): - for an `unsafe fn`, it states what the **caller** must guarantee; - for a safe function, it explains why the unsafe operations inside meet §2 **for every safe caller**. If that argument depends on something the caller could break, the function should be `unsafe` instead. The compiler checks only that the comment exists. Reviews check that it is true. ## 4. Lowering to C (c-backend §2) | Nova | C | |---|---| | `*T`, `*mut T` | `T *` (C's type of `T`); `*void` is `void *`. No `const`: read-only is a Nova rule, and CosmoOS's `-Wcast-qual` would reject casting it away | | `p.read()` / `p.write(v)` | `(*p)` / `(*p) = v` | | `p.add(n)` | `(T *)(uintptr_t)((uintptr_t)p + (uintptr_t)n * sizeof(T))`; `sub` subtracts, and `offset` converts the signed `n` to `uintptr_t` first (modulo 2^64) | | `r as *T` | `(T *)(uintptr_t)r` | | `p as *U` | `(U *)(uintptr_t)p` (no pointer-to-pointer cast, so no `-Wcast-align` or `-Wcast-qual`) | | `p as usize` / `n as *T` | `(uint64_t)(uintptr_t)p` / `(T *)(uintptr_t)n` | | `0 as *T`, `p.isNull()` | `(T *)(uintptr_t)0`, `p == NULL` | | `Ptr.toBorrow(p)` | `p` (a C pointer, as references are) | | `Slice.fromRaw(p, n)` | the slice struct `{ .ptr = p, .length = n }` | C itself makes pointer arithmetic outside an object, and comparisons of unrelated pointers with `<`, undefined. Nova's lowering avoids both: arithmetic goes through `uintptr_t`, and pointers have no ordering (only `==`/`!=`, which C defines for any two pointers). A conversion between a pointer and `uintptr_t` is implementation-defined in C11 (6.3.2.3); clang and GCC define it as the address on every target Nova supports (inferred from their documentation; checked on the corpus by the sanitizers and the interpreter). ## 5. What is checked - **Statically**: the capability rule (U1, N0280), pointee and operation rules (N0281, N0282), the `Safety:` comment (N0283). Safe code is checked as before (`type-system.md` §27.4). - **The interpreter** (`intermediate-representation.md` §5) runs unsafe code with its model of memory, and reports as an interpreter fault (exit status 70): a null pointer; an address in no live object, or in an object whose storage has ended; an access that does not fit inside the object; a misaligned access; reading uninitialised bytes. It does not track loans (no Stacked Borrows), so it does not detect a write through a pointer while safe code holds a loan of the target. - **Natively**, the run corpus also runs with ASan and UBSan. - **Not checked**: that the `Safety:` comment is true; the type invariants of §2 (a bad `bool` written through a pointer is caught only when it misbehaves); the rule for pointers made from references (§2), since the interpreter does not track loans. # Ownership, Borrowing and Lifetimes — Formal Safety Design (v0) Status: Milestone 0 proposal. This document is normative for Milestone 2, unless an item is marked *future*. **B1–B5 and E1–E3 are implemented as specified in `lifetimes.md` (M2b),** which records the v0 simplifications: immutable, once-initialised reference bindings; and B5 implemented as "the receiver's loan starts after the arguments". Related: `memory-model.md`, `language-spec.md`, `intermediate-representation.md`, `unsafe.md` (M8a). ## 0. Scope of the guarantee **Memory safety** for Nova means: no execution of a well-typed program that contains no `unsafe` code (and links only correctly-specified `unsafe` libraries) performs any of: 1. use-after-free; 2. double free; 3. dereference of a dangling reference; 4. out-of-bounds access; 5. read of uninitialised memory; 6. type-confused access; 7. invalid ownership transfer; 8. unsynchronised concurrent access where at least one access is a write (data race; only once threads exist, M-later). **Not** included in memory safety, but each with defined behaviour: | Condition | Defined behaviour | |---|---| | Integer overflow (`+ - *`, negation, `abs`) | **Panic** in all profiles (OD-6). `wrapping*`, `saturating*`, `checked*` methods available | | Division / remainder by zero, `MIN / -1` | Panic | | Shift by ≥ bit width | Panic (`wrappingShl` masks) | | Out-of-bounds index | Panic (`get(i)` returns `T?`) | | Narrowing numeric cast | `as` is **only** allowed where lossless; `truncate(x)`, `checkedCast(x)` explicit | | `T?` unwrap of `null` | Panic (`!` postfix operator) | | Allocation failure | **Value** (`Result<_, AllocError>`); panicking convenience APIs exist only in the userland profile | | Stack overflow | Profile-defined: userland relies on guard page → process termination; kernel profile uses static stack-depth checking (see `cosmoos/profiles.md`) | | Resource exhaustion, deadlock, logic errors, leaks via `forget` | Not prevented | | Panic | **Abort**, no unwinding (OD-5). Destructors do not run on panic. Userland: print + `abort()`; kernel: CosmoOS `panic()` | Leaks are memory-safe (as in Rust), but the safe standard library must not leak in ordinary use, and no Nova feature may use leaking as a substitute for lifetime soundness (master prompt §5.6). ## 1. Vocabulary - **Place**: a storage location expression: a local, a field of a place (`p.f`), an element of a place (`p[i]`), or the target of a reference (`*r`, written implicitly in surface syntax). - **Copy type**: a type whose values may be duplicated bitwise: primitives, `borrow T` (shared), raw pointers, and `struct`/`enum` types all of whose fields are Copy and which declare no `drop` (and are not marked `@noncopy`). Everything else is a **move type** (affine). - **Loan**: the creation of a reference to a place. Kinds: `shared` (`borrow T`) and `mut` (`borrow mut T`). - **Region**: a set of points in a function's control-flow graph (NIR) during which a loan must remain valid. - **Scoped type**: any type that contains a reference (`borrow T`, `borrow mut T`, `Slice`, `StringView`, arena handles, guard-protected handles), transitively, including through generic arguments. A non-scoped type is **free-standing**: it may outlive any particular stack frame. - **Owner**: the place that holds a value of a move type. Destroying the owner destroys the value. ## 2. Invariants The checker establishes these properties on the NIR (control-flow IR) of every function. "At point p" means on entry to the NIR statement at p. ### Ownership - **O1 — Unique owner.** Every live value of a move type is held by exactly one initialised place. - **O2 — Move de-initialises.** Moving from a place makes it *uninitialised*. A move out of a place is permitted only if the place is a local, or a field path rooted at a local, whose type has no `drop` (partial move). It is never permitted through a reference, an index, or out of a `drop`-implementing struct. - **O3 — Definite initialisation.** A place may be read, borrowed, or have a field assigned only if it is *definitely initialised* on every path reaching that point. (Forward must-dataflow over NIR. The same analysis implements use-after-move detection.) - **O4 — Exactly-once destruction.** At every point where a place goes out of scope (`StorageDead`), and before every assignment that overwrites an initialised place, the compiler inserts `drop(place)` if the place is definitely initialised, or a guarded drop if it is *maybe* initialised (a drop flag). A definitely uninitialised place gets no drop. Drop order: locals in reverse declaration order; struct fields in declaration order; vector elements in index order. - **O5 — Partial initialisation.** Struct literals initialise all fields atomically from the program's point of view. Their C lowering constructs in place, and no partially built struct is observable. A partially *moved* struct (O2) drops only its remaining initialised fields. ### Borrowing - **B1 — XOR aliasing.** At any point, for any place P, the live loans that overlap P are either all shared, or exactly one is `mut`. Overlap is path-prefix based: `a` overlaps `a.f`; `a.f` does not overlap `a.g`; `a[i]` overlaps `a[j]` (no index disjointness in v0). - **B2 — Loan validity.** While a loan L of P is live (inside its region), P must not be moved, dropped, go out of scope, or be assigned. If L is shared, P must also not be mutated or mutably borrowed. If L is `mut`, P must not be accessed at all except through L. - **B3 — Liveness regions (NLL).** A loan's region is the smallest set of CFG points containing every point at which some reference derived from the loan (transitively, through reborrows and copies of shared refs) may still be used. It is computed by backward liveness over NIR, then by solving outlives constraints. A shared reference copied into another variable propagates the loan. - **B4 — Reborrowing.** `borrow mut T` passed to a `borrow mut T` parameter is implicitly reborrowed. The original is unusable until the reborrow dies and becomes usable again afterwards (no move). - **B5 — Two-phase borrows.** For method calls `recv.m(args)` with a `borrow mut this` receiver, the mutable loan of `recv` is *reserved* while `args` are evaluated (permitting shared reads) and *activated* at the call. Required for `v.push(v.length)`. This is the only two-phase site in v0. ### Escape (lifetime) rules - **E1 — No escape beyond the owner.** A value of scoped type may flow only to: (a) locals of the same function; (b) callee arguments; (c) the function's return value, if the elision rule E2 links it to a parameter; (d) fields of scoped *local* aggregates (tuples/anonymous temporaries), which then become scoped. - **E2 — Return elision.** A function returning a scoped type must have exactly one `borrow`/`borrow mut` parameter (including `this`), or must annotate the source: `fn pick(a: borrow T, b: borrow T) -> borrow(a) T`. The returned value's region is constrained to be within the named parameter's loan at the call site. A function with no borrow parameters cannot return a scoped type, except `static` data (string literals, `static let`), which have the `static` region. - **E3 — Scoped fields only with region parameters.** In v0, user `struct`/`enum` declarations could not contain scoped types. Since M6x (OD-3, ADR 0006) a struct or enum may hold scoped fields if it declares region parameters (`struct Parser { input: StringView }`), and generic instances with scoped type arguments are scoped themselves (`type-system.md` §25.3–§25.4). Region values and pool boxes still stay in locals. - **E4 — No scoped globals or heap.** Scoped values may not be stored in `static`s, constants, `Box`, pools, or arrays. Since M6x, `Vector`, `StableVector`, `SlotMap` and region memory may hold scoped values; the container is then scoped and carries their loans (`type-system.md` §25.4, §25.7). E1–E4 deliberately trade expressiveness for an implementation of tractable size (see "Why not full Rust lifetimes" below). ### Regions (arenas) and guards - **R1 — Allocation borrows the region.** `arena.alloc(...)` takes `borrow this` and returns a scoped value carrying a *shared loan of the arena*. Therefore: - the arena cannot be dropped, moved, reset, or mutably borrowed while any allocation from it is live (B2); - arena allocations cannot escape the arena's scope (E1–E4). - **R2 — Trivially-destructible contents (v0).** Arena allocation is permitted only for types with no `drop` (transitively). Bulk release is then semantically equivalent to individual destruction. Arena-with-drop-list is *future* (OD-8). - **R3 — Pinned scoped bindings.** `using x = e;` declares a binding that cannot be moved, reassigned, or returned; it is dropped exactly at the end of the enclosing block. Arenas, lock guards and quiescence guards are normally bound with `using`. (Types may require `using` by declaration: `@scopedOnly`.) - **R4 — Guard-protected borrows.** An API may return values scoped to a guard (`table.lookup(borrow guard, key) -> Obj?` where `Obj` is scoped to `guard`). By B2/E1 these cannot outlive the guard. This is the mechanism for epoch/quiescence-protected kernel lookups (`cosmoos/kernel-memory-and-lifetimes.md`). ### Heap - **H1 — Box ownership.** `Box` is a move type that owns exactly one allocation of `T` obtained from allocator `A`. Its drop runs `T`'s drop, then `A.free`. `A` must be a free-standing *allocator type* (§3 of `memory-model.md`). Allocations from non-free-standing allocators are scoped values (R1). - **H2 — Collections.** `Vector` owns its buffer. Element access returns loans of the vector (`borrow this`/`borrow mut this`), so growth (which takes `borrow mut this`) is rejected while element borrows are live (B1/B2). Stable references into collections use `SlotMap`/`Handle` with generation validation (library type, M6). - **H3 — No owning cycles.** Only `Box` and owning collections own, and their ownership graph is a forest by construction (O1). Back-references and graphs use indices, handles, or arenas. ### Threads (M13 G4; ADR 0016, type-system.md §38) - **T1 — Only `Send` values cross.** A value reaches another thread only by being moved into a spawned closure, or returned from one through `join`. Each is checked `Send` (N0334). A `Send` value holds no loan and no raw pointer, so after the move the spawner reaches nothing the new thread owns. - **T2 — Sharing is through `Arc` of `Share` types.** The only safe way for two threads to reach the same memory is an `Arc` with `T: Share`, which lends only `borrow T`. A v1 type can be changed through a shared reference only if it is one of the library's `@threadSafe` types: `Mutex` (whose guard lends `borrow mut T` to one thread at a time) and the atomics (whose operations are atomic). So two threads can write the same memory only through a mutex or an atomic: there is no data race in safe code. - **T3 — The library's `@threadSafe` types** keep T2 in their `unsafe` code. `Mutex.lock` takes the platform lock before lending. The guard's drop releases it, after the guard's loan has ended (B-rules). `Arc`'s count changes only by atomic operations, and only the owner that drops it to zero destroys the value. A spawned thread is joined before `main` returns, so no thread outlives the values `main` dropped (O2). `@threadSafe` is accepted only in `lib/std`, so user code cannot claim it (N0290). - **T4 — Fork-join borrows end with the call** (`Thread.both`, §38.6; ADR 0018). A closure passed to `Thread.both` may reach the caller's values through references, because it runs only during the call and the call returns only after the thread running it has ended. Both closures' loans are live for the whole call, so the B-rules keep them from reaching one value in conflicting ways. A value reached from both threads is reached shared by both and is `Share`; one reached mutably is reached by one thread only and is `Send`. So T2's conclusion holds: there is no data race in safe code. ### Unsafe - **U1 — Capabilities.** Raw pointer dereference, pointer arithmetic, calls to `extern` or `unsafe fn`, `transmute`, access to `static mut`, MMIO and inline assembly are allowed only inside `unsafe { }` or an `unsafe fn`. - **U2 — Contract.** Every `unsafe` block must preserve O1–O5, B1–B5, E1–E4, R1–R4, H1–H3 and T1–T3 *as observed by safe code*. Every `unsafe fn` and every safe function containing `unsafe` must state its contract in a `/// Safety:` doc comment. A lint enforces that the comment exists. - **U3 — Raw pointers are inert.** Raw pointers (`*T`, `*mut T`) are Copy, carry no loan, and are ignored by the borrow checker. Converting a raw pointer to a reference (`unsafe { Ptr.toBorrow(p) }`; `type-system.md` §27.3) creates a reference with an *unconstrained* region, which must be immediately bounded by a safe wrapper's signature (as with Rust's unbounded lifetimes; this is the main hazard in unsafe code). ## 3. How the compiler rejects invalid programs Pass order (see `architecture.md`): 1. **Type check (HIR)**: kinds (Copy / move / scoped), E2 annotations, E3/E4 structural checks, `unsafe` context (U1), `using` restrictions (R3), arena element constraint (R2). 2. **NIR construction**: explicit `StorageLive/StorageDead`, `Move`, `Copy`, `Borrow(kind, place)`, two-phase reservation markers, and the `drop` sites still unelaborated. 3. **Definite-initialisation / move analysis** (O2–O3): forward dataflow, with a per-place bitset of {init, uninit, maybe}. 4. **Borrow check** (B1–B5, E1–E2, R1, R4): a. compute liveness of every reference-typed local; b. generate loans at `Borrow` statements and region variables for every reference type occurrence; c. generate outlives constraints from assignments, calls (signature instantiation), and returns (E2); d. solve regions as sets of points (NLL-style fixpoint); e. for every access, report a conflict if any live loan in the solved region at that point conflicts (B1/B2). 5. **Drop elaboration** (O4): inserts static drops and drop flags. Runs after the borrow check, so drops are checked as accesses in step 4 using the *unelaborated* drop sites. Diagnostics name the loan's creation point, the conflicting access, and the later use that keeps the loan alive, as three labelled spans. This is the single most important usability feature of the checker (see `diagnostics.md`). Example compile-fail cases (each becomes a test in `tests/corpus/compile-fail/`): ```nova fn dangling() -> borrow i32 { // error E2: no borrow parameter to derive from let x = 5; return borrow x; } fn useAfterMove(b: Box) -> void { consume(b); print(b.value); // error O3: `b` used after move at line 2 } fn invalidate(v: borrow mut Vector) -> void { let first = v[0]; // Copy of i32: fine let r = borrow v[0]; // shared loan of *v v.push(1); // error B1: mutable loan while `r` live print(r); // ... `r` used here } fn arenaEscape() -> Slice { // error E2: no borrow parameter using arena = Arena.create(SystemHeap)!; return arena.allocArray(16)!; } ``` ## 4. Why not full Rust lifetimes in v0 Named lifetime parameters on aggregates, higher-ranked bounds and variance are where most of Rust's borrow-checker complexity and annotation burden live. E1–E4 keep the analysis entirely **intraprocedural** (signatures summarise callees completely), with no lifetime parameters on types. That is enough for: borrowing parameters, slices, string views, iterators over a borrowed collection (as a local), arena-scoped computations, and guard-scoped kernel lookups. It is not enough for: long-lived parsers holding `StringView`s, structs of references, or zero-copy data structures. Those are OD-3, designed in M6f ([`od3-design.md`](od3-design.md)) so that v0 programs remain valid. ## 5. Behaviours requiring runtime checks | Check | Elidable when | Pass | |---|---|---| | Array/slice bounds | Index proven `< len` by range analysis (M9) | Lowering emits `nova_check_index` | | Integer overflow | Value ranges proven (M9) | `nova_add_i32` etc. | | Division by zero / `MIN/-1` | Divisor is a non-zero, non-minus-one constant | — | | Shift width | Constant shift amount | — | | Optional unwrap `!` | Flow-typing after `!= null` | — | | Drop flags | Static init state known (most cases) | Drop elaboration | | Allocation failure | Never elided (it is a value) | — | | Handle generation check (`SlotMap`) | Never in v0 | Built-in (M6e; library in M8) | | Kernel: stack depth | Static call-graph bound (kernel profile) | Link-time report | ## 6. Testing obligations (M2 acceptance) - Every invariant above has at least two compile-fail tests and one compile-pass test that is "close" to failing (for example NLL cases where a loan ends before the conflicting access). - Mutation suite: for each compile-pass test, automated mutants (swap statement order, remove a scope, duplicate a move) whose expected verdict is computed independently, by hand-labelling or a reference interpreter, not by the checker itself. - Generated C for compile-pass tests is run under ASan + UBSan in CI (supporting evidence only). # References and the Borrow Checker (M2b) Status: normative for increment 2b. This document turns `ownership.md` invariants B1–B5 and E1–E3 into an algorithm on NIR. The surface rules for references are in `type-system.md` §13. Where this document is stricter than `ownership.md`, it records a v0 simplification that later milestones may relax. ## 1. What makes the v0 problem small The algorithm can be simple and **exact for v0** because of four language rules (type-system §13): 1. **References are values of type `borrow T` / `borrow mut T` held only in locals and temporaries.** They never appear in struct fields (E3) or constants. 2. **A reference binding is immutable and initialised at its declaration** (`let r = …;`; no `var r: borrow T`). Assigning to `r` writes *through* it (`*r = v`) and never re-points it. So each reference local is assigned exactly once, which makes the points-to sets below flow-insensitive without losing precision. 3. **No nested references** (`borrow borrow T` is rejected). 4. **A use of a `borrow mut T` value is an implicit reborrow** of its referent (§4). Two usable mutable references to one place can therefore never coexist. Together with lexical scoping, (1) and (2) mean a reference cannot outlive its referent *inside* a function, except through a **temporary** borrowed at a call site, or through **`return`**. Both are checked explicitly (§6, §7). Conflicting accesses while a loan is live (B1/B2) are the remaining work, and they need liveness (§5). ## 2. NIR additions ```text projection = Field(f) | Deref | Downcast(V) | Index(operand) place = local projection* -- (*r).f is r Deref Field(f) rvalue += Ref(Shared | Mut, place) -- creates a loan | Slice(Shared | Mut, place, lo, hi) -- M2d: also a loan, of the whole array or slice viewed stmt += StorageDead(local) -- the local's storage ends (scope exit, statement end for temps) ``` Lowering facts the checker relies on: - Every `Ref` is assigned directly to a local or temporary: `t = Ref(k, p)`. - A temporary lives until the end of the source statement that created it. At that point the builder emits `StorageDead(t)`. - User locals get `StorageDead` at the end of their block, and on `break`/`continue` for the blocks being left. (`return` ends every local; references escaping through `return` are §7's job.) - A use of a `borrow mut T` operand at a call argument, an initialiser or a return is lowered as `Ref(Mut, (*r))`, a reborrow. A coercion of `borrow mut T` to `borrow T` is `Ref(Shared, (*r))`. ## 3. Loans and points-to sets - Each `t = Ref(k, p)` at program point P creates a **loan** `L = (k, p, P)`. - Each reference local `r` has a set `loans(r)` of the loans it may refer to (directly or through reborrows). It is the least solution of: | NIR | Constraint | |---|---| | `t = Ref(k, p)` | `L ∈ loans(t)`; and if `p` contains `Deref` of a reference local `q`, then `loans(q) ⊆ loans(t)` (a reborrow keeps the original loan alive) | | `t = Slice(k, p, lo, hi)` | as `Ref(k, p)`: a slice is a reference to `[T]`, and its loan covers all of `p`, whatever the range | | `t = Use(Place(r))` with `r` of reference type | `loans(r) ⊆ loans(t)` | | `t = Call(f, args)` where `f` returns a reference | `loans(arg_i) ⊆ loans(t)` for every parameter `i` that `f`'s signature says the result is derived from (§7) | | reference **parameter** `p_i` | `Param_i ∈ loans(p_i)`, a placeholder loan for "whatever the caller lent" | Because every reference local is assigned once (§1), solving these subset constraints by fixpoint iteration gives the same result as a flow-sensitive analysis. **Loan-holding locals (M4a).** Since M4a, every local of a *scoped* type holds loans (`type-system.md` §19.1), not only reference locals. The constraints generalise: - **Copies and moves.** When an assignment's rvalue reads a place rooted at a loan-holding local `q` (`Use`, aggregates, enum values, built-in calls), then `loans(q) ⊆ loans(t)`. This covers a payload read such as `t = (q as Ok).0`. - **`Unpack`.** `Unpack(q, …)` gives each target `loans(q)`. - **Borrowing a loan holder.** `Ref(k, p)` where `p` is rooted at a loan-holding local `q` and does not go through a deref: `loans(q) ⊆ loans(t)`. A reference to a scoped value keeps that value's loans alive. - Liveness (§5) and the return check (§7) consider all loan-holding locals. ## 4. Accesses Each NIR statement and terminator performs **accesses** to places, in evaluation order: | Construct | Access | |---|---| | operand `Place(p)` | **Read** p | | `Ref(Shared, p)` | **BorrowShared** p | | `Ref(Mut, p)` | **BorrowMut** p | | `Assign(p, …)` | the rvalue's accesses, then **Write** p | | `StorageDead(l)` | **Dead** l | Two places **overlap** if they have the same root local and one projection list is a prefix of the other. `a` overlaps `a.f`, `a.f` does not overlap `a.g`, and `(*r)` overlaps `(*r).f`. Two `Index` projections always compare equal (M2d): `a[i]` overlaps `a[j]`, because v0 does no index reasoning. Two mutable slices of one array therefore cannot be live at once, even if their ranges are disjoint. Places with different roots never overlap. That is correct because aliasing between different roots only arises through loans, and every loan is checked when it is created. **Conflicts** (B1, B2): | Live loan \ access | Read | Write | BorrowShared | BorrowMut | Dead | |---|---|---|---|---|---| | Shared | ok | **conflict** | ok | **conflict** | **conflict** | | Mut | **conflict** | **conflict** | **conflict** | **conflict** | **conflict** | ## 5. Liveness (NLL) - A reference local is **live** at a point if some path from that point reads it (as an operand or as the root of a place) before the function ends. Reference locals are never redefined (§1), so there is no kill except at the definition. This is standard backward dataflow over the NIR CFG. - The **live loans after point P** are the union of `loans(r)` over every reference local `r` live after P. - **Check:** for every access `a` at point P to place `q`, and every loan `L` live after P with `L` not created at P, it is an error if `L.place` overlaps `q` and the table in §4 says conflict. The errors are N0240 (write), N0241 (borrow), N0242 (read) and N0243 (Dead: the place stops existing while borrowed). This accepts `let r = borrow x; print(r); x = 5;`, because `r` is dead after `print`. It rejects `let r = borrow x; x = 5; print(r);`. **Overwriting a reference is not a use of its referent (M11, G3).** A **Write** or **Dead** of a local `r` whose type is a reference does not conflict with a loan whose place goes through `*r`, such as `(*r).next`. The write replaces which object `r` points to; it does not change, move or end the object it pointed to. That object stays protected by the loans the new reference inherited from `r` when it was made (§3: a reborrow `Ref(k, (*r)…)` gets `loans(r)`). A box is not a reference: overwriting a box destroys its contents, so a loan through `*b` still conflicts. This accepts a cursor walking a list, `node = cursor!; … cursor = next_of(node);`, and a temporary that is reassigned in each iteration of a loop while a reference made through the previous value lives on. **Writes happen after the value (M2d).** In `Assign(p, rvalue)` the rvalue is evaluated first and `p` is written last. The **Write** access is therefore checked against the loans live *after* the statement; the rvalue's own accesses use the loans live before it. This accepts `a[0] = sum(a[..])`: the slice is dead once `sum` has returned, before `a[0]` is written. Rust's MIR gets the same result by writing a call's result in a separate step. **B5, two-phase borrows (as implemented).** For a method call `e.m(args)` whose receiver is `borrow mut this` and `e` is a place, the receiver's loan is created **after** `args` are evaluated, right before the call. Arguments may therefore read `e` (`v.push(v.len())`), but they cannot hold a borrow of `e` across the call. Evaluating a place has no side effects, so creating its loan later does not change what the program computes. The loan exists only for the call, which is exactly the interval in which it must be exclusive. ## 6. Temporaries `f(makeValue())` with a `borrow T` parameter borrows a temporary that dies at the end of the statement (`StorageDead`). If the result of `f` is derived from that argument and is stored in a reference local used after the statement, the `Dead` access conflicts with the live loan: N0243 ("temporary dropped while still borrowed"). The fix is to bind the value to a local first. ## 7. Returning references (E1, E2) - **Signature.** A function whose return type is a reference must name the parameters the result may be derived from. With exactly one reference parameter (a `borrow`/`borrow mut` receiver counts), that parameter is implied (elision). Otherwise the return type must be written `borrow(a, b) T` or `borrow mut(a) T`, naming reference parameters (N0245 if missing or wrong). `borrow mut` results may be derived from `borrow mut` parameters only. - **Body check at each `Return(op)`.** For every loan `L ∈ loans(op)`: - `L = Param_i`: `i` must be among the named parameters (N0245); - `L` is a real loan whose place starts with `Deref` of reference parameter `p_i`: allowed if `i` is named (N0245 otherwise). Examples: `borrow this.items` with `this: borrow Self`, or `borrow (*a).f`; - any other real loan borrows a local or temporary of this function: N0244 ("returns a reference to local data"). - **Callers** use the signature as the `Call` constraint in §3. ## 8. What the v0 checker deliberately does not do - No references in aggregates, and no reference re-pointing — **relaxed in M6x** (OD-3, `type-system.md` §25): aggregates with region parameters, scoped collections and re-pointable reference fields. The checker keeps one loan set per value; writes into fields, elements and through references add the written loans to the holder and to what its mutable loans point to (iterated to a fixpoint); calls apply their signature flows; and a loan conflicts only at points reachable from where it was created (holders such as a vector receive loans after their own definition). Destroying a value whose `drop` may read its loans is a use of them. - Index-sensitive disjointness (`a[i]` versus `a[j]`) came in 2d with arrays, conservatively: all elements overlap. Since M13 G5, two different **constant** indices are different places, so `borrow mut a[0]` and `borrow mut a[2]` may be live together. That is sound because a constant index names one element whatever the array's length. Any index that is not a constant still overlaps every element. - Moves and drops are 2c. The `StorageDead` points emitted now are where drops will go. ## 9. Diagnostics Every borrow error names the conflicting access (primary span) and carries notes for the loan's creation point and for a later use of the reference that keeps it live, when one exists. # Nova Memory Model and Allocation Architecture (v0) Status: Milestone 0 proposal. Safety rules referenced as O*/B*/E*/R*/H*/U* are defined in `ownership.md`. Goose findings are in `research/goose.md` (commit `f605b5a`). Other prior art is in `research/comparative.md`. The CosmoOS kernel mapping is in `cosmoos/kernel-memory-and-lifetimes.md`. ## 1. Candidate architectures ### A. Ownership-based stack + heap (Rust-like) Values live on the native stack by default. Long-lived and dynamically sized data live in owned heap objects (`Box`, `Vector`, `String`). Safety comes entirely from ownership and borrowing. Arenas are ordinary library types built with `unsafe` plus lifetime-carrying APIs. ### B. Region-based + owned heap (Cyclone/Tofte–Talpin-like) Every allocation is placed in a *region*. Regions are lexically scoped (stack frames, dynamic regions) or the heap region. Pointer types carry a region, and an outlives/effect system prevents dangling. Owned heap objects exist as unique pointers in the heap region. Region inference reduces annotations. ### C. Hybrid: ownership + explicit regions + conservative escape analysis Architecture A's ownership/borrow core is *the only* safety mechanism. Regions (arenas, scratch stacks, fixed buffers) are **explicit library values whose allocations are loans of the region** (R1), so the borrow checker covers them with no separate region type system. A conservative escape/placement analysis (M5) may move allocations to cheaper storage only when the move is **unobservable** (§5). Goose-derived techniques (data stacks for scoped temporaries, construction in place, grow-only containers, typed pools, BCE) are adopted as **implementation strategies and library types**, not as the safety argument. ## 2. Evaluation | Criterion | A: Ownership | B: Regions | C: Hybrid | |---|---|---|---| | Safety argument | One mechanism (affine + XOR borrows). Proven for the λRust core | Region type system + uniqueness. Proven for core calculi, but **two** interacting mechanisms in practice | Same as A (one mechanism). Regions reduce to loans. Escape analysis is outside the trusted base | | Kernel fit (no hidden allocation, failure values) | Good | Good if regions are explicit. Poor with inference (hidden placement) | Good: placement changes require the profile to permit them (§5), and the kernel profile forbids implicit ones | | Ergonomics for C#/TS users | Annotation burden for structs of references. v0 limits mitigate (E3) | Many annotations, or inference with unpredictable lifetimes (MLKit space leaks) | As A, plus `using arena = ...` for batch lifetimes. Diagnostics explain placement | | Implementation complexity | High (NLL borrow checker) | High (region inference + effects) **plus** uniqueness | A's complexity + an *optional* M5 pass. Region support costs nearly nothing extra | | Runtime cost | `malloc`/`free` per heap object unless the user picks an arena | Cheap bump allocation everywhere; risk of space leaks | Same as A by default. Arena and scratch where the user or proven-safe analysis selects them | | Predictability | High | Low with inference | High: placement is either explicit or reported by `--explain-allocations` | | Goose techniques usable | Some (in-place construction, BCE) | Data stacks map naturally to lexical regions | All the "cheap, local" ones (goose.md §9 key cautions) | | Failure mode if the analysis is wrong | Unsound borrow check → safety bug | Unsound region inference → safety bug | Unsound borrow check → safety bug. Unsound placement → **caught**, because placement output is re-validated by the checker (`architecture.md` §1) | ### Recommendation: **C — Hybrid** 1. It keeps **one** safety mechanism to specify, test and eventually verify. Goose's own history shows the cost of a second, fragile mechanism: its lifetime checker is graded "C/D, structurally fragile" by its own inventory, and roughly half of its recent commits fixed holes (goose.md §9.4). Option B would add a region type system *alongside* uniqueness. Option C reduces regions to borrows. 2. Arenas, scratch regions and fixed buffers are first-class from M4 with no new type-system concept: "an allocation borrows its region" is checked by the same B1/B2/E1 rules as `borrow x`. 3. Escape analysis stays outside the safety argument (master prompt rules 10–11), and its effects are visible and reportable. 4. It fits CosmoOS: every allocation in the kernel profile names its allocator type or region value, so nothing allocates implicitly. Cost: the v0 restriction E3 (no references in user structs) is felt most for arena-heavy code (e.g. AST nodes pointing to other arena nodes). The v0 answer is indices or `Span` offsets (Ex16), and the long-term answer is OD-3. ## 3. Storage categories ### 3.1 Native stack (C automatic storage) - Scalars, fixed-size structs/enums/arrays, references, `Box` pointers, temporaries. - Size limit per value: `maxStackValue` per profile (userland default 64 KiB, kernel default 512 B). Larger fixed-size locals are a compile error suggesting `Box` or a region. This avoids unbounded native-stack consumption. - No variable-length arrays (C VLAs and `alloca` are never emitted). - Kernel profile: per-function frame-size estimates are reported, and the deepest call chain is checked against the kernel stack budget (C4/C7; see `cosmoos/profiles.md`). ### 3.2 Heap (owned) Implemented in C5 (`type-system.md` §30, `lib/core/alloc.nova`): `Layout` and `Allocator` with `allocate` and an `unsafe deallocate` (no `reallocate` yet), user allocators as field-less structs, for `Box` only. ```nova // core library (sketch) public struct Layout { size: usize; align: usize; } // align: power of two public interface Allocator { // `this` is a free-standing allocator value: ZST token or a 'static handle. unsafe fn allocate(borrow this, layout: Layout) -> Result<*mut u8, AllocError>; unsafe fn deallocate(borrow this, ptr: *mut u8, layout: Layout) -> void; unsafe fn reallocate(borrow this, ptr: *mut u8, old: Layout, newSize: usize) -> Result<*mut u8, AllocError>; // default: alloc+copy+free } ``` - *Free-standing allocator types* (`SystemHeap`, `KernelHeap`, `SlabCache`-backed allocator singletons) are zero-sized or `'static`. Only these may parameterise `Box`, `Vector`, `String` (H1). This keeps `Box` one pointer wide and makes "Box outlives allocator" trivially true. - *Instance allocators* (arenas, pools, fixed buffers) are values. Their allocations are scoped values carrying a loan of the instance (R1). A `Pool` hands out `PoolBox` (scoped, individually freeable). An `Arena` hands out `borrow mut T` / `SliceMut` (freed in bulk). - **Allocator lifetime**: free-standing allocators live for the whole program (or kernel lifetime). Instance allocators cannot be dropped while allocations are live (B2). Outstanding allocations therefore can never outlive their allocator (master prompt §5.5). - **Ownership of allocations**: always exactly one owner (O1). Freeing with the wrong allocator is impossible because the allocator is part of the type (`A`) or of the loan. - **Thread safety**: an allocator type declares `Sync` if it may be used concurrently (`SystemHeap`, `KernelHeap`: yes; `Arena`, `FixedBuffer`: no). Enforced once threads exist (M-later). - **Failure**: every allocating API returns `Result<_, AllocError>`. Convenience `...OrPanic` / `!` usage is allowed in userland and **rejected by a lint (error) in the kernel profile**. - **Testability**: `FailingAllocator(failAfter: n)` and a fault-injection hook in `SystemHeap` (env var in hosted test builds) make every failure path testable (M3 acceptance). - *As implemented in M3b (ADR 0003):* `FailingAllocator` is a built-in, zero-sized allocator type over the platform heap, steered by `FailingAllocator.failAfter(n)` and `.reset()` (a process-wide countdown). - The runtime counts every allocation attempt. `NOVA_FAIL_ALLOC_AT=k` fails attempt *k*, and `NOVA_ALLOC_REPORT=1` prints `nova alloc: attempts= live=` when `main` returns. - The test runner's sweep uses both (`testing.md`). ### 3.3 Arenas (regions) - `Arena.create(parent: A, chunkSize)`, an instance allocator over a free-standing parent. Bump allocation in chunks, with all chunks released at drop. - v0 contents must have no `drop` (R2), so bulk release equals destruction. Arena-with-drop-list is OD-8. - `reset(borrow mut this)` requires no live loans (B2), so it is safe by construction. - An arena is **not** the native stack: it is heap-backed (master prompt rule 9). ### 3.4 Scratch regions (Goose-style data stacks) From goose.md §9.1/§9.2: a per-thread, compiler-known **scratch stack** used for dynamically sized temporaries with nested lifetimes: ```nova fn render(n: usize) -> Result { using frame = Scratch.enter(); // records watermark let buf = frame.allocArray(n)?; // bump; scoped to `frame` ... } // watermark restored: one store ``` - Semantically an arena with LIFO discipline. Safety follows from R1 + R3 (`using` frames cannot be moved, and their allocations cannot escape). - Storage: hosted/userland uses a reserved virtual region with guard pages, plus an explicit capacity check. Exhaustion returns `AllocError` **unless** the frame was created with `Scratch.enterOrHeap()`, in which case it falls back to the parent heap. Kernel: a fixed per-thread buffer of profile-defined size with explicit checks. No reliance on page faults. - Nested frames must be strictly LIFO. *Corrected by ADR 0004:* `using` alone does not ensure this. Nested frames come from `parent.enter()`, which mutably borrows the parent, and only one root frame per thread may be live (a second `Scratch.enter()` panics). Goose's "one growable on top" rule is **not** adopted in v0. The scratch region hands out fixed-size allocations only. Growable scratch arrays are a future extension (OD-12). ### 3.5 Fixed-buffer and pool allocators - `FixedBufferAllocator.over(mut storage)` borrows its backing storage mutably for its whole lifetime, so the backing array cannot be touched directly while it is used. - *As implemented in M4c (ADR 0004):* `Pool` is a fixed-capacity pool whose `take(v)` returns a scoped `PoolBox`: a box that holds a loan of the pool and returns its slot when destroyed (`type-system.md` §19.6). `SlotMap` with generation-checked handles is still M6. - `Pool` / `SlotMap`: typed slots with generation counters (goose.md §9.8 plus Vale). Handles are free-standing `Handle` values (index + generation) validated on access (`get(h) -> borrow T?`). ### 3.6 Static data String literals and `static let` constants (immutable). `static var` is `unsafe` to access in v0 (data races); safe statics with synchronisation come with concurrency. ## 4. Collections and invalidation | Type | Ownership | Growth | Element references | Notes | |---|---|---|---|---| | `[T; N]` | value | — | loans of the array | Copy iff T Copy | | `Slice` / `SliceMut` | scoped borrow | — | reborrows | bounds-checked | | `Vector` (M6d, type-system §23.1) | owns buffer | reallocates (amortised ×2) | loans of the vector. Growth requires `borrow mut` → **rejected while element loans are live** | contiguous; **no address stability promise** | | `StableVector` (M6e, type-system §24.2) | owns chunks | appends a chunk, never moves | loans with *shared* access permitted during `push` (grow-only: goose.md §9.3) | push takes `borrow this` with interior bump. Not `Sync` | | `SlotMap` (M6e, type-system §24.1) | owns slots | may reallocate | `Handle` (not a loan) validated by generation | stable *identity*, not stable addresses | | `String` / `StringView` | owns / scoped | as Vector | as Vector | UTF-8 invariant | | `Box` | owns one object | — | loans of the box | `T?` niche | Iterator invalidation is impossible in safe code: iterators are scoped values holding a loan of the collection. ## 5. Placement analysis (M5; on by default since M9e) — rules The analysis may change *where* a value lives only under these conditions: 1. **Box → native stack / scratch**: allowed iff (a) the Box is created and dropped in the same function on all paths, (b) it is never moved into a callee or returned, (c) its address is not exposed to `unsafe`/FFI, (d) the profile marks `A` as *placement-transparent* (`SystemHeap` in userland: yes; **`KernelHeap`: no**, because kernel allocation is observable through accounting, and failure injection must remain meaningful), and (e) the size is below the profile's stack limit. Failure behaviour: the original program could fail with `AllocError`, and the optimised one cannot. This is permitted only because the placement-transparent contract declares allocation failure of that allocator "may spuriously succeed". 2. **Never** the reverse: a scoped/borrowed value is never promoted to the heap to make a program type-check (master prompt §7 and rule 11). There is no such code path, because placement runs after the borrow check on accepted programs only. 3. Destruction order and timing are preserved (master prompt rule 12): drops of demoted Boxes run at exactly the same NIR points. 4. Every decision is reported by `--explain-allocations`: `src/a.nova:12:9: Box placed on stack (does not escape; SystemHeap is placement-transparent)` `src/a.nova:20:9: Box stays on heap: moved into call to 'insert' at 21:5` ### 5.1 The M5 implementation (`PlacementAnalysis`) The analysis works per function on NIR after drop elaboration. It is deliberately simple and conservative: a box it cannot follow stays on the heap. Rule 1's conditions become the following checks. - **Sites.** A site is `R = Box.create(v)` or `Box.createIn(a, v)`, with `R` a whole local. The site is **eligible** when: - the allocator is `SystemHeap`, the only placement-transparent allocator so far (rule 1d). `FailingAllocator` is not transparent: its failures are its purpose; - `T` is at most 1 KiB, and the function's stack-placed boxes total at most 4 KiB (rule 1e); - each site has at most one box alive at a time, so one storage slot per site suffices. A site in no loop of the function's graph has this by construction. A site in a loop has it when every local of its flow set `F` (below) ends, by its `StorageDead`, on every path from the site back to itself (M9e): no local can then hold one execution's box when the site runs again, and since the box does not escape, it has been dropped by then. A local that is not ended inside the loop keeps the box on the heap ("may outlive its loop iteration"). The interpreter checks this rule: a site whose previous box is still alive when it runs again is an interpreter fault. - **The flow set** `F` of a site is the set of locals that may hold its box: `R` itself, and every whole local assigned (moved or copied) from a place rooted in `F` whose type is the box type or `R`'s result type. That includes the unpacked `Ok` payload and copies of the result. The analysis iterates until `F` stops growing. - **Escapes.** The box stays on the heap, with the first reason found, if: 1. a local in `F` has more than one definition. Its drops could then see a box from elsewhere; 2. a value of the box type or of `R`'s type, read from a place rooted in `F`, flows anywhere but into a local of `F`: a call or built-in argument, an aggregate, a return, or a non-local place (rule 1b); 3. a place rooted in `F` of the box type is borrowed **mutably** (`borrow mut Box`). The borrower could replace the box, and drop ours with the heap's drop glue. Borrowing the box shared, or borrowing its contents either way, is fine: the borrow checker ends those borrows before the box's drop; 4. a `Drop` destroys a place rooted in `F` that is neither a whole local of the box type nor inside its contents. Destroying contents in place is fine: drop elaboration does that to the fields left after a move out. A result holding the box, though, would be destroyed by the heap's drop glue. A result *may* be destroyed where it cannot hold the box: in a block dominated by a non-`Ok` target of a switch on that result's own discriminant, where the target is entered only from the switch. That is the error path of `?` and `!`, whose drop glue sees an `Err`. Reads through the box (`b.f`, `b.f = x`, moving a field out) and `discriminant` of the result are not escapes. Rule 1c (unsafe and FFI), from M8a: a function that makes a raw pointer from a reference, or a reference or slice from a raw pointer, keeps all its boxes on the heap (reason "address exposed"; `type-system.md` §27.5). Rule 1a (created and dropped in one function) follows from rules 2 and 4. - **Demotion.** A stack-placed site writes `v` into a per-site slot of the function's frame and makes `Ok(&slot)` without allocating; that allocation cannot fail. - A `Drop` of a box local in `F` destroys the contents only, and does not free. - A `BoxFree` of one does nothing. - Drops therefore run at the same NIR points with the same contents (rule 3). ## 6. No-GC baseline and cycles No tracing GC, no implicit RC. Ownership is a forest (H3). Graph-shaped data uses one of: `SlotMap` handles, arena + indices, or (future, OD-13) an explicit `Rc`/`Weak` library type with documented cycle-leak behaviour. In the kernel, explicit refcounted *kernel objects* wrap CosmoOS's own refcount protocol (`cosmoos/kernel-memory-and-lifetimes.md`). That is a different type (`KRef`) with type-specific release, not a generic `free`. ## 7. Goose technique adoption summary | Goose technique (goose.md §9) | Nova adoption | Milestone | |---|---|---| | Data stacks for scoped temporaries | `Scratch` regions (§3.4), with heap fallback and a no-MMU kernel mode | M4/M5 | | One-growable-on-top non-moving growth | Not adopted; `StableVector` instead | M6 | | Grow-only vs grow-shrink split | `StableVector`, monotone lengths feed BCE | M6/M9 | | Root inference by monomorphisation | **Rejected** (whole-program, fragile). NLL + E1–E4 instead | — | | Construction into destination / guaranteed NRVO | NIR destination places + guaranteed RVO for single-named-result functions | M2 (RVO), M7 | | Inline variable-size layouts | Future opt-in `@flat` types (OD-14) | post-M9 | | Relative references + verifier | Future library feature for zero-copy formats | post-M9 | | Type-safe pools | `Pool`/`SlotMap` with generations | M6 | | BCE with index types, 2^48 size cap, `checkfree` | Adopt the size cap in the spec now (all `len ≤ 2^48`), BCE + `@checkfree` in M9 | M0 (spec) / M9 | | `return … from` | Not adopted; `Result` + `?` | — | | Share-nothing workers | Possible library model later | post-M9 | **Spec rule adopted now (from Goose BCE):** every object, slice and collection length is `≤ 2^48 − 1` elements. Allocators reject larger requests with `AllocError`. This makes `len * sizeof(T)` overflow-free for `sizeof(T) < 2^15` and lets index arithmetic on `usize` be proven non-overflowing. # Formatting (M10 T1) `novac fmt` rewrites Nova source into one canonical style. It has no options: two programmers who run it get the same spacing, indentation, blank lines, brace placement and comment alignment. Where the style leaves a choice to the author (§3), it is a choice of line breaks, which the formatter keeps. The formatter changes only whitespace, and the trailing commas of §3.3. A formatted file parses to the same tree as the original (the same AST, apart from spans) and keeps every comment, in order. Formatting a formatted file changes nothing. ## 1. Command ```sh novac fmt file.nova... # rewrite each file in place (only if it changes) novac fmt --check file.nova... # change nothing; list the files that would change, exit 1 if any novac fmt --stdout file.nova # print the formatted text of one file ``` A file with a syntax error is not formatted: its diagnostics are printed, the file is left as it is, and the exit status is 1. A directory argument means every `.nova` file under it. ## 2. Lines and indentation - The text is UTF-8 with LF line endings; it ends with exactly one newline. No line has trailing whitespace. - Indentation is 4 spaces per level; there are no tabs. - A level is opened by a `{` that ends its line (a block, a struct, enum, interface or impl body, a match, a multi-line struct or array literal) and by a broken list (§3.3). Its contents are indented one level, and its closing delimiter is on its own line at the opening line's indentation. - **Blank lines:** at most one in a row. None directly after an opening `{` or before a closing `}`, and none at the start or end of the file. Elsewhere, between two items, two members or two statements, a blank line is kept if the source has one or more, and not added if it has none. - **Line width:** 120 columns, counting the code and not a trailing comment. A line longer than that is broken by §3.4 where it can be. A line that cannot be broken (a long string literal, a long name) stays long. ## 3. Line breaks ### 3.1 What the author decides The formatter keeps these choices and only lays out the result: - whether a block, struct, enum, interface or impl body, or match is written on one line (`{ return x; }`) or across lines (anywhere between its braces); - whether a list (arguments, parameters, struct fields, array elements) is broken, and, in a broken list, which elements start a new line (so a literal can keep several fields per line); - whether an expression body (`=> expr`) starts on the line of the `=>` or on the next one; - where a long binary expression or a chain of member calls (a `.` after a call, an index, `!` or `?`) is broken; - whether an attribute is followed by a line break. Any other line break in the source is removed: two tokens on different lines that no rule here separates are joined with their §4 spacing. In particular `else` always follows the `}` before it on the same line. A one-line form is kept only if it fits in 120 columns and holds no line comment; otherwise it is broken, as if the author had written it across lines. ### 3.2 One-line forms - A one-line block has one space inside each brace: `{ x += 1; }`, `{ break; }`. An empty block is `{}`. - A one-line struct body is `struct Point { x: i32; y: i32; }`; a one-line enum is `enum E { A, B(i32) }`. - A one-line match is `match (v) { 0 => "zero", _ => "many" }`, and as a statement `match (o) { Some(v) => { use(v); } null => {} }`. - A one-line struct literal is `Point { x: 1, y: 2 }`; an array literal is `[1, 2, 3]`; argument and parameter lists are `f(a, b)` and `fn f(a: i32, b: i32)`. One-line lists have no trailing comma. - **An if/else chain is laid out as a whole:** if one of its blocks is broken, or the author started an `else` on a new line, every block of the chain is broken. ```nova if (c) { a(); } else { b(); } // fits: kept on one line if (c) { // otherwise: every block broken a(); } else { b(); } ``` ### 3.3 Broken lists Statements, members, enum variants and match arms of a broken body are one per line. A list that is broken (by its author, or by §3.4) is laid out as a block: - the opening delimiter ends its line; - the elements are indented one level; each element that the author started on a new line starts a new line, and the others follow the element before them after `, `; a list broken by §3.4 has one element per line; - the closing delimiter starts its own line at the opening line's indentation; - the last element is followed by a comma, where the grammar allows one (grammar.md §2: parameters, arguments, struct field initialisers, array elements, enum variants, and the arms of a match expression, except an arm whose value is a block). The commas between the arms of a match statement are left as written. Type parameters, generic arguments, attribute arguments, closure parameters, function-type parameters and interface method parameters are never broken. ```nova let bodies = [ Body { x: 0.0, y: 0.0, z: 0.0, vx: 0.0, vy: 0.0, vz: 0.0, mass: SOLAR_MASS }, Body { x: 4.84143144246472090e+00, y: -1.16032004402742839e+00, z: -1.03622044471123109e-01, vx: 1.66007664274403694e-03 * DAYS_PER_YEAR, vy: 7.69901118419740425e-03 * DAYS_PER_YEAR, }, ]; ``` ### 3.4 Breaking a line that is too long When a line does not fit, the formatter breaks one construct that starts on it and lays the file out again, until the line fits or nothing on it is left to break. It takes the first kind in this order, and of that kind the outermost: 1. a function's expression body (`=> e`): the expression moves to the next line, indented one level; 2. a list or a one-line block or body (§3.3); 3. the value of a match arm or a closure (`=> e`), moved to the next line; 4. an initialiser or an assigned value (`= e`), moved to the next line; 5. a binary expression: after its operator, with the right operand indented one level; 6. a chain of member calls: before a `.`, indented one level. The author's breaks of the same kinds (§3.1) are laid out by the same rules. ## 4. Spaces - One space around binary operators (`a + b`, `x == y`, `a && b`), around `=` and the compound assignments, around `->` and `=>`, after `,`, `;` (inside a one-line form) and `:` (`x: i32`, `x: 1`). - No space around `..` (`0..n`, `a[i..j]`, `a[..]`), `.`, inside parentheses, brackets and generic arguments (`f(a)`, `[1, 2]`, `Vector`), before `(` in a call or declaration, after a unary operator (`-x`, `!b`, `*p`), before `?` and `!` (`f()?`, `r!`), or before `,` and `;`. - Keywords: one space after `if`, `while`, `for`, `match`, `return` (with a value), `fn` (in a declaration), `let`, `var`, `borrow`, `mut`, `move`, `as` and the visibility keywords; `if (c) {`, `} else {`, `} else if (d) {`. - One space before an opening `{` that follows code on its line. - Attributes are followed by one space or a line break, as written: `@context(atomic) fn f()`. ## 5. Comments - Comments keep their text and order. A comment on a line of its own is indented like the code that follows it (or, at the end of a block, like the code before the `}`). - A comment after code on the same line (a trailing comment) follows it after one space. Trailing comments on consecutive lines are aligned: each starts one space after the longest code among those lines. A line without a trailing comment, or a blank line, ends the group. - Doc comments (`///`) stay directly before their item; a blank line between a doc comment and its item is removed, since it would detach the comment (grammar.md §1.2). - A block comment (`/* … */`) inside a line keeps one space on each side. ## 6. Guarantees and their tests - **Same tree:** for every file of the repository that parses (the corpus included), the formatted text parses without error to an AST equal to the original's, spans aside (`FormatterTests`). - **Comments:** the comments of the formatted text are those of the original, in order. - **Fixed point:** formatting the formatted text changes nothing. - **The repository's own code** (`lib/`, `programs/`, `benchmarks/`, `integrations/`, the README's examples) is kept formatted: `novac fmt --check` runs over it in the test suite. The test corpus is not reformatted, since its annotations name lines and some files test unusual layout; it is the formatter's test input. # Tooling (M10) The commands of `novac` beside `build`, `check` and `run`. The formatter has its own document (`formatting.md`). ## 1. Documentation generator (T2) ```sh novac doc file.nova... # Markdown for the files' public items, on standard output novac doc lib/core -o docs/api/core.md --title "lib/core" novac doc --missing file.nova... # list the public items without a doc comment; exit 1 if there are any ``` The files are one package, as for `check` (a directory means every `.nova` file under it, in ordinal path order). `lib/core` is documented only when it is named. A file with a syntax error stops the command: its diagnostics are printed and nothing is written. The package does not have to type-check: documentation is made from the syntax, so it can be written for code that does not build yet. ### 1.1 What is documented - Every item declared `public`: a function, a constant, a struct, an enum, an interface, and a function in an `extern "C"` block. - Inside a public struct: its `public` fields, its `public` methods, and whether it has a `drop` block. Inside a public enum: every variant. Inside a public interface: every method. - An `impl I for T` block, listed under `T` as "Implements `I`" when `T` is a public struct or enum of the package. Nothing else is listed. Items in a `namespace` are listed under their simple names, as the language names them today (type-system.md §1). ### 1.2 The page The output is deterministic: the same files give the same bytes. - A title (`--title`, or the first file's name without its extension) as a level-1 heading, then a list of the documented items, each a link to its section. - One level-2 section per item, in the order of the files as given and of the items in each file. Its heading is the item's kind and name (`struct Volatile`, `fn sortBy`). - The item's **signature** in a `nova` code block: the item's attributes and its declaration up to its body (the `{ … }` or `=> …` of a function, the members of a struct), as `novac fmt` prints them (formatting.md). A struct's block lists its public fields; an enum's, its variants; an interface's, its methods. - The item's **doc comment** (grammar.md §1.2): its lines as written, which are Markdown. An item without one has only its signature. - A struct's public methods follow as level-3 sections (`StructName.method`), each with its signature and doc comment; then "Implements `I`" for each `impl`. ### 1.3 Missing documentation `--missing` writes nothing but a line per public item without a doc comment, `path:line: kind name`, and exits 1 if there is any. It is a lint for a package's public surface; nothing requires it to pass. ### 1.4 In the repository `docs/api/core.md` is generated from `lib/core` with `novac doc lib/core -o docs/api/core.md --title "lib/core"`. The test suite fails if it is out of date (`DocTests`), as it does for the README's examples. ## 2. Language server (T3) ```sh novac lsp # a Language Server Protocol server on standard input and output ``` The server speaks LSP 3.17 over JSON-RPC 2.0, with `Content-Length` framing, on standard input and output. It is part of `novac` and has no dependency beyond .NET's own libraries. The editor starts it; `editors/vscode` is a VS Code extension that does (§2.4). ### 2.1 What a document is checked with A document that is a source of a package (a `nova.toml` above it whose sources include it) is checked with its package, as `novac check` in the package would check it (§4.4): the editor's text for the document, the package's other files from disk, and the dependencies its `nova.lock` names, never fetching; the manifest's profile and `float` apply. Any other document, or one whose package does not resolve, is checked as a package of its own, with `lib/core`, as `novac check file.nova` would check it, using the text the editor holds. Its profile is hosted, unless its first line is `// profile: freestanding` (the corpus convention, `testing.md` §3). ### 2.2 Capabilities | Request or notification | What the server does | |---|---| | `initialize` | Answers with the capabilities below: full-text synchronisation, hover, definition, document symbols, formatting, quick fixes | | `textDocument/didOpen`, `didChange`, `didClose` | Keeps the text; after an open or change, checks the document (§2.1) and publishes its diagnostics. A close clears them | | `textDocument/publishDiagnostics` (sent) | Every diagnostic of the check: its range, severity, code (`N0205`) and message, with each note as related information | | `textDocument/hover` | On a name or an expression: its type, as Nova writes it. On a local or parameter, `let x: T` or `var x: T`; on a function or method, its signature; on a field, `name: T` | | `textDocument/definition` | On a local, parameter, function, method, field, struct, enum or interface name: the location of its declaration, if it is in the document | | `textDocument/documentSymbol` | The document's items and their members, as a tree: functions, structs (fields and methods), enums (variants), interfaces, constants | | `textDocument/formatting` | The document formatted by `novac fmt` (`formatting.md`), as one edit; nothing if it has a syntax error | | `textDocument/codeAction` | The fixes (§5.2) of the diagnostics in the requested range, as `quickfix` actions (§5.5) | | `shutdown`, `exit` | Stops | Anything else is answered with the JSON-RPC error `MethodNotFound` (requests) or ignored (notifications). Positions are LSP positions: zero-based lines, and columns in UTF-16 code units, which is how .NET indexes strings, so no conversion is needed. Hover and definition use the typed tree of the last check; if the document does not parse, they answer null and only the diagnostics are shown. ### 2.3 Errors The server never stops on a bad document. A request that fails inside the compiler is answered with the JSON-RPC error `InternalError` and the exception's message, and the server goes on. A message that is not valid JSON-RPC is answered with `ParseError` when it has an id. ### 2.4 The VS Code extension `editors/vscode` holds: - a TextMate grammar for `.nova` files (keywords, literals, comments, attributes), whose keyword list is the compiler's (`Keywords.Table`; `LspTests` compares them); - a language configuration (comments, brackets, auto-closing pairs); - the client, which starts `novac lsp` (the `nova.server.path` setting, default `novac` on the `PATH`) through the `vscode-languageclient` library, bundled with it into `dist/extension.cjs`. It is built with [Vite+](https://viteplus.dev/) (`vp`), not with the .NET build: ```sh cd editors/vscode vp install # the dependencies, pinned by pnpm-lock.yaml vp test # the grammar and the server tests below vp run check # lint and formatting vp run package # bundle the client (vp pack) and make nova-language-0.1.0.vsix (vsce) ``` Its tests: `test/grammar.test.mjs` tokenizes Nova lines with `vscode-textmate`, as VS Code does, and checks the scope each token gets; `test/server.test.mjs` drives a built `novac lsp` through `vscode-jsonrpc`, the library under VS Code's client (diagnostics, hover, definition, an unknown method, shutdown and exit). ## 3. Debugging (T4) A Nova program is debugged as the C it compiles to, with the C mapped back to Nova lines. No debugger needs to know about Nova. ### 3.1 A debug build `novac build -g file.nova -o program` writes `#line` directives into the C (`c-backend.md`, source maps) and compiles it with `-g -O0`. A debugger then shows Nova files and lines: breakpoints set by Nova line stop there, and stepping goes from one Nova statement to the next. A line can take more than one step, since one Nova statement can be several C statements (a checked addition, a temporary). | | lldb | gdb | |---|---|---| | Stop at a Nova line | `breakpoint set -f demo.nova -l 12` (`b demo.nova:12`) | `break demo.nova:12` | | Stop when a check fails, before the program aborts | `b nova_panic` | `break nova_panic` | | Where am I | `bt` | `bt` | ### 3.2 Names The C names follow `c-backend.md` §2.1, so a Nova name can be found in the debugger: | Nova | C | |---|---| | function `length` | `nvF_length` | | method `Counter.bump` | `nvM_7Counter_bump` (the type's name, prefixed by its length) | | local or parameter `sum` | `v_sum_1` (a number makes each local's name unique in its function) | | a temporary the compiler made | `t_3` | | field `x` | `f_x` | | struct `Point` | `nvT_Point` | ### 3.3 Nova names in the debugger `editors/debug/nova_lldb.py` and `editors/debug/nova_gdb.py` add two commands, which print with Nova names: - `nova-locals`: the current frame's locals and parameters, `name = value`, without the compiler's temporaries; struct values show their fields by their Nova names; - `nova-bt`: the backtrace, one frame a line, `Nova name at file:line` (`Counter.bump at demo.nova:3`). Load them with `command script import editors/debug/nova_lldb.py` (lldb) or `source editors/debug/nova_gdb.py` (gdb). Split C (`--split`, c-backend.md §2.4) renames the program's functions to `nvF__…`: set `NOVA_PROGRAM` to the program's name before starting the debugger, and the helpers read them as before. ### 3.4 What is checked `DebuggerTests` builds a program with `-g` and runs it under lldb (macOS) or gdb (Linux), in batch mode. A breakpoint at a Nova line must stop at that line, `nova-locals` must show the locals by their Nova names, and a breakpoint on `nova_panic` must give a backtrace whose frames are Nova functions at Nova lines. On a machine with neither debugger, the test does nothing, and the run log says which debugger ran. ## 4. Packages (T6, ADR 0013) ### 4.1 The manifest A package is a directory with `nova.toml`. Its sources are the `.nova` files under the directories `sources` names (default `src`), in ordinal path order. The manifest is a subset of TOML: `[table]` headers, `key = value` lines whose value is a string, a boolean, an array of strings or an inline table of strings, and `#` comments. ```toml [package] name = "app" # required: an identifier, not a keyword and not "core" version = "0.1.0" # optional; required to publish (§4.8), MAJOR.MINOR.PATCH kind = "executable" # "executable" (default), "library", or "object" (cosmoos/profiles.md §4.3) profile = "hosted" # or "freestanding" float = true # false is --no-float (type-system.md §35.9) kernel = "cosmoos" # with profile = "freestanding": include lib/kernel, as --kernel cosmoos (ADR 0025) trusted = true # its types may be declared @threadSafe (ADR 0025 §3.2); listed for reviewers sources = ["src"] c = ["src/shim.c"] # C files compiled and linked with an executable (type-system.md §28.6) [dependencies] geometry = { path = "../geometry" } lz4 = { git = "https://example.org/lz4-nova.git", tag = "v1.2.0" } # or rev = "", or branch = "main" shapes = "2.1" # from the registry (§4.6): a version requirement json = { version = "0.9", index = "https://example.org/my-index.git" } # from another index ``` A dependency's key is its package's `name`: a dependency is known by the name its own manifest gives it. A dependency of kind `executable` is N0321: only a library's items are used. Errors in the manifest are N0320 (its syntax) and N0321 (a missing, unknown or invalid field), with the line. ### 4.2 Resolving dependencies From the package being built, every dependency is resolved, and then its dependencies, recursively: - `path` is a directory relative to the manifest that names it, holding the dependency's `nova.toml`; - `git` is a repository; `rev` names a commit, `tag` and `branch` are resolved to one (§4.3). The commit is checked out into the cache, `$NOVA_HOME/git/checkouts//` (default `~/.nova`), and the dependency is the package at the checkout's root. A package name reached by two different sources (two paths, two commits, a path and a commit) is N0326, naming both dependents. A package that depends on itself, directly or not, is N0327. A missing directory or manifest is N0322. Nothing runs during resolution except `git` (ADR 0013 D5). ### 4.3 The lock file `nova.lock`, beside `nova.toml`, records the resolution so that every build uses the same code: ```toml # Written by novac fetch. Commit it with nova.toml. [[package]] name = "lz4" source = "git+https://example.org/lz4-nova.git" commit = "3f2a9c0d…" # the full commit id hash = "sha256:…" # of the checkout's manifest and sources [[package]] name = "geometry" source = "path+../geometry" ``` - `novac fetch` resolves every dependency, fetching git repositories and resolving tags and branches (`git ls-remote`, `git fetch`), and writes the lock. A `rev` is used as written; a `tag` or `branch` is the commit it names at that moment, until the next `novac fetch`. - Every other command reads the lock. It fails with N0324 if the manifest has a dependency the lock lacks or records differently (run `novac fetch`), and with N0325 if a git checkout's manifest and sources no longer have the lock's hash. A missing checkout of a locked commit is fetched, unless `--offline` is given (N0323 if it cannot be). - A path dependency has no hash: it is code being worked on beside the package. - The hash is SHA-256 over, for the manifest and each source file in ordinal order, the path relative to the package root, a zero byte, the file's bytes and a zero byte. ### 4.4 Commands - `novac new ` makes `/nova.toml` and `/src/main.nova` (a `main` that prints a line); `--lib` makes a library with `src/lib.nova` instead. It also writes `.gitignore` (`out/`) and the agent instructions of §7: `AGENTS.md` by default, others with `--agent`, none with `--no-agent`. - `novac agents [--agent …]` writes or refreshes the agent instructions of the package in the current directory or above it (§7.3). - `novac fetch [--offline] [--update [name]]` resolves and writes `nova.lock` (§4.7). - `novac publish` checks that the package can be published and prints its index entry (§4.8). - `novac build`, `check`, `run`, `doc` and `fmt` without file arguments use the package of the nearest `nova.toml` in the current directory or above it. `build` writes `out/` (an executable, or `out/.o` for `object`), or the C sources with `--emit c`; the manifest's profile, `float` and C files apply, and command-line options add to them. `fmt` formats the package's own sources, not its dependencies'. - The language server (§2.1) checks a document that belongs to a package (a `nova.toml` above it, whose sources include it) with that package: the open document's text, the package's other files from disk, and its locked dependencies. Without a lock, or outside a package, the document is checked alone as before. ### 4.5 Errors | Code | Meaning | |---|---| | N0320 | `nova.toml` or `nova.lock` is not valid in the TOML subset of §4.1 | | N0321 | A manifest field is missing, unknown, or has an invalid value | | N0322 | A path dependency's directory or manifest is missing, or a dependency is named differently from its key | | N0323 | A git dependency cannot be fetched or resolved, or is needed with `--offline` and absent | | N0324 | The lock file is missing or does not match the manifest: run `novac fetch` | | N0325 | A git checkout's manifest and sources do not match the lock's hash | | N0326 | One package name reached through two different sources | | N0327 | A dependency cycle | | N0328 | An invalid version, version requirement, or index entry | | N0329 | No versions satisfy the requirements: the chain of requirements that conflict | | N0335 | `novac publish`: the package cannot be published as it is | ### 4.6 The registry (M13 G6, ADR 0017) A **registry dependency** names a package and a **version requirement**. The package is found in an **index**: a git repository, by default `https://github.com/vkuttyp/nova-index`, or `$NOVA_INDEX`, or the dependency's `index`. - **Versions** are `MAJOR.MINOR.PATCH`, three decimal numbers with no leading zeros, compared numerically. There are no pre-release or build suffixes in v1. - **Requirements** are one or more comparators separated by commas, all of which must hold: - `"1.4"`, `"^1.4"` and `"^1.4.2"` are caret requirements: at least that version, below the next major version, or below the next minor for `0.x` (`"0.3"` is `>=0.3.0, <0.4.0`) and the next patch for `0.0.x`. - `"=1.4.2"` means exactly that version. - `">=1.4"`, `">1.4"`, `"<2"` and `"<=1.9.3"` compare. A missing minor or patch is 0. - `"*"` means any version. - **The index** holds `packages/.toml`, with one `[[version]]` table per published version, in publishing order: ```toml [[version]] version = "1.4.0" git = "https://github.com/someone/geometry.git" commit = "3f2a9c0d…" # a full commit id hash = "sha256:…" # of the commit's manifest and sources (§4.3) dependencies = { shapes = "2.1" } # its registry requirements, as its nova.toml writes them yanked = false # optional; a yanked version is never chosen for a new resolution ``` Entries are only appended. A published package's dependencies are all registry dependencies, and those without an `index` come from the index that published the package. ### 4.7 Version solving `novac fetch` resolves path and git dependencies as before (§4.2), gathering the registry requirements of the root and of every path and git package. It then chooses one version of each registry package, transitively, from the index's entries: - The **newest** version that satisfies every requirement on it is chosen, and so on through its own dependencies. On a conflict, the solver backtracks. - A version the lock already records is preferred while it satisfies the requirements, so `novac fetch` changes nothing that needs no change. `--update` prefers the newest for every package, and `--update ` for that one. - **One version per package name** (OD-26). A name reached through a path or git dependency and through the registry is N0326. - If no choice satisfies every requirement, N0329 shows the requirements on the package that could not be chosen, and where each came from: `no version of 'shapes' satisfies ^2.1 (from geometry 1.4.0) and ^1.0 (from plot 0.3.0)`. - Each chosen version's commit is checked out as for a git dependency (§4.2). Its hash must be the index's (N0325), and its manifest's name and version must be the entry's (N0328). - The lock records each as `source = "registry+"` with `version`, `repository` (the git URL), `commit` and `hash`. Other commands use the lock: an entry whose version no longer satisfies the manifest's requirement is N0324. ### 4.8 Publishing `novac publish` (no network but `git`, and nothing written) checks the package in the current directory: - it is a library with a valid `version`; - every dependency is a registry dependency; - it type-checks with its dependencies; - its git working tree is clean, and its commit is on a remote branch. It then prints the index entry for the version: the remote's URL, the commit, the hash and the requirements. To publish, add that entry to `packages/.toml` in the index by pull request. The index's continuous integration runs `novac index-check`, which checks every new entry: - the commit exists; - its manifest has the entry's name and version; - its hash and dependencies are the entry's; - the version is newer than every earlier entry. A new package name is reviewed by the index's maintainers. Later versions from the same repository merge after the checks pass. ## 5. Diagnostics for programs (M12 A1) People read diagnostics as text (`diagnostics.md`, "Format"). Programs, AI agents and editors among them, read the same diagnostics as JSON, and can apply the fixes the compiler is sure of. ### 5.1 `--diagnostics-format=json` `check`, `build`, `run` and `parse` accept `--diagnostics-format=json` (or `=text`, the default). With `json`, diagnostics are not written to standard error; instead one JSON document is written to standard output, as one line, after the command's own work. `build` still writes its outputs, and `run` still runs the program, if there are no errors; the program's own output then precedes the document. The exit status is unchanged. ```json { "version": 1, "errors": 1, "warnings": 0, "diagnostics": [ { "code": "N0249", "severity": "error", "file": "main.nova", "start": { "line": 7, "column": 10 }, "end": { "line": 7, "column": 11 }, "message": "this parameter is 'borrow mut': write 'mut' before the argument to let the call modify it", "summary": "`mut` argument marker missing or misplaced", "notes": [ { "file": "main.nova", "start": { … }, "end": { … }, "message": "…" } ], "fixes": [ { "title": "write 'mut' before the argument", "edits": [ { "file": "main.nova", "start": { "line": 7, "column": 10 }, "end": { "line": 7, "column": 10 }, "newText": "mut " } ] } ] } ] } ``` - `version` is the format's version: fields may be added within a version, never removed or changed. - Positions are 1-based lines and columns, columns in UTF-16 code units, as in the text form. `end` is exclusive. - `file` is the path as the text form prints it. - `summary` is the code's "Meaning" in `diagnostics.md`, which `novac` embeds. - Diagnostics are in the order the text form prints them. Keys are written in the order shown. ### 5.2 Fixes A fix is a list of edits that the compiler is sure resolves the diagnostic. It is offered only where the intent is unambiguous; a diagnostic with several plausible repairs offers none. Applying a fix removes that diagnostic and introduces none at the edited place (checked by `FixTests` for every kind below, on every corpus case that offers it). The kinds: | Code | When | Edit | |---|---|---| | N0249 | an argument for a `borrow mut` parameter lacks `mut`, and is a place that can be borrowed mutably | insert `mut ` before the argument; if the place is a `let` local, also `let` → `var` at its declaration (one fix) | | N0249 | an argument for a shared parameter has `mut` | delete the `mut ` | | N0211 | assignment to a `let` local declared in the function | `let` → `var` at its declaration | | N0248 | `borrow mut` (or a `borrow mut this` call) of a `let` local declared in the function | `let` → `var` at its declaration | | N0208 | `e as T` between integer types that can lose information | `truncate(e)` (wrap-around, as the message says) | | N0100 | `expected ';'`, with no earlier error in the file, after a declaration, assignment, `return`, or a call statement (not after an expression that cannot be a statement, N0225) | insert `;` after the previous token | ### 5.3 `novac fix` `novac fix [FILES]` (or the package, without files) applies every fix, writes the files, checks again, and repeats until no fix applies or after 8 rounds. It then prints the remaining diagnostics, as text or with `--diagnostics-format=json`, and exits 1 if errors remain. With `--dry-run` it changes no file and prints the edits of the first round. Edits to one file never overlap: when two fixes would, the later one waits for the next round. ### 5.4 `novac explain` `novac explain N0249` prints the code's row of `diagnostics.md` (its meaning and example) and the paragraphs of `diagnostics.md` that mention it. An unknown code is exit status 2. ### 5.5 In the language server `textDocument/codeAction` returns the fixes of the diagnostics that touch the requested range as `quickfix` actions, each with its diagnostic and a `WorkspaceEdit` for the document (§2.2). A fix that would edit another file is not offered. ## 6. The MCP server (M12 A2) `novac mcp` serves the [Model Context Protocol](https://modelcontextprotocol.io) on standard input and output, so an AI agent's host (Claude Code, an IDE, any MCP client) can use Nova's compiler, interpreter and documentation as tools. It is part of `novac`, with no other dependency, and serves the documents embedded in that `novac`, so the answers always match the compiler that gives them. ```json { "mcpServers": { "nova": { "command": "novac", "args": ["mcp"] } } } ``` ### 6.1 Protocol - JSON-RPC 2.0, one message per line of UTF-8 on standard input and output; nothing else is written to standard output. The server stops at the end of its input. - `initialize` negotiates the protocol version: the client's, if it is one the server supports (`2025-06-18`, `2025-03-26`, `2024-11-05`), otherwise the newest of those. The server declares `tools`, `resources` and `prompts`, none of which change while it runs, and gives brief usage `instructions`. - `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list` and `prompts/get` are answered; other requests are `MethodNotFound` (-32601); notifications are ignored. - With version `2025-06-18`, each tool declares an `outputSchema` and its result carries `structuredContent`, with the same data as JSON text in `content`. With earlier versions, only `content`. - A tool call that the tool cannot carry out (a file it cannot read, a source that does not parse when it needs to) is a result with `isError: true` and the reason. Unknown tools and invalid arguments are `InvalidParams` (-32602). The server never stops on a bad request. ### 6.2 Sources Every tool that compiles takes its program as either - `files`: an array of `{ "path": "main.nova", "text": "…" }`, compiled as one package, with nothing read from disk; or - `paths`: an array of `.nova` file paths, read from disk relative to the server's working directory; plus `profile` (`hosted`, the default, or `freestanding`) and `noFloat` (default false). Paths in results are the paths given. ### 6.3 Tools | Tool | Input (besides §6.2) | Result | |---|---|---| | `nova_check` | | the diagnostics document of §5.1, with fixes | | `nova_fix` | | `files`: each file's text with every fix applied, over rounds as `novac fix` does (§5.3); `applied`: the number of fixes; `diagnostics`: what remains. Nothing is written to disk | | `nova_format` | `text`, `path` (optional) | `text`: formatted (`formatting.md`); or an error with the syntax diagnostics | | `nova_run` | `stdin` (text), `args`, `maxSteps` (default 10,000,000), `maxOutputBytes` (default 65,536) | `outcome` (`exited`, `panicked`, `fault`, `not-compiled`), `exitCode`, `stdout`, `stderr`, `truncated`, and the diagnostics if it did not compile | | `nova_emit_c` | `explain`: any of `allocations`, `bounds-checks`, `overflow-checks` | `files`: the generated C (`main.c`, and `nova_exports.h` when there is one; not the runtime); `explanations`: the `--explain-*` text | | `nova_explain` | `code` | `code`, `meaning`, `example`, `paragraphs` of `diagnostics.md` (§5.4) | | `nova_spec_search` | `query`, `limit` (default 10) | `results`: the sections whose heading or text contain every word of the query, best first: `document`, `heading`, `snippet`, `uri` | | `nova_spec_section` | `document` (`type-system`, `grammar`, …), `section` (`31.2`, or heading text) | `text` of that section, with its subsections | | `nova_symbol` | `path`, `line`, `column` (1-based) | the type or signature under the position, and its declaration's location, as hover and definition give them (§2.2) | `nova_run` runs the program in the interpreter (`run --interp`) **sandboxed**: - files, sockets and processes are refused (a fault that names the refusal); - `maxSteps` statements at most (a fault), and `maxOutputBytes` of each output kept (`truncated` says if more was written; the program goes on); - 256 MiB of interpreter memory at most: the heap then reports out of memory, which the program sees as `AllocError.OutOfMemory`; a local or static allocation beyond it is a fault; - standard input is `stdin`, the arguments are `args`, and the run has no access to the server's environment. A fault's exit code is 70 and a panic's 134, as for `run --interp`. ### 6.4 Resources Each embedded document is a resource `nova://docs/` (for example `nova://docs/docs/type-system.md`, `nova://docs/site/guide/basics.md`), `text/markdown`, named by its first heading. `resources/read` returns its text. An unknown URI is the error -32002 (resource not found). ### 6.5 Prompts | Prompt | Arguments | What it asks | |---|---|---| | `write-nova` | `task` | To write Nova for the task: the agent guide (`site/guide/for-agents.md`), then the task, with the edit-check loop of `nova_check` and `nova_fix` | | `port-c-to-nova` | `file`, `api` | To port a C file to Nova as C6 and C9 did: imported C structs from the header, safe code over slices, `unsafe` only at the exported boundary, and the C's own tests, a differential test and a benchmark (`cosmoos/c9-report.md`) | ## 7. Agent instructions in packages (M12 A4) AI coding agents read a file of instructions at the root of a project. `novac new` writes one, so that an agent working in a new Nova package knows from the start what Nova is, which commands check its work, and the mistakes to avoid. The convention follows Vite+'s (`vp create`). ### 7.1 The Nova section `AGENTS.md` holds the **Nova section**, between the lines `` and ``: what Nova is and where its guide for agents is; the package's layout; the commands (`check`, `fix`, `run --interp`, `build`, `fmt`, `explain`, `--diagnostics-format=json`); how to use the MCP server; a checklist before finishing; and the common mistakes of the agent guide. The text is embedded in `novac` (`templates/AGENTS.md`) and is the same for every package. Everything outside the markers belongs to the package's authors: novac never changes it. ### 7.2 Agents `--agent` takes one or more of these names, separated by commas or by repeating the option; the default is `agents`. Each name writes the files below; `AGENTS.md` is written whenever any agent is chosen, since the others refer to it. | Name | Agent | Instructions | MCP configuration (`novac mcp`, §6) | |---|---|---|---| | `agents` | Codex, Amp, OpenCode and others that read `AGENTS.md` | `AGENTS.md` | — | | `claude` | Claude Code | `CLAUDE.md`: `@AGENTS.md` (Claude Code's import) | `.mcp.json` | | `gemini` | Gemini CLI | `GEMINI.md`: `@AGENTS.md` (Gemini CLI's import) | `.gemini/settings.json` | | `copilot` | GitHub Copilot | `.github/copilot-instructions.md`: a copy of the Nova section | `.vscode/mcp.json` | | `cursor` | Cursor | `.cursor/rules/nova.mdc`: a copy of the Nova section, applied always | `.cursor/mcp.json` | An unknown name is a usage error (exit status 2), and so is `--agent` with `--no-agent`. The MCP configurations name the server `nova`, with command `novac` and argument `mcp`, in each tool's format: `{"mcpServers": {"nova": …}}`, or `{"servers": {"nova": {"type": "stdio", …}}}` for VS Code. ### 7.3 `novac agents` In a package, `novac agents [--agent …]` writes the chosen agents' files (default `agents`): - a missing file is written as `novac new` writes it; - in an instructions file that has the markers, the text between them is replaced with the current Nova section, and the rest is kept; a file without them gets the Nova section appended, after a blank line; a file that only imports `AGENTS.md` (`@AGENTS.md`) is left as it is; - an MCP configuration that exists is left as it is, and the command says so: it may hold the authors' other servers. It prints what it wrote and what it left. Run it after updating `novac`, so the instructions match the compiler. ## 8. The C compiler (`novac build`) `novac build` writes C and gives it to a C compiler. It needs no configuration when a usual compiler is installed. ### 8.1 Which compiler `--cc ` names it; otherwise `$CC`; otherwise: - on Linux and macOS, `cc`; - on Windows, the first of these that exists: `clang` on `PATH`; `%ProgramFiles%\LLVM\bin\clang.exe` (where the LLVM installer, `winget install LLVM.LLVM`, puts it); `gcc` on `PATH` (MinGW-w64); `cl` on `PATH`; Visual Studio's `cl` (§8.2). clang and gcc are fully supported; MSVC is best effort (`c-backend.md` §1.1). With none, the error names what was looked for and how to install one. ### 8.2 Visual Studio's environment (Windows) clang on Windows and `cl` compile against Visual Studio's C library and the Windows SDK, which they find through the environment that Visual Studio's developer prompt sets (`INCLUDE`, `LIB`, `PATH`). When that environment is missing (`INCLUDE` unset) and the compiler is clang or `cl`, `novac build` sets it up for the compiler, as the developer prompt would: 1. find Visual Studio, or its Build Tools, with the C++ tools for this machine: `vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64` on an ARM64 Windows, `…VC.Tools.x86.x64` otherwise (other products that `vswhere` knows, such as SQL Server Management Studio, have no C++ tools and are not taken); 2. run its `VC\Auxiliary\Build\vcvarsall.bat` for the machine's architecture (`arm64` or `x64`) and take the environment it leaves; 3. run the compiler in that environment. The user's own environment is not changed. Without Visual Studio, clang still works where the environment is already set; otherwise the error says to install Visual Studio's "Desktop development with C++" workload (or the Build Tools), or MinGW-w64. ### 8.3 Flags | Compiler | Flags | |---|---| | clang, gcc, `cc` | `c-backend.md` §1's: `-std=c11`, the strict warnings, `-ffp-contract=off -fno-math-errno`, `-O2` (or `-g -O0`), `--cflag`s, `-o `; on Windows also the 8 MiB stack, `-Wl,/STACK:8388608` for clang and `-Wl,--stack,8388608` for gcc | | `cl` (MSVC) | `/nologo /std:c11 /W3 /O2` (or `/Zi /Od` with `-g`), `--cflag`s, `/Fo:` the build directory (so no `.obj` lands in the project), `/Fe:`, `/link /STACK:8388608` (`c-backend.md` §1.1) | A compiler is MSVC when its file name is `cl` or `cl.exe`. `--emit obj` (one relocatable object) needs clang or gcc. ### 8.4 The output The executable is `out/` in a package, or `-o `. On Windows, a path without an extension gets `.exe`, so `novac build` makes `out\hello.exe`. The output's directory is created if it does not exist. # C Backend Design (v0) Status: Milestone 0 proposal. Applies from M1. CosmoOS-specific flags are covered in `cosmoos/abi.md` and `cosmoos/build-integration.md`. ## 1. Target dialect - **C11** (ISO/IEC 9899:2011), compiled with `-std=c11` (or `gnu11` where the host project requires it). Generated code must also compile under C17. - Headers used by generated code: **only** freestanding headers (``, ``, ``, ``, ``, ``) plus `nova/rt.h`. Everything hosted lives behind runtime functions. - Compiler-specific features are allowed only through macros in `nova/rt.h` with a portable fallback: `__builtin_*_overflow`, `__builtin_expect`, `__attribute__((noreturn, cold))`, `_Static_assert`. - Supported compilers: clang ≥ 15 (CosmoOS's floor; its CI uses Debian clang 19.1.7) and gcc ≥ 11 for hosted builds. **CosmoOS profiles are clang/lld only** (`research/cosmoos-platform-audit.md` §3). MSVC is best-effort for the hosted profile (C11 mode, VS 2022), without the kernel profile. - The generated C must compile warning-free under `-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-conversion -Werror` **and** under CosmoOS's own warning set (`-Wcast-qual -Wmissing-prototypes -Wstrict-prototypes -Wvla -Wundef -Wimplicit-fallthrough`, `-Wframe-larger-than=2048` for the kernel; see `cosmoos/abi.md`). All non-exported functions are `static`, and every exported one has a prototype in the generated header. - Correct both with and without `-fno-strict-aliasing`. CosmoOS compiles with it, but Nova does not rely on it. - **No floating-point contraction or fast-math** (M11 G7, type-system.md §35.6). `rt.h` turns contraction off where a pragma can (clang, MSVC) and requires `FLT_EVAL_METHOD == 0`, in programs that use floats (`NV_FLOATS`; a program without floats compiles under `-mgeneral-regs-only`); builds pass `-ffp-contract=off` to clang and gcc and never `-ffast-math` or its parts, so each float operation rounds once, as in the interpreter. - **No compiler-rt / libgcc.** CosmoOS links no builtins library, so generated code must never need one. That rules out `__int128` division, `long double` arithmetic, 16-byte atomics and, in AArch64 userland, outline-atomic helpers (to be verified in C1; pass `-mno-outline-atomics` if needed). The runtime header supplies portable fallbacks instead of builtins when a builtin would lower to a libcall. - `-nostdinc` builds: freestanding headers come from `-isystem $(clang -print-resource-dir)/include` (CosmoOS invariant B2). Generated code must not include libc headers directly; only `nova/rt*.h` does, and only in hosted and userland profiles. - **Runtime per profile (M8c, `cosmoos/profiles.md` §4.2).** Hosted programs get `nova/rt.h`, the core runtime `nova_rt_core.c`, and the platform layer `nova_rt_hosted.c` / `nova_rt_io.c` (C library calls). A hosted program that prints a float also gets `nova_rt_float.c`, the float formatter and its table (type-system.md §35.7; M11 G7b). Freestanding programs get `nova/rt.h` and `nova_rt_core.c` compiled with `NV_FREESTANDING` (a `#define` at its top), which leaves out everything that needs a platform service; the environment defines `nova_panic`. Freestanding builds add `-ffreestanding -fno-stack-protector -fno-asynchronous-unwind-tables -fno-unwind-tables` (the stack protector's `__stack_chk_*` symbols come from the C library; unwind tables are useless with panic = abort and have no unwinder to read them in a kernel). - Plain C `char` is **never** used for Nova data (`char` is signed on x86-64 and unsigned on AArch64; CosmoOS's `limits.h` is also wrong about it on AArch64). Bytes are `uint8_t`. ## 1.1 MSVC, best effort (M7e) `novac build --cc cl` uses it, and so does `novac build` on Windows when no clang or gcc is found (`tooling.md` §8). The hosted profile also builds with Microsoft's C compiler: `cl /std:c11 /W3 /WX`, checked on x64 (CI) and ARM64 (a Windows 11 VM). Programs are linked with an 8 MiB stack (`/link /STACK:8388608`), as Linux and macOS give the main thread. Windows' default is 1 MiB, which `run-pass/deep-recursion.nova` overflows. This is **best effort**: no CosmoOS profile uses MSVC, and the strict warning set (§1) is checked with clang and GCC only. What differs: - **`rt.h`** uses compiler builtins only behind a test for GCC or clang, with portable code otherwise: - checked arithmetic falls back to the portable overflow checks, as with `NV_NO_BUILTIN_OVERFLOW`; - counting leading zeros (stable vectors) uses `nv_clz64`, a shift loop off GCC and clang; - `NV_UNUSED` and `NV_COLD` are empty; - `nv_max_align_t` stands for C11's `max_align_t`, which MSVC's `` lacks in C mode. There it is `double`. The generated alignment assertions use it. - **Process behaviour** (`nova_rt_hosted.c`, under `_WIN32`): - standard output and standard error are switched to binary mode at the first write, so `"\n"` stays one byte; - a panic ends with exit status 134 (`_exit(134)` after flushing), the status `abort()` gives on Linux and macOS. Windows' own `abort()` would give 3. - **I/O** (`nova_rt_io.c`, under `_WIN32`): - program arguments work as elsewhere; - files use the C runtime's `_open`, `_read`, `_write`, `_close` and `_unlink` in binary mode; - `Process.run` uses `_spawnvp(_P_WAIT, …)`; - **TCP is not supported**: after the same address check as elsewhere, every socket call fails with `ENOSYS` (`IoError.Other`). - **Test.** `MsvcTests` checks three things: - every run-pass and run-fail program builds with MSVC and gives the corpus expectations; - `nwc` reads files and standard input; - a program runs `cmd /c exit 7` with `Process.run`, sees a missing program fail, and sees TCP reported as unsupported. No corpus program needs an exception: `io-errors.nova` passes only invalid addresses, which fail the address check first. The `test-windows` CI job runs the tests on x64. Locally, a Windows 11 ARM64 VM (MSVC 19.51) runs them. On other hosts they return early. ## 2. Lowering of types | Nova | C | |---|---| | `i8..i64`, `u8..u64` | `int8_t..int64_t`, `uint8_t..uint64_t` | | `isize`/`usize` | the fixed-width type of the target's pointer width (`int64_t`/`uint64_t` on LP64), plus `_Static_assert(sizeof(void *) == 8)`. `ptrdiff_t`/`size_t` are used at FFI boundaries (M8) | | `bool` | `bool` (`_Bool`) | | `char` | `uint32_t` | | `borrow fn(T…) -> R` (M11 G8b) | `const nvCK_ *`, a pointer to the first member, the code pointer `R (*)(const void *, T…)`, of an environment `nvCE_` that holds a pointer per capture; the body is `nvCB_`, which converts `env` back through `uintptr_t`; a call is `(*f)(f, args…)` (type-system.md §36.2) | | `borrow dyn I`, `borrow mut dyn I`, `Box` (M13 G2) | `nvDR_` = `struct { void *data; const nvDT_ *vt; }` (16 bytes; the dyn type mangles as `D`, `dyn Shape` → `D5Shape0`), typedef'd before every struct, so any struct may hold one. Not a niche optional: `Box?` has a tag. The table type `nvDT_…` is defined after the structs: `drop` (NULL if `T` needs none), `size`, `align`, then `m0…` in the interface's method order, each taking `void *` for the receiver. Per (`T`, `I`) pair that a coercion names: `static const nvDT_… nvVT__`, with adapters `nvVT_…_` that convert the address back through `uintptr_t` and call `T`'s method, and `nvVT_…_drop` over `T`'s drop glue. A coercion is `((nvDR_…){ .data = (void *)(uintptr_t)p, .vt = &nvVT_… })`; a call through `x` is `x.vt->m(x.data, args…)`; borrowing `*x` is `x` itself. `Box`'s drop glue calls `vt->drop` and frees `data` with `A` (a user allocator's `deallocate` gets the table's size and alignment) (type-system.md §37.4). **`dyn fn`** (M13 G3, §36.5) mangles as `DF` (`DG` for `dyn mut fn`), its effect list and its signature; its table's one entry `m0` is a move literal's body, whose `self` is the boxed environment `nvCE_` (the code pointer, then the captured values; drop glue `nvD_CE_`) | | `fn(T…) -> R` (M11 G8a) | a typedef `nvN_` of a pointer to the functions' C signature; a value is a function's address, a call an indirect call (type-system.md §36.1) | | `f32`/`f64` | `float`/`double` (`rt.h` checks the IEEE formats; literals by their bits through `nv_f32_from_bits`/`nv_f64_from_bits`; float→integer conversions through range-checked helpers; type-system.md §35). The CosmoOS kernel forbids floats (`cosmoos/profiles.md`; `--no-float` in G7c) | | `struct S` | `typedef struct nvT_S nvT_S;` then `struct nvT_S { … }` with fields `f_` in declaration order, structs emitted after the structs they contain. Each struct is followed by `_Static_assert`s on `sizeof`, `_Alignof` and every field's `offsetof`, against `TypeTable.LayoutOf`. An empty struct gets a `uint8_t nv_empty` member (C has no empty structs). `@repr(C)` is the default in v0; field reordering is an M9 opt-in (`@repr(auto)`) | | `Box` with a user allocator (C5, type-system §30) | `T *` as below; `nvB_` calls `A`'s `allocate` method with `{ sizeof(T), _Alignof(T) }` (a `{ 0 }` value of the field-less allocator struct as `this`), and the drop glue and `BoxFree` call `nvBF_`, which calls `deallocate` with the same layout | | `Box` (M3b) | `T *` (a shared reference to a box is `T *const *`). Drop glue `nvD_X` destroys `*p`, then calls `nova_free(p, NV_ALLOC_SYSTEM \| NV_ALLOC_FAILING)`. `Box.create`/`createIn` call a per-type helper `nvB_X(T v)`: `nova_alloc(sizeof(T), id)`; on NULL it destroys `v` and returns `Err(OutOfMemory)`, otherwise it stores `v` and returns `Ok(p)`. It asserts `_Alignof(T) <= _Alignof(max_align_t)`. **Built in place** (M9e′): when the argument is a temporary assigned by the statement just before, by a construction (an array repeat, an array, a struct or enum aggregate), and used nowhere else, and `T` needs no drop and holds no C state, the site calls `nvBA_X()` (the allocation alone: `p` or NULL) and then writes the construction into `*p`, with no temporary and no by-value copy. The construction's operands are already evaluated, so it cannot panic or allocate, and the order of allocation and construction is unobservable; `T` has no drop, so a failed allocation leaves nothing to destroy. A stack-placed box (§2.2) is likewise built in its slot | | Deep boxes (M3c) | If destroying `T` ends, as its last step, in a `Box` (followed through struct fields and enum variants' last payloads), the glue of `Box` is a loop: destroy the node except that tail (`next = tail pointer`), `nova_free` the node, continue with `next` (type-system.md §18.5). `Unpack` becomes one assignment per target; `BoxFree` is `nova_free` | | `Arena` (M4a) | `nv_arena` from `nova/rt.h` (40 bytes, asserted there), created by `nova_arena_create(allocator, chunk)`, dropped by `nova_arena_drop`. A shared reference to a region is a plain `nv_arena *`: allocation mutates the arena through a shared borrow. `alloc`/`allocArray` call per-type helpers `nvAA_`/`nvAR_`, which bump-allocate with `nova_arena_alloc` (chunks come from `nova_alloc`, so they are counted and fault-injectable) and return the scoped result. Element names: `P`/`Q` + T for shared/mutable references, `U` + T for `[T]`, `G` for regions | | `FixedBufferAllocator`, `ScratchFrame` (M4b) | `nv_fba` (24 bytes) and `nv_scratch` (32 bytes) from `nova/rt.h`. Allocation helpers take `(void *region, uint32_t kind, …)` and call `nova_region_alloc`, which dispatches to the arena, the fixed buffer, or the runtime's scratch stack (heap chunks via `nova_alloc`; a frame's drop frees the chunks newer than its entry state). `Scratch.enter()` is `nova_scratch_enter_root(loc)`, which panics if a frame is live; `frame.enter()` is `nova_scratch_enter_child`. A frame's borrow-checker parts (`memory`, `allocation`) are places of the frame itself in C | | `Pool`, `PoolBox` (M4c) | `nv_pool` (48 bytes; `nova_pool_create`/`nova_pool_take`/`nova_pool_release`/`nova_pool_drop`; slots of one size, allocated at the first take, with a free list through them). `PoolBox` is `nvPB_` = `struct { T *ptr; nv_pool *pool; }`, and its contents are `(*b.ptr)`. Its drop glue `nvD_Y` destroys `*ptr`, then releases the slot; `BoxFree` of a partially moved pool box releases only. `pool.take` calls `nvPT_(pool, v)`, which destroys `v` when no slot is free | | Interfaces (M6b) | No runtime representation: dispatch is static. An `impl` block's methods are methods of their type, named like other members (`impl Comparable for i32` → `nvM_3i32_compare`). Calls through a bound in generic code exist only in generic versions, which are not emitted; each instantiation calls the concrete method | | Generic code (M6a) | Generic versions are type-, move- and borrow-checked but never emitted. Each instantiation is a separate C type or function. Struct and enum instances are `nvT_`/`nvE_` + ``, using the element codes (`Pair` → `nvT_4Pair1i32`; `S4Pair1i32` inside other names). Generic function instances are `nvG_` (`id>` → `nvG_2id1S4Pair1i32`). Members of an instance are `nvM__`. Every non-generic form starts with a letter or a length, so the forms cannot collide | | `Result` (M3a) | like an enum, named `nvR_` with the element codes below (`Result` → `nvR_S3ResE4Code`, `void` → `v`). Its drop glue is `nvD_R`. `e!` on `Err` calls `nova_panic(NV_PANIC_UNWRAP, loc)` | | `enum E { A, B(T) }` | `struct { uint32_t tag; union { T B; } u; }`. The tag width is the smallest unsigned that fits, unless `@repr(u8)` etc. A value without a payload is `((E){ .tag = n })`. A value with a payload of scalars and field-less enums is a compound literal too. A variant whose payload may hold a pointer (anything else) has a constructor `static inline nvMk__v_(payload…)`, defined after the type, which zero-initialises the value and stores the tag and each payload field; its values are built by it, in the helpers too. Clang's static analyzer loses a pointer read back from a union built by a compound literal, so a `Box`'s allocation looked leaked (M14, ADR 0025 §7). The constructors are kept off scalar payloads because gcc inlined less around them (`string-building` ran 1.8× slower with one on its cold growth path, inferred from inlining). Static initialisers (§39) keep compound literals, which C requires there | | `T?` (M6c) | like an enum with variants `null` (tag 0) and `Some` (tag 1, field `f__0`), named `nvO_` with the element codes (`i32?` → `nvO_i32`; `O` inside other names). A niche optional (type-system §22.1, M9e′) is `struct nvO_ { T f__0; }`: its tag is `(o.f__0 != NULL)`, `Some`'s payload is `o.f__0`, and `null` is `{ .f__0 = NULL }`; drop glue `nvD_O`. `o!` on `null` calls `nova_panic(NV_PANIC_UNWRAP_NULL, loc)`; `o == null` compares the tag with 0. The niche layout for `Box` and references is future work (type-system.md §22.1) | | `Vector`, `String` (M6d) | `nvV_<[T] or Z>` = `struct { T *ptr; uint64_t len; uint64_t cap; }` (24 bytes, asserted), element code `V or Z>` (`Vector` → `nvV_Ui32S10SystemHeap`). An empty vector points to the runtime's `nova_vec_empty` (a `max_align_t` object, never dereferenced) with `cap == 0`, so slices of it never do arithmetic on NULL. Growth is `nova_vec_grow` (doubling, at least 4; a first buffer is allocated, a later one resized with `nova_realloc`, which keeps the old bytes; NULL with nothing changed on overflow or allocation failure). Push and append call it only when the buffer is full, and pass the capacity through a local, so the vector's address does not escape the helper and the C compiler can keep its length in a register. Per type: `nvVP_` push (destroys the value on failure), `nvVO_` pop, `nvVC_` clear, `nvVA_` append (strings; it copies a text of up to 16 bytes with a byte loop and a longer one with `nova_mem_copy`, since a `memcpy` of a length unknown to the C compiler is a library call for gcc, also for one byte), `nvVW_` withCapacity; drop glue `nvD_V…` destroys the elements in index order, then frees the buffer if `cap != 0`. The elements place `*v` is the vector itself, so elements are `v.ptr[i]` and views are rebuilt from `ptr` and `len` | | `SlotMap`, `Handle` (M6e) | `nvK_` = `struct { nvKS_… *ptr; uint64_t len, cap, count; uint32_t free; }` (40 bytes) with slots `nvKS_…` = `{ uint32_t gen; uint32_t next; T value; }`; element code `K`. `Handle` is `nv_handle` = `{ uint32_t index; uint32_t gen; }` from `nova/rt.h` (code `H`), compared with `nova_handle_eq`. Generations are odd while a slot is live; a removal whose generation reaches `UINT32_MAX - 1` retires the slot (never put back on the free list), so generations never wrap. Helpers `nvKI_` insert (grows the slots with `nova_vec_grow`, at most `UINT32_MAX - 1` slots), `nvKV_` validity, `nvKG_`/`nvKM_` get (an optional pointer), `nvKR_` remove; drop glue destroys values in odd-generation slots | | `StableVector` (M6e) | `nvSV_` = `struct { void *chunks[NV_SVEC_CHUNKS]; uint64_t len; }` (264 bytes); element code `W`. Chunk `k` holds `8 << k` elements and is allocated by `nova_svec_slot` when its first element is pushed; chunks never move or shrink. Element `i` is `(*nvSVE_…(&sv, i))` (`nova_svec_at`: chunk `60 - clz(i + 8)`). A shared reference to a stable vector is a non-const pointer, since `push` mutates through it. Drop glue destroys the elements, then `nova_svec_free` | | `File`, `TcpListener`, `TcpStream` (C3) | `int32_t` descriptors; drop glue is the runtime's `nova_fd_drop` (close). I/O built-ins call `nova_*` functions declared in `nova/rt.h` and implemented in `runtime/hosted/nova_rt_io.c`; each returns `nv_io_result { value, error }`, and a per-type helper `nvIOR_` turns it into `Result` (written only for an `X` an I/O built-in returns: `void`, an integer or a descriptor) (the runtime maps errno to the variant; `Other` keeps the number). The generated `main` is `int main(int argc, char **argv)` — the one place plain `char` appears — and passes argv to `nova_rt_args` as bytes | | `StringView` (M6c) | `nv_str` = `struct { const uint8_t *ptr; uint64_t len; }` from `nova/rt.h` (16 bytes); element code `PZ` (`Z` is the unsized `str`). A literal is `((nv_str){ .ptr = (const uint8_t *)"…", .len = n })`, pointing to static storage. `s[lo..hi]` calls `nv_check_str_range` (range, then character boundaries; `NV_PANIC_STR_BOUNDARY`); `==`/`!=` call `nova_str_eq`; `s.bytes()` rebuilds the struct as `nvSl_u8`; printing calls `nova_write_view` | | `*T`, `*mut T` (M8a) | `T *` for both, `void *` for `*void`: read-only is a Nova rule, and casting `const` away would trip `-Wcast-qual`. Element codes `N` / `M`. Every conversion to or from a pointer goes through `uintptr_t` (`(U *)(uintptr_t)p`), and `add`/`sub`/`offset` compute the address in `uintptr_t`, so no C pointer arithmetic or pointer-to-pointer cast is ever emitted; `p.read()` / `p.write(v)` are the place `(*p)`; `s.ptr` is the slice's `ptr` field; `Slice.fromRaw` builds the slice struct (`unsafe.md` §4) | | `borrow T` | `const T *`. `borrow mut T` → `T *` (`restrict` only where the borrow checker proves exclusivity *and* M9 enables it). `Ref(k, p)` is `&p`, a deref place is `(*r)`, and reference locals are zero-initialised to `NULL` (never read before assignment) | | `Slice` / `SliceMut` | `nvSl_` = `struct { const T *ptr; uint64_t len; }` and `nvSm_` = `struct { T *ptr; uint64_t len; }` (M2d; size 16, align 8, asserted). Slices are only made from arrays (N ≥ 1), so `ptr` is never NULL; `a[n..]` is the one-past-the-end pointer with length 0. An element is `s.ptr[i]`; a reborrow or `SliceMut`→`Slice` coercion rebuilds the struct from `ptr` and `len` | | `Box` | `T*` when `A` is zero-sized; `struct { T* ptr; A a; }` otherwise | | Arrays `[T; N]` | `nvA_` = `struct { T e[N]; }`, so assignment, parameters and return by value work (M2d). An element is `a.e[i]`; `[v; N]` is a `for` loop over `e`; zero-initialised with `{0}`. Element names: integers and `bool` by name, `S` for structs, `E` for enums, `A_` for arrays (`[[i32; 3]; 2]` → `nvA2_A3_i32`) | | Function pointers | C function pointer types via typedef | Names (as implemented in M2a): - functions: `nv_`; - struct members: `nvM__`, where the length prefix makes the boundary unambiguous; - struct types: `nvT_`; - fields: `f_`; - locals: `v__` and `t_`. The prefixes keep the namespaces apart and avoid C keywords. Planned: package/namespace qualification, plus a stable hash of the type arguments for monomorphised instances (not .NET `GetHashCode`). `@export("sym")` overrides the name. Arrays and slices are defined with the structs, each after the types it contains by value; slices come after their element types. Every array and slice type the program uses is emitted, in the order the checker created it. Bounds checks (M2d) call `nv_check_index(i, len, loc)` and `nv_check_range(lo, hi, len, loc)` from `nova/rt.h`; they panic with "index out of bounds" and "slice range out of bounds". A constant index below a constant length, and a constant range within a constant length, are not checked. Since M9c, an index check that bounds-check elimination proved cannot fail is not emitted either (`optimisations.md` §2; `--no-bce` keeps them all). Likewise `+`, `-`, `*` and negation proved not to overflow are written without their helper, as `(T)(a + b)` (`optimisations.md` §3; `--no-oce` keeps them all). `d.swap(i, j)` (M11 G8c), after its two index checks, exchanges the elements through a temporary of the element's C type, which moves them whatever the type. `v.truncate(n)` is an inline block that lowers the length and calls the element type's drop glue on each removed element, lowest index first. Bulk slice operations (M9b, type-system §16.5): `d.copyFrom(s)` is `nv_check_length(s.len, d.len, loc)`, which panics with "slice lengths differ" (`NV_PANIC_LENGTH`), then `nv_slice_copy(d.ptr, s.ptr, d.len, sizeof *d.ptr)`, which calls `nova_mem_move` unless the length is 0 (C11 leaves `memmove` on a null pointer undefined even for 0 bytes, and a slice from raw parts may have one). `d.fill(v)` is `nv_slice_fill_byte`, a `nova_mem_set`, for a one-byte integer or `bool` element, and otherwise an element loop in the generated C, which the C compiler may itself turn into `memset`. `nova_mem_move` and `nova_mem_set` are `__builtin_memmove` and `__builtin_memset` under clang and gcc, which may lower to calls to `memmove` and `memset`: both are in the freestanding allowlist (`cosmoos/profiles.md` §4.4). Under MSVC they are ``'s functions. Layout verification: for every emitted struct the backend emits `_Static_assert(sizeof(S) == N && _Alignof(S) == A, "...")` and `offsetof` assertions, where `N`/`A` come from `TargetInfo`. If the C compiler for the actual target disagrees with Nova's model, the build fails, and nothing miscompiles silently. The message names the Nova type. It is an unevaluated string, which may not contain numeric escapes (C23; clang rejects them in every mode), so `?` is written `\?` (otherwise `Box??` would contain the trigraph `??)`), and any byte outside printable ASCII becomes `_` (M9a). ## 2.1 C names (M7d) Generated code and the runtime share one C namespace, so their names are kept disjoint by prefix: | Owner | Names | |---|---| | **Runtime** (`nova/rt.h`, `nova_rt_*.c`) | `nova_*`; `nv_` followed by a lowercase letter (`nv_add_i32`, `nv_check_index`, `nv_str`, `nv_loc`); `NV_*` | | **Generated** | `nv` followed by a capital letter: `nvF_name` (top-level functions), `nvM_` (members), `nvG_` (generic instances), `nvXS_` (variant `n` of the function named `` made by closure-call specialisation, optimisations.md §5), `nvT_`/`nvE_` (types), and the helper families (`nvD_`, `nvB_`, `nvVP_`, …); locals `v__` and `t_`; fields `f_`; variants `v_`; labels `bb`; files `nv_file_`; source locations `nvL_` (§6) | **Packages** (M10 T5, ADR 0013 D4): an item of a dependency, rather than of the package being built, has its package's name in its C name: `nvF_geometry_area`, `nvT_geometry_Point`, `nvM_14geometry_Point_norm`. The package name is part of the item's name as the backend sees it (`geometry.Point`, written with `_` in C), so every rule above applies unchanged, and two packages may declare the same name. Until M7d, top-level functions were `nv_`. A Nova function named `add_i32` then collided with the runtime's `nv_add_i32`, and the C failed to compile. The random program generator found this, and `run-pass/runtime-names.nova` keeps it fixed. ## 2.2 Stack-placed boxes (M5; on by default since M9e, `--no-placement`) When `CEmitOptions.Placement` is on (the default since M9e, `optimisations.md` §4), `PlacementAnalysis` decides each `Box.create` site (`memory-model.md` §5.1). - **Slot.** A stack-placed site has a slot `NV_UNUSED T nv_box_ = {0};` among the function's locals. A site in a loop reuses its slot in every iteration; the analysis places it only when the previous box is gone by then. - **Creation.** `R = nvB_…(v)` becomes `nv_box_ = v;` (or the construction written into `nv_box_`, as for a box built in place, §2) followed by `R = nvMk__v_Ok(&nv_box_);`. Nothing is allocated, so nothing can fail. - **Destruction.** - A `Drop` of a box local of the site calls the contents' drop glue `nvD_T(b)` if `T` needs drop, and nothing else. A drop flag still guards it. - A `BoxFree` of one emits nothing. Everything else is unchanged. Heap boxes, and every other site, use the drop glue of §2. ## 2.3 C interoperability (M8b; type-system.md §28) - **Extern functions.** Each `extern "C"` function gets a prototype with the C types of §28.3 (`size_t strlen(const char *);`) and a `static` wrapper `nvC_` with Nova's own C types, which converts the arguments (`(size_t)n`; a `T *` converts to `const T *` implicitly), calls the function and converts the result back (a `const T *` through `uintptr_t`; a `@repr` enum through a check that panics with `NV_PANIC_FFI_ENUM`). Nova code calls the wrapper like any function. The prototypes use C's own types so that they are compatible with the C library's declarations: the generated code includes no C library header (unless a `@header` names one), but clang and GCC know the library functions' types (`-Wincompatible-library-redeclaration`, `-Wbuiltin-declaration-mismatch`), and C11 6.5.2.2 makes a call through an incompatible type undefined. - **Exports.** Each `@export` function gets a non-`static` wrapper under its symbol, with the C types, that converts the arguments (checking `@repr` enums), calls `nvF_`, and converts the result. Nothing else in the generated code has external linkage except `main`. - **`@repr(C)` structs** are `struct S` with members named as in Nova (other structs are `struct nvT_S` with members `f_`), and `typedef struct S nvT_S;`, so generated code uses them like any struct, and C code that includes `nova_exports.h` sees the same type (C11 6.2.7 makes struct types in two translation units compatible only if their tags and members match). - **`@repr(uN)` enums** keep the internal form `struct { uintN_t tag; }` (the tag has the repr's width and signedness); wrappers convert to and from the integer. - **`nova_exports.h`**: an include guard; ``, ``, ``; each `@repr(C)` struct (after the structs it contains); each `@repr` enum as `typedef E;` and constants `E_Variant`; each export's prototype. It is generated only when there is something to declare. - **Imported C types (C6b, type-system.md §31)**: `main.c` and `nova_exports.h` `#include "…"` each `@header` after `nova/rt.h`, once, in declaration order; they are the only headers generated code includes besides the runtime's. An imported type has no definition, typedef or layout assertion in generated code: it is its C spelling (`struct name` or the `@ctype` name), and a field place is a member access. Pointer, `usize` and `isize` fields convert through `uintptr_t` or a cast when read and written, as wrappers do. For each declared field, two `_Static_assert`s (on `_Generic` of the member, with the field's C type, and on its `sizeof`) make a header that disagrees a C compile error that names the struct, the field and the Nova declaration's file and line. - **C arrays in imported types (C9, ADR 0014)**: a field `f: [E; N]` is the member seen as Nova's array struct, `(*(nvA_ *)(uintptr_t)&p->f)`: no copy, and every array operation works on it. Its checks are `_Generic(&member, CE (*)[N]: 1, default: 0)` (the member itself would decay to a pointer) and equal `sizeof` and `_Alignof` with the array struct. A `@cname` field uses its C name in both. An array of an imported type is emitted as an array struct of the C type, with no layout assertion (C lays it out), and `add`/`sub`/`offset` on a pointer to an imported type scale by C's `sizeof`. - **Exported constants (C9, type-system.md §28.4)** are read-only C data, `const CE symbol[N] = { … };` or `const CT symbol = v;`, each after an `extern` declaration of itself (so `-Wmissing-variable-declarations` is quiet), and declared `extern` in `nova_exports.h`. Nova code uses the constant's value, not the object. - **C sources** given to `novac build` are copied unchanged to `c/` beside `main.c` and compiled with it (`-I` names the directory, so `#include "nova_exports.h"` works). - **Function pointers (M8d, §29.5)** are `typedef R (*nvP_)(A, …);` with the C types of §28.3 (element code `F`), declared after the struct names and before the structs. A value is the extern function's name or the export's symbol (exports are declared before every function); a call converts its arguments and result like an extern wrapper. `nova_exports.h` declares the struct names first, then the function pointer types. - **`lib/core` (M8d, §29.2)** is compiled with every package, but only the functions that emitted code calls, the types it mentions, and their files' names are emitted, so a program that uses none of it gets the same C as before. - **Volatile accesses** (§29.1) are `(*(volatile T *)p)`; `@align(n)` (§29.4) is `_Alignas(n)` on a struct's first member; `Cpu.spinHint()` and the target queries are `nova/rt.h` inlines. ## 2.4 Split output (M14 K2a, ADR 0022) `novac build --split` writes the program's C in several translation units, so a C compiler can build it in parallel and rebuild only what changed. Without it the output is one `main.c`, as above, byte for byte. - **`nova_program.h`**, included by every unit, holds what one `main.c` has before its first function body (the source-location tables, the types, the prototypes, the C extern wrappers, `dyn` tables, thread and fork-join helpers), and the per-type helpers that `main.c` defines after its functions: drop glue, the `Box`, vector, container and I/O helpers. Each of them is `static`, the per-type helpers `static inline`, so every unit has its own inlinable copy. It starts with an include guard named after the program. - **One unit per Nova source file**, named after it (`tcp.c`), with as many directories before the name as it takes to make the names distinct (`net_tcp.c`, `vfs_tcp.c`). It holds the functions declared in that file and the closures written in them. - **`nova_instances.c`** holds what is defined once and belongs to no user file: lib/core's functions, generic instances, `@export` constants, callbacks, and C's `main`. - **Linkage.** Every function defined in a unit or in `nova_instances.c` has external linkage, and its prototype in the header has no `static`. The header renames each of them with the program's name, `#define nvF_main nvF__main` (the name after the family prefix; `nvARG` becomes `nvARG_`), so two Nova programs linked into one image never define the same symbol. The program's name is `--program`, the package's, or the first file's. `@export` names are not renamed. - **Determinism.** The units, their names and each function's placement are a function of the package's file list and the program, and two builds are byte-identical. - **Inlining.** A call from one unit to a Nova function in another (lib/core's included) is not inlined without link-time optimisation. The helpers in the header are in every unit, so calls to them still inline: with the per-type helpers in `nova_instances.c` instead, `vector-growth` and `string-building` ran 1.7–1.9× slower, and with them in the header as fast as one file (Mac, 2026-10-29). - **Checked by** the run corpus's `clang -O2 split` configuration (`CToolchains`): every run-pass and run-fail program is built split, with the strict warnings, and must behave as in every other configuration. ## 3. Lowering of control flow and cleanup **Flat form** (M1; `--flat-c`): each NIR block becomes a C label. Terminators become `goto`, `switch` (SwitchInt) or `return`. It is the reference form: simple enough to check by reading. **Structured form** (M7b; the default). It is a readability pass and adds nothing that correctness depends on. One invariant makes it safe: **every block's code ends in an explicit transfer**: `goto`, `break`, `continue`, `return`, or a `_Noreturn` call. Code therefore never falls into the next block by accident. Where a block is placed changes only how the code reads, and a transfer may be dropped only where falling through provably reaches the same label. 1. **Analysis.** - Reverse postorder, dominators (Cooper–Harvey–Kennedy), and *forward predecessors*: in-edges that are not back edges, counted with multiplicity, so a switch with two cases to one block counts twice. - A *back edge* `u → h` is one where `h` dominates `u`. A retreating edge that is not a back edge makes the graph irreducible. The function is then emitted flat. Nova source has no `goto`, so this is not expected; it is a fallback. - The natural loop of a header `h` is `h` plus every block that reaches a back-edge source without passing through `h`. 2. **Placement.** Each block's code is emitted exactly once, inside the code of its immediate dominator `d`, in one of three ways: - **Inline**, at the branch to it: when it has one forward predecessor and leaves no loop that contains `d`. - **Exit follow**: when it leaves loops. Let `h` be the outermost loop header whose loop contains `d` but not the block. The block is emitted after `h`'s loop statement. One exception keeps early returns in place: a block that leaves a loop from a block other than the header itself is inlined if it has one forward predecessor and its dominator subtree is *closed* and small. Closed means no edge leaves the subtree; small means at most 2 blocks and 8 statements. - **Follow**: otherwise (a merge point). It is emitted after `d`'s own code, inside the same loops. Follows and exit follows are emitted in reverse postorder. 3. **Shapes.** - A loop header's code (with its inline and in-loop placements) becomes `for (;;) { … }`. - `NirBranch` becomes `if (c) { … } else { … }`, and `NirSwitch` becomes `switch` with one `case` body per target. - A branch to an inline block emits that block's code there. Every other branch is first emitted as `goto bbN;`. 4. **Transfer elimination**, in tree order and repeated to a fixpoint, since removing a `goto` can lengthen what falls through before it: - A `goto L` is removed when falling through reaches `L`. Falling through means passing over only labels, the end of an `if` arm, or the end of a loop body back to its own header's label. - Otherwise it becomes `continue` if `L` is the innermost loop's header. - Otherwise it becomes `break` if `L` is what falls through after the innermost loop, unless a `switch` lies in between. - Otherwise it becomes `break` if `L` falls through after the innermost `switch`. - The end of a `case` body never falls through: its transfer is kept, so no `case` falls into the next (`-Wimplicit-fallthrough`). 5. **Labels** that no remaining `goto` names are removed (`-Wunused-label`). A label at the end of a block is written `bbN: ;`. 6. **Guard clauses.** - An `if` arm that cannot complete normally is hoisted: `if (c) { A } else { B }` with `A` ending in a transfer becomes `if (c) { A } B`. Symmetrically, when only `B` ends in a transfer it becomes `if (!c) { B } A`. When both arms end in a transfer, the shorter arm is the guarded one. - An empty `then` arm is negated away. - An `else` holding a single `if` is printed as `else if`. The run corpus is built in both forms under every C configuration: structured by default, flat as the oracle in a `clang -O2 flat` configuration. Both must agree. Shape of `gcd` (structured): ```c v_x_2 = v_a_0; v_y_3 = v_b_1; for (;;) { t_5 = (v_y_3 != ((uint64_t)0)); if (!t_5) { break; } v_t_4 = nv_rem_u64(v_x_2, v_y_3, &nvL_0); v_x_2 = v_y_3; v_y_3 = v_t_4; } t_6 = v_x_2; return t_6; ``` Flat form of the same function (M1), shown in an earlier, illustrative notation: ```c int32_t nv_demo_gcd(uint64_t a, uint64_t b) { uint64_t x; uint64_t y; uint64_t t; bb0: x = a; y = b; goto bb1; bb1: if (!(y != 0u)) goto bb3; goto bb2; bb2: if (y == 0u) nova_panic_div0(NV_SRC(3, 17)); /* inserted check (statically elidable later) */ t = x % y; x = y; y = t; goto bb1; bb3: return x; } ``` Cleanup: drop elaboration has already put `Drop` calls on every exit edge, so early `return`, `?` and `break` need no special backend support. Drop flags are `bool` locals initialised to `false`. No `setjmp/longjmp` and no C++. Panics are `noreturn` calls into the runtime (`nova_panic(kind, src)`), so destructors are not run (panic = abort, OD-5). ## 4. Evaluation order and temporaries NIR has already sequenced every side effect into separate statements, so the C backend never emits an expression whose meaning depends on unspecified C evaluation order (function-argument order, `a[i] = i++`-style). Arguments with side effects are always assigned to temporaries first. ## 5. UB avoidance checklist (enforced by construction + tests) | Hazard | Strategy | |---|---| | Signed overflow | `nova_add_i32_chk(a,b,src)` via `__builtin_add_overflow`, with a portable fallback that computes in unsigned and compares. Wrapping ops compute in unsigned and convert back through a well-defined helper (implementation-defined conversion avoided by `memcpy`-free bit tricks verified per compiler) | | Division | Check for zero and `MIN/-1` before `/` and `%` | | Shifts | Check `0 <= n < width`. Left-shifting a negative signed value is done in unsigned | | Unsigned promotions | `uint8_t`/`uint16_t` arithmetic is promoted to `int`. All narrow arithmetic is performed as `uint32_t` and truncated explicitly | | Pointer arithmetic | Never form out-of-range pointers. Bounds checks compare indices before computing `ptr + i`. Raw-pointer arithmetic (M8a) is done on `uintptr_t`, and raw pointers have no ordering comparisons (`unsafe.md` §4) | | Strict aliasing | No type punning. Reinterpretation (`transmute`, unsafe only) goes through `memcpy` (a builtin; supplied by the runtime in freestanding builds) | | Uninitialised reads | Definite-init analysis forbids them. A local of aggregate type (struct, enum, array, slice, vector, …) is zero-initialised (`{0}`) in C when any part of it can be written other than by assigning it whole: a write to a field, element or payload, a borrow of it or of a part (`&x`, `x.e + i`, shared or mutable), or being an `Unpack` target. So is one whose variant payload is read (a `match` arm, an `Unpack` source): the arm for a variant the local never holds reads bytes its assigned variants do not cover, and gcc's `-Wmaybe-uninitialized` (with `-Werror`) rejects that at `-O0`, although the tag test means it never runs (found by the M9d fuzz campaign); so indeterminate members or padding are never copied. Every other aggregate local is assigned whole before each read (definite assignment, which the interpreter checks on every run), and is declared without an initializer (M9d: gcc 12 does not remove the zeroing, which was most of `fbvalid`'s gcc gap, `milestone-9.md`). Scalars and references are always zero-initialised; the C compilers drop those stores | | Null | References are never null (type system). `T?` niche checks are explicit | | Alignment | All accesses are through properly typed lvalues. `@packed` fields are accessed via `memcpy` helpers | | Lifetime of compound literals / temporaries | Not used across statements. Temporaries are named locals | | `memcpy` overlap | Moves use `memmove` when the source and destination may alias; NIR knows when they cannot | | Float→int conversion out of range (UB in C) | Only via `toIntSaturating`/`checkedCast` helpers that range-check first | | Warnings on valid programs (M7d) | Nova accepts code that C compilers warn about, and generated C must still be warning-free. `x = x;` emits nothing (`-Wself-assign`). A comparison of a place with itself is the constant `true`/`false` (`-Wtautological-compare`). So is an ordering comparison against the type's own minimum or maximum, such as `u < 0` or `x <= MAX` (GCC's `-Wtype-limits`, found in Linux CI by `FuzzTests`). A constant index past a fixed array's end, after the check that always panics, goes through `nv_opaque_index` (`-Warray-bounds`). So does a constant slice or vector index too large for any object (index × element size > 2⁶³ − 1), found by the M7e campaign (seeds 1009815, 1014252, 1016777). In the structured form, statements that print nothing are dropped, so no label ends a block (`-Wc23-extensions`). Found by the random program generator; each case is in `run-pass/warning-free.nova` or `run-fail/constant-index-past-end.nova` | | Infinite side-effect-free loops (C11 6.8.5p6 permits assuming termination) | Loops whose controlling expression is not a constant expression and that have no side effects get a `nova_loop_barrier()` (an empty `asm volatile` / volatile access) | ## 6. Runtime surface (`nova/rt.h`) Heap (M3b): - `nova_alloc(size, allocator)`, `nova_realloc(p, size, allocator)` and `nova_free(p, allocator)` go through the platform's `nova_platform_alloc`/`nova_platform_realloc`/`nova_platform_free` (hosted: `malloc`/`realloc`/`free`, which also serve CosmoOS userland). They count attempts and live allocations; a reallocation is one attempt, and on failure its block is unchanged and still live. `nova_fail_after`/`nova_fail_reset` implement `FailingAllocator`. - **Threads** (M13 G4, type-system.md §38): `runtime/hosted/nova_rt_thread.c`, linked only when the generated C calls `nova_thread_*`, `nova_mutex_*` or `nova_atomic_*`. - `nova_thread_start`/`join` run over POSIX threads, or Win32 threads on Windows. - Mutexes are allocated: `pthread_mutex_t`, or `SRWLOCK` on Windows. - The atomics are sequentially consistent: `__atomic` builtins, or `Interlocked*` under MSVC. - Thread records, environments and mutexes come from `nova_alloc`, so fault injection reaches them. - The allocation counters change with relaxed atomic adds (`NV_COUNT`), since any thread may allocate. - A `Thread` is `nvTB_ *`, a record of `{ nv_thread *thread; void *data; const void *vt; T result; }`. - `nvTS_` boxes a spawned move literal's environment and starts `nvTR_`, which calls the body through the `dyn mut fn() -> T` table and stores the result. - `nvTJ_` joins, takes the result, then drops and frees the environment and the record. A `Thread`'s drop glue joins, then drops the result. - `Thread.both(a, b)` (§38.6) is a block, not a call: it starts `nvTF_` on `a` with `nova_thread_start`, calls `b`, then joins; if the start returned `NULL` it calls `a` first, then `b`. `nvTF_` takes the closure as `void *`, converts it back to its own type and calls it, so no function pointer is called through another type. A `fn()` argument is passed as the address of a local holding it, which lives until the join. - The generated `main` calls `nova_rt_start()` (reads `NOVA_FAIL_ALLOC_AT`, `NOVA_ALLOC_REPORT` via `nova_platform_config`) and returns through `nova_rt_finish(code)`, which prints the allocation report if asked. `Process.exit` and panics skip the report. - `nova_panic(kind, src)`: `noreturn`. Provided by the profile: hosted (write to stderr + `abort()`), CosmoOS userland (same, through CosmoOS libc), kernel (calls the kernel panic routine; C5). - Checked-arithmetic helpers (`static inline`). - `nova_mem_copy/move/set` (M9b): `__builtin_memcpy`, `__builtin_memmove` and `__builtin_memset` under clang and gcc, which may become calls to the C library's or the kernel's `memcpy`, `memmove` and `memset`; ``'s functions under MSVC. Used by the bulk slice operations (§2) and by vector growth. - Allocator vtable ABI for dynamic allocators (`memory-model.md` §3.2). - Source locations (M9d). A location is an `nv_loc`: the Nova path as written to the compiler (a file table `nv_file_`, so no absolute host path is embedded unless one was given), and the 1-based line and column. Each location a checked helper (`nv_add_*`, `nv_check_index`, …) can panic with is a file-scope constant `static const nv_loc nvL_`, one per distinct location, in the order of first use; the helper takes `const nv_loc *` and reads it only when the check fails. A direct `nova_panic` passes the location by value (`NV_LOC(file, line, column)`), and `nova_panic` itself still takes `nv_loc` by value (`cosmoos/profiles.md` §4.2). Why a pointer: with the location by value, gcc 12 at `-O2` loads each check's location into registers on the path where the check passes, about half of what the checks cost in `data-oriented` (`milestone-9.md`, 9d). The table holds pointers to the file names, so it is in `.rodata` (`.data.rel.ro` in position-independent code), with absolute 64-bit relocations, which the CosmoOS module loader applies (`cosmoos/c4-report.md`). ## 7. Debugging and source maps (M7a) - **`#line` directives** map generated C back to Nova source. They are off by default and turned on by `--line-directives`; `novac build -g` turns them on and compiles the C with `-g -O0` instead of `-O2`. - With directives on, each function body maps to Nova lines: - the function's opening line maps to the line of its name; - each statement and terminator maps to the first line of the source construct it came from (its span). A directive is written only when the line or file changes. Code without a span of its own, such as compiler temporaries, keeps the line before it. - A block's label shares the line of the block's first statement, so labels need no directive of their own. The local declarations share one line, attributed to the function's name. - After each function, `#line "main.c"` hands the following lines back to the C file. Everything outside function bodies (types, drop glue, helpers, `main`) is attributed to `main.c`. - **Paths.** Directives and panic locations name a source by its *source path*. This is the path given to the compiler, with `/` separators. The CLI rewrites an absolute path under the current directory to a relative one. Any other absolute path is reduced to its file name. Generated C therefore never contains a host path (`build-integration.md` §6). - `novac build` runs the C compiler with the current directory as its working directory. The debug info's compilation directory then makes the source paths resolve, so a debugger finds the `.nova` files. - Locals keep stable names: `v__` for a named local, `t_` for a temporary. A C debugger therefore shows recognisable variables (`v_count_3`). - Directives never change the code itself. A program with directives must behave exactly like one without them. The run corpus is built both ways (§8). ## 7a. Implementation notes (M1, increment 1c) - Every Nova function becomes `static NV_UNUSED` with a prototype. Nova allows unused functions, parameters and locals, and `-Wunused-*` must stay clean under `-Werror`. - Locals are declared at the top of the function, zero-initialised as in §5 (scalars only in M1). Only blocks that are jump targets get a label. - Runtime files: - `nova/rt.h`: freestanding checked helpers `nv_add_i32`, `nv_div_u64`, `nv_shl_u8`, `nv_shift_amount_s/u`, `nv_as_iN`, …; - `nova_rt_core.c`: output formatting and the panic message, with no libc; - `nova_rt_hosted.c`: the platform layer over stdio. `novac build --emit c` writes all of them next to `main.c`. Build with `cc -I /*.c`. - Panic output: `nova panic: at ::`. Stdout is flushed first, then `abort()` (exit status 134). - `main() -> i32` becomes `int main(void) { return (int)nvF_main(); }` (M7d renamed `nv_` to `nvF_`, §2.1). `Process.exit(c)` calls `exit(c)`, which flushes stdio. ## 8. Validation - Golden-file tests for representative C output (mainly to review diffs). - Every `run-pass` test is compiled with clang and gcc (host) at `-O0` and `-O2`, with and without `-fsanitize=address,undefined`. Outputs must agree (differential testing). - M7a: every run-corpus program is also built with `#line` directives (clang `-O0 -g`) and must produce the same results. `SourceMapTests` compiles programs with `-g` and checks the debug line table: it names the `.nova` file and the lines of the statements. - M7: a Csmith-style random program generator for Nova; three-way differential testing (clang/gcc × opt levels), plus a reference interpreter on NIR (`novac run --interp`) as an oracle (`milestone-7.md`).