The Dune Programming Language

Dune is a small, statically typed, compiled language with a clean C-family syntax. It has function overloading, generics with bounds, records, choices (tagged unions) with when expressions, contracts, first-class function values, lambdas and value-capturing closures, deterministic cleanup with defer, and a module system with a standard library written in Dune itself.

// Doc-comments above a declaration are shown on hover in the editor.
/// brief: Squares a value.
/// returns: value * value
fn square<T is numeric>(value: T): T {
    return value * value;
}

values: [int] = [1, 2, 3, 4];
total = values.filter(is_positive).map(square).sum();
print(total);

One front end, one backend

Dune shares a single front end — lexer, parser, type checker — feeding a single execution backend:

  • A bytecode virtual machine that runs programs directly. It supports every language feature and needs no external toolchain.

How to read this book

  • Language — the reference for the language itself: syntax, types, functions and generics, records, choices, modules, and comments.
  • Standard library — one page per stdlib module, generated from the source doc-comments.
  • Guides — building Dune, the command-line tool, the editor integration, and a tour of the runnable examples.

New to Dune? Start with Installation to build the dune binary, then read Syntax.

Syntax basics

Bindings

A binding introduces a variable. The type can be inferred from the initializer or written explicitly after a colon. Bindings are mutable — assign to them again with =.

x = 40 + 2;            // inferred as int
name: text = "Dune";   // explicit type
x = x - 1;             // reassignment

Use const when the name itself must not be reassigned or shadowed. const does not recursively freeze an array or record; see Values, copying, and mutation.

const answer: int = 42;
const values: [int] = [1];
values.push(2); // valid: values still names the same mutable array

Statements end with a semicolon. Blocks are delimited with { }.

Numeric literals

Integer literals support _ separators, 0x hex, 0b binary, and explicit integer suffixes such as i32, i64, u8, u64, and usize.

size = 1_000_000;
mask = 0xffu64;
bits = 0b1010_0101u8;
wide = 123i64;
rough: real64 = 1_000.5_25;

Printing and formatting

print(expression) prints a value. print also accepts a string literal with positional {} placeholders followed by arguments; the same formatting is available as the format(...) expression, which returns text.

name: text = "Dune";
version: int = 1;

print(name);
print("{} v{}", name, version);
message: text = format("{} v{}", name, version);

The format string must be a literal, placeholders are plain {}, and the number of placeholders must match the number of arguments. Printable values are the scalar types (integers, real32/real64, bool, glyph, text) and any record that provides a to_text(): text method — see the Display contract.

Text and glyph literals

Normal text literals decode \n, \t, \r, \\, \", and \0; glyph literals decode \n, \t, \r, \\, \', and \0. Unknown escapes are compile-time errors. Raw single-line text literals use r"..." and keep backslashes literally.

path: text = r"C:\Users\name\data.csv";
line: text = "hello\n";
tab: glyph = '\t';

Types

Scalar types

CategoryTypes
Signed integersint, i8, i16, i32, i64, isize
Unsigned integersu8, u16, u32, u64, usize, uint8, uint16, uint32, uint64
Realsreal, real32, real64
Otherbool, glyph, text, unit

unit is the type of an expression with no meaningful value (for example a function that returns nothing).

Text and glyphs

A text is an immutable string; a glyph is a single character. Text supports indexing (returning a glyph), slicing, len, and the + operator:

import io;

name: text = "Ada";
greeting: text = "Hi, " + name + "!";   // text + text
shout: text = greeting + '!';           // a glyph appends a character
io.println(greeting);                    // Hi, Ada!
io.println("ab" + "cd" + "ef");          // abcdef

+ joins two text values, and a glyph on either side is appended as a single character (glyph + glyph remains a numeric error). The text module builds on it with helpers such as repeat, reverse, to_upper, to_lower, replace, split, pad_start, pad_end, and the free join function. To interpolate non-text values, use fmt.format.

Arrays

An array type is written [T]. Arrays support literals, indexing, slicing, len, push, and pop, plus a large set of stdlib helpers (see array).

import io;

values: [int] = [1, 2, 3, 4];
values.push(5);
io.println(values.len());     // 5
io.println(values[0]);        // 1
middle: [int] = values[1:3];

Slice bounds are zero-based and the right bound is excluded. Either bound may be omitted (values[:end], values[start:], or values[:]). Both bounds must be within 0..values.len() and the start cannot exceed the end.

Array assignment shares the same mutable array. Use import array; and values.copy() for a fresh outer array; that operation is shallow. See Values, copying, and mutation.

Tuples

Tuples group a fixed number of values of possibly different types and can be destructured on assignment.

pair = (1, "one");
(number, label) = pair;

The tuple shell is immutable. Arrays and records stored in tuple elements keep their normal shared-handle semantics.

Type aliases

Type aliases give a shorter name to an existing type. They are transparent compile-time aliases, not new runtime types, and can target primitive, array, tuple, record, choice, generic, and module-qualified types.

import matrix;
import outcome;

type Count = int;
type Counts = [Count];
type Vec = matrix.Vector<real64>;

type VecOf<T> = matrix.Vector<T>;
type Pair<T, U> = (T, U);
type TextOutcome<T> = outcome.Outcome<T, text>;

Generic aliases take one or more type arguments and substitute them transparently into their target. They may target other aliases and module-qualified types, and the number of arguments must match the declared parameters exactly. Alias parameters are unbounded in the current release; bounds and named const parameters are future extensions.

Explicit casts

Convert between numeric types with the to operator.

value = 17;
exact: real64 = value to real64;

Values, copying, and mutation

This page defines what assignment, function calls, returns, const, and copy() mean in Dune. These rules are part of the language contract and apply to the bytecode VM and the type checker.

The core rule

Every assignment, argument, and return copies a Dune value. It never implicitly deep-copies an object and never moves from or invalidates the source.

There are two relevant kinds of runtime value:

  • Independent values contain their complete observable value. Copying one gives an independent value.
  • Shared handles identify a mutable runtime object. Copying the handle makes another name for the same object.

This distinction is fixed by the type; it is not inferred from whether a binding is declared with const.

Value kindCopy resultMutable through the value?
integers and realsindependent valueno
bool, glyph, unitindependent valueno
textindependent immutable valueno
fn(...)independent function handleno
array [T]handle to the same arrayyes
recordhandle to the same recordyes
tupleimmutable tuple value; nested handles remain sharedno outer mutation
choiceimmutable tag/payload value; a handle in the payload remains sharedno outer mutation

Standard-library structures such as Dict, Set, Vector, and Matrix are records, so ordinary assignment follows the record row of the table.

Assignment

Scalar and immutable values are independent after assignment:

x = 1;
y = x;
y = 2;
print(x); // 1

first = "Dune";
second = first;
second = "VM";
print(first); // Dune

Arrays and records share their mutable object:

record Counter { value: int }

values = [1];
alias = values;
alias[0] = 2;
print(values[0]); // 2

counter = Counter { value: 3 };
counter_alias = counter;
counter_alias.value = 4;
print(counter.value); // 4

Reassigning a handle binding changes only that binding. It does not change the object selected by another binding:

left = [1];
right = left;
right = [9];
print(left[0]); // 1

There is no implicit copy-on-write behavior.

Construction and slicing

Each evaluation of an array literal, array comprehension, record literal, tuple literal, or choice constructor creates a new outer value. Record field defaults are evaluated for each record construction, so a default array is not accidentally shared between separately constructed records.

Array slicing (values[start:end]) creates a fresh outer array and copies the selected elements using the same shallow value rules. A nested array or record inside a slice therefore remains shared. Text slicing creates another immutable text value. Slice bounds are zero-based, right-exclusive, and may be omitted.

Text positions currently count UTF-8 bytes. Therefore text.len(), indexing, and slicing use the same byte-offset coordinate system; slice bounds should be placed on UTF-8 character boundaries when the text contains non-ASCII data.

Some wrapper constructors intentionally retain a supplied array. In the matrix module, vector(data), Vector.new(data), from_flat(rows, cols, data), and Matrix.new(rows, cols, data) use data as their backing array without copying it. Mutating either view is visible through the other. from_rows(rows) is different: it flattens the cells into a fresh backing array.

const freezes a binding, not an object

const prevents the binding from being assigned again or shadowed. It does not turn the referenced object into a deeply immutable object.

const values: [int] = [1];
values[0] = 2;  // valid: mutate the shared array
values.push(3); // valid: mutate the shared array
values = [4];   // error: cannot assign to constant 'values'

The same rule applies to records and receiver calls:

record Counter {
    value: int,
    fn increment(): unit { this.value = this.value + 1; }
}

const counter = Counter { value: 0 };
counter.value = 1;  // valid
counter.increment(); // valid
counter = Counter { value: 2 }; // error

This is binding immutability, similar to JavaScript's const. Dune currently has no deep-const, frozen-array, or read-only-record type.

Function arguments

A parameter receives a copy of the argument value. Therefore:

  • changing a scalar parameter cannot change the caller's scalar;
  • mutating an array or record parameter changes the shared caller-visible object;
  • assigning a different value to the parameter is local to the call;
  • the argument remains valid after the call.
fn update(values: [int]): unit {
    values[0] = 2; // visible to the caller
}

fn replace_locally(values: [int]): unit {
    values = [9];  // only this parameter now names the new array
}

source = [1];
update(source);
replace_locally(source);
print(source[0]); // 2
source.push(3);   // source was not moved and is still usable

The rules do not depend on whether a function is generic or overloaded. Dict, Set, Vector, and Matrix are records, so they follow the same rule. For example, mutating a dictionary parameter changes the caller's dictionary; assigning Dict.new() to the parameter changes only that local parameter.

Return values

Returning also copies a Dune value. Returning an array or record therefore returns another handle to the same object:

fn identity(values: [int]): [int] {
    return values;
}

source = [1];
result = identity(source);
result[0] = 2;
print(source[0]); // 2

Returning a freshly constructed array or record returns a handle to that new object. Return does not consume any local or argument at the language level.

Method receivers

An instance or receiver method receives the caller's value as this. The receiver follows the same copy rule as an ordinary argument. For records and arrays it is a copied handle to the same mutable object, so field, element, and nested-container mutation is visible to the caller. Immutable receiver types remain immutable.

this is a non-reassignable receiver binding:

record Counter {
    value: int,

    fn increment(): unit {
        this.value = this.value + 1; // valid
    }

    fn invalid(): unit {
        this = Counter { value: 0 };
        // error: cannot reassign method receiver 'this'
    }
}

Non-reassignability avoids the misleading impression that replacing this could replace the caller's binding. It applies to both record methods and extension methods; it does not make the receiver object immutable.

Mutating and value-returning methods

The method syntax does not itself promise mutation or purity. A method mutates the caller-visible object when its body writes through this; a method can instead construct and return a new value. The return type alone is not enough to classify it: pop() both mutates an array and returns an element.

The standard collection APIs use these concrete rules:

TypeMutates existing objectReturns fresh structure
arrayindexed assignment, push, pop, clearcopy, slicing, append, prepend, concat
Dict<V>set, remove, clearcopy, keys, values
Setadd, remove, clearcopy, values
Vector<T>set, fillcopy, slice, concat, arithmetic, reshape
Matrix<T>set, fillcopy, row/column extraction, flatten, reshape, arithmetic, transpose and products

"Fresh structure" still uses ordinary shallow element semantics. For example, Dict<V>.values() returns a new outer array, but a record or array stored as a V remains shared. Custom record methods must document which operations they perform; Dune does not infer an effect annotation from the method name.

Tuples and choices

A tuple's element slots and a choice's tag/payload cannot be assigned through the tuple or choice. However, an array or record stored inside them remains a shared handle:

choice Payload { Values([int]), Empty }

source = [1];
(unpacked, marker) = (source, 0);
unpacked[0] = 2;
print(source[0]); // 2

payload: Payload = Values(source);
// Binding the Values payload obtains another handle to source.

Immutability of the outer tuple/choice is not recursive deep immutability.

Explicit copies

Dune has no implicit deep-copy operation. Copying structure is requested by an ordinary API named copy():

  • [T].copy() creates a new outer array and copies its elements using normal Dune value semantics;
  • record Name derive copy generates a new outer record and copies each field using normal Dune value semantics;
  • Dict<V>.copy() creates fresh key/value arrays, while V values are copied shallowly;
  • Set.copy() creates a fresh backing array (its elements are immutable text);
  • Vector.copy() and Matrix.copy() create fresh numeric backing arrays.

For example, array copy separates the outer array but shares a nested array:

import array;

nested: [[int]] = [[1]];
clone = nested.copy();
clone.push([2]);       // changes only clone's outer array
clone[0][0] = 9;      // changes the shared inner array
print(nested.len());  // 1
print(nested[0][0]); // 9

Likewise, derived record copy is shallow:

record Bag derive copy {
    name: text,
    values: [int],
}

original = Bag { name: "a", values: [1] };
clone = original.copy();
clone.name = "b";       // independent outer record field
clone.values[0] = 2;    // shared nested array
print(original.name);   // a
print(original.values[0]); // 2

An API that needs recursive independence must construct it explicitly by copying each nested array/record at the desired depth. The name copy() alone never promises recursive copying.

No source-level move semantics

Ordinary Dune values are never moved at the language level. There is currently:

  • no move expression;
  • no consumed or moved-from state;
  • no use-after-move diagnostic;
  • no ownership transfer caused by assignment, calls, or returns.

The VM implementation may use C++ moves internally as an optimization only when this is unobservable. Such implementation details cannot change the rules on this page.

Closures and reserved resource rules

Closures extend the same model without changing ordinary values:

  • a closure capture copies the captured Dune value at closure creation; scalars/text are snapshots, while captured array/record handles continue to share their object;
  • rebinding the original local after capture does not retarget the captured value;
  • captured bindings cannot be reassigned from inside a closure; aggregate contents can still be mutated through their shared handles;
  • nested closures forward captured values through their enclosing closure environments, and returned closures keep those environments alive;
  • callable values can themselves be captured and composed.

See Functions and generics for syntax and examples.

Resource-owning values are still reserved work. Their future implementation must preserve today's value and closure behavior:

  • resource-owning types must be explicitly marked move-only and use an explicit move/consume operation;
  • the type checker must reject copying a move-only resource and using it after transfer;
  • adding resources must not make today's arrays, records, arguments, or returns implicitly move-only.

These are compatibility constraints for planned resource work, not resource syntax that is accepted today.

Functions and generics

Functions

Functions are declared with fn, take typed parameters, and declare a return type after the parameter list.

fn add(a: int, b: int): int {
    return a + b;
}

total: int = add(10, 20);

Arguments and return values follow Dune's uniform value semantics: scalars are independent values, while arrays and records are shared handles. Parameter reassignment stays local, but aggregate mutation is visible to the caller. See Values, copying, and mutation.

Functions may be overloaded by the number and types of their parameters; the type checker selects the matching definition at each call site.

Generics and bounds

Type parameters go in angle brackets. A parameter may carry a bound that constrains which types satisfy it.

fn square<T is numeric>(value: T): T {
    return value * value;
}

Available bounds:

  • integer — the integer types.
  • numeric — integers and reals.
  • comparable — supports == / !=.
  • ordered — supports <, <=, >, >=.

A contract name is also a valid bound: T is Display requires T to implement the Display contract.

Multiple bounds

A single type parameter can carry several bounds. Group them with + under one is, or repeat is for the same parameter — both mean "every listed constraint must hold":

// Grouped: T must be both ordered (for `<`) and comparable (for `==`).
fn spans<T is ordered + comparable>(low: T, value: T, high: T): bool {
    return (low < value) == (value == high);
}

// Repeated form is equivalent.
fn spans2<T is ordered, T is comparable>(low: T, value: T, high: T): bool {
    return (low < value) == (value == high);
}

When an argument fails a bound the diagnostic names the specific unmet constraint, e.g. type 'text' does not satisfy bound 'numeric' on 'T'.

Const generics and static shapes

A generic argument can also be a positive integer literal instead of a type. The matrix module uses this to carry the shape of a Matrix or Vector in its type, so shape mistakes become compile-time errors instead of runtime panics:

import matrix;

// A 3x3 matrix and a length-3 vector, spelled in the type.
a: matrix.Matrix<real64, 3, 3> = matrix.identity(3);
v: matrix.Vector<real64, 3> = matrix.vector([1.0, 2.0, 3.0]);

// The product's shape is checked and flows into the result type.
r: matrix.Vector<real64, 3> = a.mul_vector(v);

Matrix<T, Rows, Cols> takes two dimensions; Vector<T, Len> takes one. The type checker verifies shapes for the core operations — vector dot, matrix add/sub/mul/div (element-wise, same shape), matrix matmul/dot, and matrix–vector mul_vector/dot — and rejects incompatible ones:

m: matrix.Matrix<real64, 3, 3> = matrix.identity(3);
w: matrix.Vector<real64, 4> = matrix.vector([1.0, 2.0, 3.0, 4.0]);
bad = m.mul_vector(w);
// error: matrix-vector shape mismatch: matrix.Matrix<real, 3, 3> has 3 column(s)
//        but matrix.Vector<real, 4> has length 4

Static shapes are optional and coexist with the dynamic API: a plain Matrix<real64> (no dimensions) matches any shape, so existing code keeps working and a dynamic value is assignable to and from a statically-shaped binding. Shapes are a type-check-time concern only — the runtime representation is unchanged.

Phase 1 accepts integer literals only. Named const parameters, const expressions (N + 1), and shape inference from array literals are planned follow-ups (see issue #43).

Function values

Function types use fn(P1, P2): R. Named functions and lambdas are ordinary values: they can be stored in bindings and aggregates, passed, returned, copied, and invoked through any function-valued expression.

import array;

fn is_positive(value: int): bool { return value > 0; }
fn square(value: int): int { return value * value; }

values: [int] = [-2, 3, -1, 4];
result = values.filter(is_positive).map(square).sum();

An overloaded named function needs an expected function type so the checker can select one signature. Generic named functions are monomorphized at call sites; they cannot be stored by bare name without concrete type arguments.

Lambdas

A lambda starts with fn but has no name:

square = fn(value: int): int {
    value * value
};

result: int = square(6); // 36

The body is a full function body. It supports local bindings, loops, conditionals, when, early return, and a final tail expression. A unit lambda can use return;.

When the target has a function type, omitted annotations are inferred from that context:

increment: fn(int): int = fn(value) { value + 1 };

Without a contextual function type, omitted parameter and result annotations use the same int defaults as named functions. Explicit annotations are recommended at public or non-obvious boundaries because diagnostics then show the intended signature directly.

Lambdas do not declare their own generic parameter list. They may, however, appear inside a generic named function; monomorphization substitutes the concrete types through the lambda and its capture environment:

fn remember<T>(value: T): fn(): T {
    fn(): T { value }
}

answer = remember(42);
label = remember("Dune");

Closures and captures

A lambda becomes a closure when it references a binding outside its own body. Captures follow Dune's ordinary value semantics and are evaluated once when the closure is created:

  • scalars, text, and callable bindings are snapshots;
  • arrays and records copy their shared handles, so aggregate mutation remains visible through every alias;
  • rebinding the original variable later does not change an existing closure;
  • a captured name cannot be reassigned inside the closure;
  • nested closures forward any outer values needed by their own children.
factor: int = 10;
scale = fn(value: int): int { value * factor };
factor = 20;
scale(4); // 40: the closure captured 10

items = [1];
append = fn(value: int): unit {
    items.push(value); // aggregate contents may change
    return;
};
append(2); // the outer array is now [1, 2]

Factory calls create independent environments, so closures can safely outlive the function invocation that created them:

fn make_adder(base: int): fn(int): int {
    return fn(value: int): int { base + value };
}

add_two = make_adder(2);
add_ten = make_adder(10);

Calling function-producing expressions

Any expression with a function type is callable. Parenthesized lambdas can be invoked immediately, and calls can be chained when a function returns another function:

answer = (fn(value: int): int { value + 1 })(41);
same = make_adder(40)(2);

callbacks: [fn(): int] = [fn(): int { 40 }, fn(): int { 2 }];
also = callbacks[0]() + callbacks[1]();

The checker reports the complete expected and actual fn(...) signatures for argument, arity, and return mismatches. Calling a non-function value is rejected before bytecode generation.

Standard-library integration

import array; supplies map(fn(T): U), filter(fn(T): bool), reduce/fold, any, all, and count_where. Named functions and capturing lambdas use the same callback path:

offset = 3;
shifted = [1, 2, 3].map(fn(value: int): int { value + offset });

Function values and closures run on Dune's canonical bytecode VM; there is no separate backend or native-only closure behavior.

Foreign functions

foreign fn binds a Dune signature to a native C symbol. These are used sparingly (the standard library restricts them to a single sanctioned primitive).

foreign fn c_sqrt(value: real64): real64 = "sqrt";

print(c_sqrt(81.0));   // 9

Compile-time evaluation (foreknown)

A declaration marked foreknown is evaluated at compile time. The value it produces is folded into the bytecode before the program starts running, so there is no runtime cost for the computation — only for using the result.

foreknown can be applied to both constants and functions.

Foreknown constants

A foreknown const is computed once, during compilation:

foreknown const KB: int = 1024;
foreknown const PAGE: int = KB * 4;   // 4096, folded at compile time

Foreknown constants may only appear at the top level of a file.

Foreknown functions

A foreknown fn is an ordinary function that is also allowed to run during compilation. It can be called to initialise a foreknown const:

foreknown fn factorial(n: int): int {
    if n <= 1 {
        return 1;
    }
    return n * factorial(n - 1);
}

foreknown const FACT5: int = factorial(5);   // 120, computed by the compiler

Foreknown functions support the usual control flow — if/else, while, C-style for, break, continue, return — and local bindings:

foreknown fn sum_to(n: int): int {
    total: int = 0;
    for i: int = 0; i <= n; i = i + 1 {
        total = total + i;
    }
    return total;
}

foreknown const TRIANGLE: int = sum_to(10);   // 55

A foreknown function remains a normal function too: it can still be called at runtime like any other.

Rules and limitations

Because foreknown code runs inside the compiler, it must be pure and self-contained. The type checker rejects a foreknown declaration that:

  • calls a function that is not itself foreknown;
  • performs I/O (io.print, io.println, …) or string formatting (fmt.format);
  • uses the ? try operator or the in membership operator;
  • reads module members other than foreknown constants;
  • builds or indexes aggregates (arrays, tuples, records, comprehensions);
  • mutates aggregate values, or assigns to anything but a local variable.

In addition, a foreknown function may not be foreign, and generic foreknown functions are not supported yet.

These restrictions guarantee that the compiler can fully evaluate the declaration and bake the result into the program.

Records, methods, and contracts

Records

A record groups named fields. It can also declare methods, which receive the instance as this. Records are shared handles: assignment, arguments, and returns preserve the identity of the same mutable record. this cannot be reassigned, but its fields may be mutated. See Values, copying, and mutation.

record Point {
    x: int,
    y: int,

    fn magnitude_squared(): int {
        return this.x * this.x + this.y * this.y;
    }
}

p: Point = Point { x: 3, y: 4 };
print(p.magnitude_squared());   // 25

A static fn belongs to the record rather than an instance and is often used as a constructor:

record Counter {
    value: int,

    static fn zero(): Counter { return Counter { value: 0 }; }
}

Derive

derive asks the compiler to generate common methods from the fields:

  • eq generates equals (structural equality).
  • copy generates copy (a shallow copy: the record is new, but nested arrays and records remain shared).
  • debug generates to_text (a debug rendering).
record Vec2 derive eq, copy {
    x: int,
    y: int,
}

The generated copy() creates a new outer record. Each field is copied using normal Dune semantics, so nested arrays and records remain shared. See Explicit copies.

The Display contract

A record is printable when it provides a to_text(): text method. print(record) and format("{}", record) call it.

record Point {
    x: int,
    y: int,

    fn to_text(): text {
        return format("({}, {})", this.x, this.y);
    }
}

print(Point { x: 1, y: 2 });   // (1, 2)

import display; provides a matching Display contract (so a record can declare with display.Display) and a show(value) helper.

Contracts

A contract names a set of method signatures a record can promise to implement with the with clause. This gives generic code a way to require behavior.

contract Display {
    fn to_text(): text;
}

record Tag with Display {
    name: text,
    fn to_text(): text { return this.name; }
}

Operator overloading

When the left operand of +, -, *, or / is a record, the operator dispatches to a conventionally-named method on that record:

OperatorMethod
a + ba.add(b)
a - ba.sub(b)
a * ba.mul(b)
a / ba.div(b)

The method is resolved with the normal overload rules, so the right operand can be another record or a scalar, and the result type is whatever the method returns:

record Vec2 {
    x: int,
    y: int,
    fn add(other: Vec2): Vec2 { return Vec2 { x: this.x + other.x, y: this.y + other.y }; }
    fn mul(factor: int): Vec2 { return Vec2 { x: this.x * factor, y: this.y * factor }; }
}

a: Vec2 = Vec2 { x: 1, y: 2 };
b: Vec2 = Vec2 { x: 3, y: 4 };

sum: Vec2 = a + b;      // Vec2 { x: 4, y: 6 }
scaled: Vec2 = a * 10;  // Vec2 { x: 10, y: 20 }

This is how the matrix module's vectors and matrices get natural arithmetic — v + w, v * scalar, and element-wise v * w all map to the corresponding Vector/Matrix methods. Applying an operator to a record that lacks the matching method is a compile-time error (operator '+' is not defined for type 'Point'). Matrix multiplication stays explicit via .matmul(...) / .dot(...) to avoid ambiguity with element-wise *.

Visibility across modules

Record fields and methods are private across module boundaries unless the member is marked export. See Modules.

Choices, when, and ?

Choices

A choice is a tagged union: a value is exactly one of its variants, and a variant may carry a payload.

choice Shape {
    Circle(real64),
    Rectangle,
}

s: Shape = Circle(2.0);

when expressions

when matches a value against patterns and produces a result. It has two forms: a literal/wildcard form and a variant-binding form.

// literal / wildcard
label = when value {
    is 1 { "one" }
    is _ { "many" }
};

// bind a variant payload
area = when s {
    Circle(radius) => 3.14159 * radius * radius;
    Rectangle => 0.0;
};

Exhaustiveness and unreachable arms

The type checker proves that every choice variant is handled. A match may list every variant explicitly or finish with _. If variants are missing, the diagnostic names each one in declaration order. Repeating a variant, or placing an arm after _, is an error because that arm can never be selected.

import fmt;

choice Status { Ready, Running(int), Failed(text) }

label = when status {
    Ready => "ready";
    Running(count) => fmt.format("running {}", count);
    Failed(message) => message;
};

Literal matches over int, real numbers, glyph, and text require a final _ because their possible values are not finite. Duplicate literal arms are rejected. A bool match is exhaustive when it handles both true and false, so it does not need a redundant fallback:

state = when enabled {
    true => "enabled";
    false => "disabled";
};

Optional and result: maybe and outcome

The standard library builds two common choices on top of this machinery:

  • maybeMaybe<T> with present(value), absent(default), and value_or().
  • outcomeOutcome<T, E> with done, failed, and failure_or().

The ? operator

The postfix ? operator propagates the "empty" or "error" case of a Maybe or Outcome out of the enclosing function, returning early, and otherwise unwraps the contained value. It binds tighter than binary operators.

import outcome;

fn total(): outcome.Outcome<int, text> {
    a = read_number()?;   // returns early on failure
    b = read_number()?;
    return outcome.done(a + b, "");
}

Printing choices

Choices print by default — no boilerplate. io.println(value) and fmt.format("{}", value) render a choice as its variant name, plus the payload in parentheses when the variant carries one:

import io;

choice Shape { Circle(int), Named(text), Empty }

a: Shape = Circle(5);
io.println(a);      // Circle(5)
io.println(Empty);  // Empty

This works when every variant payload is a scalar or text (including generic choices instantiated with such types, like Maybe<int>). A choice whose variant carries a record, array, or tuple is not printable by default — give the payload type its own to_text rendering or format the fields explicitly. See Display.

Loops, ranges, and comprehensions

Ranges

start..end is a half-open integer range used by for loops and stdlib helpers.

for i in 0..4 {
    print(i);   // 0 1 2 3
}

for and while

for ... in iterates an array or a range. while loops on a condition. Both support break and continue.

values: [int] = [10, 20, 30];
for value in values {
    print(value);
}

x = 3;
while x > 0 {
    x = x - 1;
}

Array comprehensions

An array comprehension builds a new array from an iterable, with an optional filter.

squares = [x * x for x in 0..5];             // [0, 1, 4, 9, 16]
evens = [x for x in 0..10 if x % 2 == 0];    // [0, 2, 4, 6, 8]

For a functional style over existing arrays, see the higher-order pipeline in Functions and the array module.

Deterministic cleanup with defer

defer registers synchronous cleanup for the current lexical scope. The cleanup runs exactly once when that scope is left, whether control reaches the closing brace or leaves through break, continue, return, ?, or a VM runtime error such as runtime.panic.

Use the expression form for one operation. Its result must be unit:

resource = open_resource("events.log");
defer resource.close();

Use the block form for several operations or conditional cleanup:

defer {
    resource.flush();
    resource.close();
}

A semicolon is required after the expression form and optional after the block form.

Order and scope

Each defer belongs to the innermost enclosing statement scope. Cleanups in one scope run in reverse registration order (LIFO):

defer io.println("last");
defer io.println("first");
// prints "first", then "last"

A loop body is a fresh scope on every iteration. Therefore a deferred cleanup inside the body runs before the next iteration on continue and before leaving the loop on break. Function, lambda, test, and top-level bodies also own their registered cleanups. When a runtime error crosses several function calls, Dune cleans the innermost frame first and proceeds outward.

The value of a return is saved before cleanup starts, so cleanups cannot replace it. The ? operator uses the same return path: propagating a Failed outcome.Outcome still runs every pending cleanup in the function.

Capture and evaluation

Registration creates a zero-argument closure; the cleanup body itself executes only when the scope exits. Captures follow ordinary Dune value semantics:

  • numbers, booleans, glyphs, text, and callable values are snapshots taken at registration;
  • arrays and records are captured as shared handles, so later element or field mutation is visible to cleanup;
  • a captured binding itself cannot be reassigned inside cleanup, just as in an ordinary closure;
  • names declared after defer are not in scope and produce a type error.

Calls and other operations written inside the deferred expression or block are delayed until cleanup. If they need a value computed immediately, bind it before defer; the scalar binding is then captured as a snapshot.

path: text = current_path(); // runs now
defer remove_path(path);     // remove_path runs on exit

Failures during cleanup

The expression form accepts only unit, so a fallible close operation returning outcome.Outcome cannot be accidentally discarded. Handle that result explicitly in a block (for example by logging it or converting it to a panic):

defer {
    closed = resource.try_close();
    if closed.is_failed() {
        log.error(closed.failure_or("close failed"));
    }
}

? cannot propagate from such a block because cleanup itself returns unit; the block must choose its failure policy explicitly.

Cleanup is best-effort and deterministic. If one cleanup fails, Dune still runs the remaining pending cleanups in LIFO order. When another runtime error is already being handled, it remains the primary error; cleanup failures are appended as while running deferred cleanup context. If cleanup is the first operation to fail during an otherwise normal exit, that failure becomes the primary error.

A return inside a deferred block returns from that cleanup block, not from the surrounding function. break and continue cannot target an outer loop from a cleanup block. A cleanup may register its own nested defer; those nested cleanups finish before the outer cleanup returns.

Resource API convention

There is deliberately no mandatory Dispose contract. Any function or method returning unit can be deferred, which keeps pure-Dune modules and user-defined FFI wrappers on the same path. Resource-owning APIs should:

  1. provide an idempotent close(), release(), or similarly explicit unit operation;
  2. show defer resource.close(); immediately after successful acquisition in their documentation;
  3. use an outcome.Outcome result when acquisition can fail, then register cleanup only after unwrapping the resource.

defer is synchronous: a cleanup finishes before execution continues outside the scope. A future asynchronous resource model will need a separate contract; this statement does not detach or schedule background work.

See the runnable defer_cleanup.dn example for normal return, early return, ?, loop exits, capture behavior, and LIFO ordering.

Runtime errors and stack traces

Dune distinguishes recoverable values from failures that abort the current VM execution. A recoverable operation returns a choice such as outcome.Outcome<T, E>; a panic is reserved for violated invariants and operations that cannot continue, such as division by zero or indexing outside an array.

Explicit panic

The runtime module exposes the only native primitive used by Dune's standard library:

import runtime;

fn require_positive(value: int): unit {
    if value <= 0 {
        runtime.panic("value must be positive");
    }
}

runtime.panic(message) has the panic category and preserves message verbatim. It aborts the current script, test, REPL entry, or notebook cell after running all pending defer cleanups.

Categories

Every VM failure has a stable category:

CategoryMeaning
panicAn explicit runtime.panic call.
bounds errorInvalid array, tuple, record, text, or slice position.
arithmetic errorAn arithmetic operation cannot produce a value, such as division by zero.
runtime type errorInvalid operands reached the VM or a dynamic callable had the wrong shape.
I/O errorA non-recoverable stream/runtime I/O failure. Normal filesystem APIs return Outcome values.
foreign function errorA C FFI symbol or invocation is unsupported or invalid.
VM errorA bytecode invariant failed. This normally indicates a compiler or VM bug.

The category is available to C++ embedders as RuntimeErrorKind; the original message, frames, and deferred-cleanup failures remain structured on RuntimeError instead of requiring consumers to parse what().

Stack trace format

The compiler records the source span of every bytecode instruction. When an operation fails, the VM snapshots all live Dune frames before unwinding them:

panic: value must be positive
stack trace:
  0: require_positive
      at examples/check.dn:5:9
  1: validate
      at examples/check.dn:10:5
  2: <top-level>
      at examples/check.dn:13:1

Frames are ordered from the failing function to the outermost caller. Imported functions use their module-qualified names and point to the imported .dn file, including pure-Dune standard-library modules. Lambdas use a generated <lambda@line:column> name.

dune test names the outer frame as test "name" for both test blocks and @test functions. REPL entries use <repl>. Notebook frames use <path>.dnb#cell-<id> and report lines relative to that cell, so a function defined in one cell and called in another points to both cells correctly.

Panic during defer

The first failure remains primary. Dune still runs every pending cleanup in LIFO order. Each cleanup failure is appended as while running deferred cleanup: ... and keeps its own stack trace; it does not replace the original category, message, or frames.

Recoverable errors remain values

Returning Failed(error) from outcome.Outcome<T, E> does not create a stack trace and is not caught by the panic machinery. It remains a normal choice value that can be matched, inspected, or propagated with ?. Use Outcome for expected failures and runtime.panic for conditions the current execution cannot safely continue from.

Modules

Modules are loaded from .dn files. The standard library is a set of such modules (see the Standard library section).

Importing

There are three import forms, and they interoperate freely:

import math;                          // plain: use as `math.square`
import matrix as m;                   // alias: use as `m.Vector`
from matrix import Vector, Matrix;    // selective: use `Vector` unqualified

Selective imports are comma-separated and may span several lines. Importing an unknown symbol, a private symbol, or reusing an alias that collides with another module is a compile-time error that names the offending symbol.

Module declaration

A file may open with a module name; declaration that names the unit for documentation and diagnostics. It is optional and, in this first version, does not bind the file to a directory layout — modules are located by file name on the search path.

module geometry;

export record Point {
    export x: int,
    export y: int,
}

export fn manhattan(a: Point, b: Point): int { /* ... */ }

Export visibility

If a module contains any explicit export, only exported functions, constants, records, choices, and contracts are visible through module.name; everything else stays private to the module. Record fields and methods are private across module boundaries unless the member itself is marked export.

export const ANSWER: int = 42;

fn hidden(): int { return 7; }         // private to this module

export fn public(): int { return hidden(); }

Receiver methods declared by a module become available on values of the receiver type after import — for example, import array; enables both array.first(xs) and xs.first().

Comments and doc-comments

Dune has single-line // comments and multi-line /* ... */ block comments.

// a line comment
/* a block comment
   that spans several lines */

Doc-comments

A comment block written directly above a declaration becomes its documentation. The editor's LSP shows it in the hover for that symbol, including symbols in other modules (hovering math.square pulls the comment from math.dn). A blank line between the comment and the declaration detaches it, and a comment trailing code on the same line never attaches.

Plain // comments are shown as prose, so existing comments document their symbols with no extra syntax. Doc-comments may also use the /// line form or the /** ... */ block form.

Structured tags

Doc-comments may carry structured tags — brief, param, returns, and example — which the editor renders as sections:

/// brief: Squares a value.
/// param value: the number to square
/// returns: value * value
fn square(value: int): int {
    return value * value;
}

Tags work on functions, records, record fields, and record methods:

/** brief: A point on the integer grid. */
record Point {
    // The horizontal coordinate.
    x: int,

    /// brief: The squared distance from the origin.
    fn magnitude_squared(): int { return this.x * this.x + this.y * this.y; }
}

The documented.dn example demonstrates every form. The standard-library reference in this book is generated from these same doc-comments.

Source-level attributes

Attributes attach compile-time metadata to a top-level declaration. They are written before the declaration (and before export, foreign, or foreknown) and become part of the AST:

@deprecated("use matrix.dot")
export fn dot_old(left: int, right: int): int {
    return left * right;
}

@test
fn vector_sum(): unit {
    return;
}

Several attributes can be stacked. Attribute names may be qualified with dots, and the syntax accepts an optional comma-separated list of literal arguments:

@tool.flags(-2, 3.5, 'x', true, "stable",)
fn configured(): unit { }

Integer, real, glyph, text, raw-text, and boolean literals are represented in the AST without becoming runtime expressions. A trailing comma is accepted. Calls, identifiers, arrays, and other expressions are rejected as attribute arguments.

The compiler currently defines eight attributes. Unknown attributes, including qualified names, are errors so that a misspelling cannot silently change a build.

AttributePurpose
@deprecated("message")Warn whenever an old API is used.
@experimental("message")Warn that an API contract may still change.
@since("version")Record the release that introduced a declaration.
@must_use / @must_use("message")Warn when a function result is discarded.
@testRegister a zero-argument unit function with dune test.
@ignore / @ignore("reason")Discover but skip an @test function.
@should_panic / @should_panic("message")Require an @test function to panic.
@should_fail / @should_fail("message")Require any typed runtime failure.

@deprecated(message)

@deprecated accepts exactly one non-empty text argument. It is valid on functions, constants, records, choices, contracts, and type aliases:

@deprecated("use MAX_RETRIES")
const OLD_RETRIES: int = 3;

@deprecated("use Result")
choice LegacyResult { Ok(int), Error(text) }

Using a deprecated function, function value, constant, record constructor or literal, record member, choice variant, type annotation, type alias, or contract bound produces a source-mapped warning. A warning does not fail dune <file>, dune check, or dune test. Metadata is preserved when an exported declaration is qualified and loaded from another module, so clients of that module receive the same warning.

The language server publishes these diagnostics with LSP warning severity and shows the attribute plus a deprecation callout in hover. dune doc includes the deprecation message in generated API documentation.

@experimental(message)

@experimental has the same declaration targets and module behavior as @deprecated, but communicates that an API is available for evaluation while its contract may still change:

@experimental("the shape API may change before 1.0")
export fn infer_shape(): int {
    return 2;
}

Every use produces a source-mapped warning. @experimental requires one non-empty text message and cannot be combined with @deprecated on the same declaration.

@since(version)

@since records the release in which a declaration became available. It accepts exactly one non-empty text value and works on every supported top-level declaration:

@since("0.14.0")
export fn stable_api(): int {
    return 42;
}

It does not change runtime behavior. The metadata is preserved in the AST and modules and is rendered as an “Available since” callout by LSP hover and dune doc.

@must_use(message?)

@must_use is valid on functions that explicitly return a non-unit value. It warns when a direct call is used as a standalone statement and its result is discarded:

@must_use("check whether the operation succeeded")
fn save(): bool {
    return true;
}

save();                 // warning
saved: bool = save();   // no warning

The explanatory text is optional, but when present it must be a non-empty text literal. The rule also works for exported, imported, overloaded, and generic functions because the metadata follows the resolved overload.

@test

@test turns a named function into a test discovered by dune test:

from assert import assert_eq;

@test
fn addition(): unit {
    assert_eq(20 + 22, 42);
}

A test function must:

  • be top-level;
  • take no arguments;
  • explicitly return unit;
  • have a body (it cannot be foreign);
  • be non-generic and non-foreknown.

Attributed test functions and test "name" { ... } blocks can coexist and run in declaration order. The function name is used in the test report. Like test blocks, they are not executed automatically during an ordinary run. Because the @test form is still a named function, user code can call it explicitly.

@ignore(reason?)

@ignore skips an @test function without compiling out the declaration. The optional reason is printed in the test report, and ignored tests are counted separately:

@test
@ignore("requires a local service")
fn integration_test(): unit { }

@should_panic(message?)

@should_panic makes a panic the successful outcome of an @test function. With no argument, any runtime panic is accepted. With a text argument, the panic message must contain that text:

import runtime;

@test
@should_panic("index out of bounds")
fn rejects_bad_index(): unit {
    runtime.panic("index out of bounds");
}

A test fails if it returns normally or panics with a different message.

@should_fail(message?)

@should_fail is the broader counterpart to @should_panic: any typed runtime failure (panic, bounds, arithmetic, type, I/O, or foreign error) satisfies the expectation. Its optional text argument must occur in the primary error message:

@test
@should_fail("array index out of bounds")
fn rejects_bad_index(): unit {
    values: [int] = [1];
    io.println(values[4]);
}

@ignore, @should_panic, and @should_fail require @test. They are mutually exclusive so every test has one unambiguous execution policy.

Placement and validation

Attributes are currently supported only on top-level declarations. They cannot be attached to local bindings, statements, parameters, fields, variants, or record methods. The compiler reports duplicate attributes, unknown attributes, invalid targets, invalid arguments, conflicting combinations, and invalid test signatures at the attribute's source location.

Standard library

The standard library is ordinary Dune loaded from .dn files. Each page below is generated from that module's source doc-comments.

  • array — Generic helpers and higher-order pipelines for arrays.
  • assert — Assertion helpers for tests.
  • autograd — Scalar reverse-mode automatic differentiation.
  • canvas — Deterministic SVG canvas and immediate-mode GUI widgets.
  • cli — Command-line argument parsing and help output.
  • collections — Shared collection utilities.
  • csv — CSV parsing and numeric-matrix I/O.
  • dict — A hash-map style dictionary built in Dune.
  • display — The Display contract and show helper.
  • fmt — String formatting with {} placeholders.
  • fs — File-system access (read, write, list).
  • io — Standard input/output: print, read a line, and flush streams.
  • log — Levelled diagnostics with stderr output and filtering.
  • math — Numeric constants and generic math functions.
  • matrix — A small NumPy-style foundation: vectors and matrices.
  • maybe — The optional Maybe<T> choice and helpers.
  • outcome — The result-style Outcome<T, E> choice and helpers.
  • plot — Deterministic SVG/HTML chart rendering.
  • process — Process access: arguments and environment.
  • random — A small deterministic pseudo-random generator.
  • regex — Safe ASCII regular expressions for validation and text cleanup.
  • runtime — Runtime helpers such as panic.
  • set — A hash-set built in Dune.
  • stats — Descriptive statistics, relationships, models, histograms, and probability helpers.
  • text — Text and glyph helpers.

array

Generic helpers and higher-order pipelines for arrays.

Generic array utilities exposed both as methods on [T] and as free functions. Most are written directly on top of indexing, len(), and push(); nothing here needs native support. Methods with a <T> are generic over the element type; some carry a T is numeric bound for arithmetic.

Use array when you want the built-in [T] type to feel like a small collection library. It adds copying, slicing, concatenation, searching, and value-counting helpers, plus higher-order pipelines such as map, filter, fold, reduce, any, and all.

copy() creates a fresh outer array and copies elements shallowly. Nested arrays and records remain shared according to Dune's value semantics.

The module also provides numeric reductions and constructors: range, repeat, zeros, ones, full, sum, product, min, max, argmin, and argmax. Importing the module enables both free-function calls and receiver-style calls on arrays.

import array;

fn square(value: int): int { return value * value; }
fn is_even(value: int): bool { return value % 2 == 0; }

values = array.range(1, 6);
evens_squared = values
    .filter(is_even)
    .map(square);

print(evens_squared.sum());
print(values.take(3).last());

Auto-generated from stdlib/array.dn by tools/gen_stdlib_docs.py.

method<T> [T].copy(): [T]

A shallow copy of this array (new backing array, same elements).

method<T> [T].reverse(): [T]

A new array with the elements in reverse order.

method<T> [T].contains(needle: T): bool

True when needle appears in the array (delegates to index_of).

method<T> [T].index_of(needle: T): int

The index of the first element equal to needle, or -1 if none.

method<T> [T].first(): T

The first element (indexing panics if the array is empty).

method<T> [T].last(): T

The last element (index len-1).

method<T> [T].append(value: T): [T]

A copy of this array with value appended at the end.

method<T> [T].prepend(value: T): [T]

A copy of this array with value inserted at the front.

method<T> [T].concat(other: [T]): [T]

A new array made of this array followed by other.

method<T> [T].slice(start: int, end: int): [T]

The sub-array from start (inclusive) to end (exclusive).

method<T> [T].take(count: int): [T]

The first count elements (a prefix slice).

method<T> [T].drop(count: int): [T]

Everything after the first count elements (a suffix slice).

method<T> [T].count_value(needle: T): int

How many elements equal needle.

method<T> [T].equals(other: [T]): bool

True when this array and other have equal length and equal elements.

method<T> [T].fill(value: T): unit

Overwrite every slot with value in place (returns unit, mutates self).

method<T, U> [T].map(transform: fn(T): U): [U]

A new array with transform applied to every element.

method<T> [T].filter(keep: fn(T): bool): [T]

A new array holding only the elements for which keep returns true.

method<T, Acc> [T].fold(initial: Acc, combine: fn(Acc, T): Acc): Acc

Left fold: seed an accumulator with initial, then combine it with each element in order. combine receives (accumulator, element).

method<T> [T].reduce(combine: fn(T, T): T): T

Reduce with the first element as the seed (panics on an empty array, since there is no element to start from). combine receives (accumulator, element).

method<T> [T].any(predicate: fn(T): bool): bool

True when predicate holds for at least one element (short-circuits).

method<T> [T].all(predicate: fn(T): bool): bool

True when predicate holds for every element (short-circuits on the first failure; vacuously true for an empty array).

method<T> [T].count_where(predicate: fn(T): bool): int

How many elements satisfy predicate.

fn range(start: int, end: int): [int]

The half-open integer range [start, end) as an array.

Example:

array.range(2, 6).sum()  // 14

fn range(end: int): [int]

The range [0, end); an overload that defaults the start to 0.

fn range(start: int, end: int, step: int): [int]

The range [start, end) advancing by step (supports negative steps).

fn repeat<T>(value: T, count: int): [T]

An array containing value repeated count times (generic element type).

Example:

array.repeat(4, 3).sum()  // 12

fn zeros<T is numeric>(count: int): [T]

count numeric zeros. The literal 0 takes on the requested numeric type T.

fn ones<T is numeric>(count: int): [T]

count numeric ones (the literal 1 takes on the numeric type T).

fn full<T>(count: int, value: T): [T]

count copies of value; a named alias for repeat.

fn sum(values: [int]): int

Sum of an int array (free-function form).

method<T is numeric> [T].sum(): T

Sum of a numeric array as a method. result starts at 0 typed as T.

Example:

[1, 2, 3].sum()  // 6

method<T is numeric> [T].product(): T

Product of all elements (starts from the multiplicative identity 1).

Example:

[1, 2, 3, 4].product()  // 24

method<T is numeric> [T].min(): T

The minimum element (assumes at least one element; seeds with index 0).

Example:

[3, 1, 4, 1, 5].min()  // 1

method<T is numeric> [T].max(): T

The maximum element (seeds with the first element, then scans the rest).

Example:

[3, 1, 4, 1, 5].max()  // 5

method<T is numeric> [T].argmin(): int

The index of the minimum element (argmin).

method<T is numeric> [T].argmax(): int

The index of the maximum element (argmax).

method [bool].all(): bool

True when every element of a bool array is true (logical AND-reduce).

Example:

[true, true, false].all()  // 0

method [bool].any(): bool

True when any element of a bool array is true (logical OR-reduce).

Example:

[false, false, true].any()  // 1

fn sum(values: [real64]): real64

Sum of a real64 array (free-function overload; result seeded as 0.0).

assert

Assertion helpers for tests.

Tiny assertion helpers used by tests. The is_*/equals_* helpers return a bool the caller can check; the assert_* helpers abort the current test via panic when they fail, and are meant for test "..." { ... } blocks run by dune test. Import them with from assert import assert_eq, assert_true, assert_false; to call them unqualified inside tests.

assert provides two families of helpers. The is_*/equals_* predicates return a bool so the caller can decide how to report the result — handy in examples or ad-hoc checks. The assert_* helpers are meant for test blocks: each one calls runtime.panic when its check fails, which aborts the current test and marks it failed under dune test.

Import the assertions with from assert import assert_eq, assert_true, assert_false; to call them unqualified inside tests, or import assert; to reach every helper through the module name.

from assert import assert_eq, assert_true, assert_false;

test "arithmetic" {
    assert_eq(6 * 7, 42);         // any comparable type
    assert_true(2 + 2 == 4);
    assert_false(2 + 2 == 5);
}

The boolean predicates are still available when a returned value is more convenient than a panic:

import assert;

print(assert.is_true(2 + 2 == 4));
print(assert.equals_int(6 * 7, 42));
print(assert.equals_text("dune", "dune"));

Auto-generated from stdlib/assert.dn by tools/gen_stdlib_docs.py.

fn is_true(value: bool): bool

True when value is already true (identity predicate on a bool).

Example:

assert.is_true(true)  // 1

fn is_false(value: bool): bool

True when value is false (logical negation of the input).

Example:

assert.is_false(false)  // 1

fn equals_int(actual: int, expected: int): bool

True when the observed integer actual matches the expected integer.

Example:

assert.equals_int(2, 2)  // 1

fn equals_text(actual: text, expected: text): bool

True when the observed text actual matches the expected text.

Example:

assert.equals_text("a", "a")  // 1

fn assert_true(value: bool): unit

Fail the current test unless value is true; a failure aborts the test.

Example:

assert.assert_true(2 + 2 == 4)

fn assert_false(value: bool): unit

Fail the current test unless value is false; a failure aborts the test.

Example:

assert.assert_false(2 + 2 == 5)

fn assert_eq<T>(actual: T, expected: T): unit

Fail the current test unless actual equals expected, for any comparable type T; a failure aborts the test.

Example:

assert.assert_eq(2 + 2, 4)

autograd

Scalar reverse-mode automatic differentiation.

autograd is a scalar reverse-mode automatic differentiation module. Each Value stores its forward data, accumulated grad, whether it requires_grad, and the parent links needed to propagate derivatives backward through a computation graph.

Create differentiable inputs with variable or value, constants with constant, then build expressions with arithmetic helpers such as add, mul, div, pow, relu, tanh, exp, ln, and sqrt. Calling backward on an output seeds its gradient with 1.0 and fills in the gradients of upstream variables.

import autograd;

x = autograd.variable(2.0);
y = autograd.variable(3.0);

loss = x.mul(y).add(x.pow(2.0)).add(1.0);
loss.backward();

print(loss.data);
print(x.grad);
print(y.grad);

Auto-generated from stdlib/autograd.dn by tools/gen_stdlib_docs.py.

record Value

A node in the autodiff graph: its numeric value plus the edges to its inputs.

Fields:

  • data: real64, // the forward (computed) value
  • grad: real64, // accumulated gradient after backward()
  • requires_grad: bool, // whether gradient should flow through this node

fn variable(data: real64): Value

Create a leaf variable (participates in gradients, no parents).

Example:

autograd.variable(3.0).data  // 3

fn constant(data: real64): Value

Create a leaf constant (does not participate in gradients).

Example:

autograd.constant(5.0).data  // 5

fn value(data: real64): Value

Alias for variable: the default way to introduce a differentiable value.

Example:

autograd.value(2.0).data  // 2

fn data(value: Value): real64

Read a node's forward value.

fn grad(value: Value): real64

Read a node's accumulated gradient.

fn add(left: Value, right: Value): Value

Addition: d/dleft = 1, d/dright = 1.

Example:

autograd.variable(2.0).add(autograd.variable(3.0)).data  // 5

fn add(left: Value, right: real64): Value

Addition with a plain scalar on the right (wrapped as a constant).

fn add(left: real64, right: Value): Value

Addition with a plain scalar on the left.

fn sub(left: Value, right: Value): Value

Subtraction: d/dleft = 1, d/dright = -1.

fn sub(left: Value, right: real64): Value

Subtraction with a scalar right operand.

fn sub(left: real64, right: Value): Value

Subtraction with a scalar left operand.

fn mul(left: Value, right: Value): Value

Multiplication: d/dleft = right.data, d/dright = left.data (product rule).

Example:

autograd.variable(3.0).mul(autograd.variable(4.0)).data  // 12

fn mul(left: Value, right: real64): Value

Multiplication with a scalar right operand.

fn mul(left: real64, right: Value): Value

Multiplication with a scalar left operand.

fn div(left: Value, right: Value): Value

Division: d/dleft = 1/right, d/dright = -left/right^2 (quotient rule).

Example:

autograd.variable(10.0).div(autograd.variable(4.0)).data  // 2.5

fn div(left: Value, right: real64): Value

Division with a scalar denominator.

fn div(left: real64, right: Value): Value

Division with a scalar numerator.

fn neg(value: Value): Value

Negation: d/dvalue = -1.

fn pow(base: Value, exponent: real64): Value

Power with a constant exponent: d/dbase = exponent * base^(exponent-1).

Example:

autograd.variable(3.0).pow(2.0).data  // 9

fn relu(value: Value): Value

ReLU: passes positives through (derivative 1) and clamps negatives to 0.

Example:

autograd.variable(0.0 - 5.0).relu().data  // 0

fn tanh(value: Value): Value

Hyperbolic tangent computed from exp, with derivative 1 - tanh^2.

Example:

autograd.variable(0.0).tanh().data  // 0

fn exp(value: Value): Value

Exponential: d/dvalue = exp(value), which equals the forward result itself.

Example:

autograd.variable(0.0).exp().data  // 1

fn ln(value: Value): Value

Natural log: d/dvalue = 1/value.

Example:

autograd.variable(1.0).ln().data  // 0

fn sqrt(value: Value): Value

Square root: d/dvalue = 0.5 / sqrt(value).

Example:

autograd.variable(9.0).sqrt().data  // 3

fn zero_grad(value: Value): unit

Reset gradients throughout the graph feeding into value.

fn backward(value: Value): unit

Run a full backward pass: clear old gradients, then seed the output with 1.0 and propagate. After this, each input's grad holds d(value)/d(input).

Example:

autograd.variable(3.0).backward()

canvas

Deterministic SVG canvas and immediate-mode GUI widgets.

canvas is a pure-Dune immediate-mode drawing layer for scripts that need more than charts. It records deterministic drawing commands, renders them to SVG, can save the SVG through fs, and can open it in the VM native canvas window where that backend is available.

The module includes low-level primitives (line, polyline, polygon, rect, rounded_rect, circle, ellipse, path, image, and text alignment helpers) and higher-level GUI widgets (panel, button, checkbox, toggle, slider, progress, tabs, toolbar, badge, input, select_box, metric, and table). The widgets are rendered as SVG, so they are stable in tests and safe in headless runs.

Preview — a GUI scene built from canvas widgets (panel, button, checkbox, slider, progress, tabs, badge, metric) and rendered by canvas.svg():

Controls Tools Run Stop snap zoom 62 Draw Data View LIVE fps 60

Auto-generated from stdlib/canvas.dn by tools/gen_stdlib_docs.py.

record Style

Methods:

  • static fn default(): Style — Build the default drawing style: dark stroke, no fill, one-pixel line.
  • fn with_stroke(color: text): Style — Return a copy with a different stroke color.
  • fn with_fill(color: text): Style — Return a copy with a different fill color.
  • fn with_width(width: real64): Style — Return a copy with a different stroke width.
  • fn with_font_size(size: int): Style — Return a copy with a different text size.
  • fn with_opacity(opacity: real64): Style — Return a copy with a different opacity.
  • fn with_dash(dash: text): Style — Return a copy with an SVG stroke-dasharray such as "4 2".

record Canvas

Methods:

  • static fn new(title: text, width: int, height: int): Canvas — Create an empty canvas with an explicit pixel size.
  • fn title(value: text): Canvas — Return a copy with a new native window / SVG title.
  • fn background(color: text): Canvas — Return a copy with a new background color. — e.g. canvas.new("s", 100, 100).background("#f8fafc").to_svg().contains("#f8fafc") // 1
  • fn view_box(x: real64, y: real64, width: real64, height: real64): Canvas — Return a copy with a custom SVG viewBox.
  • fn clear(): Canvas — Drop all drawing commands but keep canvas size, title, background, and viewBox.
  • fn line(x1: real64, y1: real64, x2: real64, y2: real64, style: Style): Canvas — Draw a straight line.
  • fn line(x1: real64, y1: real64, x2: real64, y2: real64, color: text): Canvas — Draw a straight line with a stroke color. — e.g. canvas.new("s", 100, 100).line(0.0, 0.0, 50.0, 50.0, "#000").to_svg().contains("<line") // 1
  • fn polyline(xs: [real64], ys: [real64], style: Style): Canvas — Draw a connected polyline.
  • fn polyline(xs: [real64], ys: [real64], color: text): Canvas — Draw a connected polyline with a stroke color.
  • fn polygon(xs: [real64], ys: [real64], style: Style): Canvas — Draw a closed polygon.
  • fn polygon(xs: [real64], ys: [real64], stroke_color: text, fill_color: text): Canvas — Draw a closed polygon with stroke and fill colors.
  • fn rect(x: real64, y: real64, width: real64, height: real64, style: Style): Canvas — Draw a rectangle.
  • fn rect(x: real64, y: real64, width: real64, height: real64, stroke_color: text, fill_color: text): Canvas — Draw a rectangle with stroke and fill colors. — e.g. canvas.new("s", 100, 100).rect(10.0, 10.0, 40.0, 30.0, "#000", "#eee").to_svg().contains("<rect") // 1
  • fn rounded_rect(x: real64, y: real64, width: real64, height: real64, radius: real64, style: Style): Canvas — Draw a rounded rectangle.
  • fn rounded_rect(x: real64, y: real64, width: real64, height: real64, radius: real64, stroke_color: text, fill_color: text): Canvas — Draw a rounded rectangle with stroke and fill colors.
  • fn circle(cx: real64, cy: real64, radius: real64, style: Style): Canvas — Draw a circle.
  • fn circle(cx: real64, cy: real64, radius: real64, stroke_color: text, fill_color: text): Canvas — Draw a circle with stroke and fill colors. — e.g. canvas.new("s", 100, 100).circle(50.0, 50.0, 20.0, "#000", "none").to_svg().contains("<circle") // 1
  • fn ellipse(cx: real64, cy: real64, rx: real64, ry: real64, style: Style): Canvas — Draw an ellipse.
  • fn ellipse(cx: real64, cy: real64, rx: real64, ry: real64, stroke_color: text, fill_color: text): Canvas — Draw an ellipse with stroke and fill colors.
  • fn text(x: real64, y: real64, value: text, style: Style): Canvas — Draw left-aligned text.
  • fn text(x: real64, y: real64, value: text, color: text): Canvas — Draw left-aligned text with a fill color. — e.g. canvas.new("s", 100, 100).text(10.0, 20.0, "hi", "#000").to_svg().contains(">hi<") // 1
  • fn text_center(x: real64, y: real64, value: text, style: Style): Canvas — Draw center-aligned text.
  • fn text_right(x: real64, y: real64, value: text, style: Style): Canvas — Draw right-aligned text.
  • fn path(data: text, style: Style): Canvas — Draw a raw SVG path. Useful for icons and custom shapes.
  • fn image(x: real64, y: real64, width: real64, height: real64, href: text): Canvas — Draw an SVG image reference.
  • fn grid(step: real64, color: text): Canvas — Draw a background grid aligned to the current viewBox. — e.g. canvas.new("s", 100, 100).grid(20.0, "#eee").to_svg().contains("canvas-grid") // 1
  • fn axes(origin_x: real64, origin_y: real64, color: text): Canvas — Draw horizontal and vertical axes at the given origin.
  • fn point(x: real64, y: real64, radius: real64, color: text): Canvas — Draw a filled point.
  • fn arrow(x1: real64, y1: real64, x2: real64, y2: real64, color: text): Canvas — Draw a line with a simple endpoint marker.
  • fn panel(x: real64, y: real64, width: real64, height: real64, title: text): Canvas — Draw a titled panel for dashboards and tool surfaces. — e.g. canvas.new("s", 100, 100).panel(4.0, 4.0, 80.0, 40.0, "Box").to_svg().contains(">Box<") // 1
  • fn button(x: real64, y: real64, width: real64, height: real64, label: text, active: bool): Canvas — Draw a button. The active state uses a stronger fill and inverted text. — e.g. canvas.new("s", 100, 100).button(10.0, 10.0, 60.0, 24.0, "OK", true).to_svg().contains(">OK<") // 1
  • fn checkbox(x: real64, y: real64, label: text, checked: bool): Canvas — Draw a checkbox with a label. — e.g. canvas.new("s", 100, 100).checkbox(10.0, 10.0, "on", true).to_svg().contains(">on<") // 1
  • fn toggle(x: real64, y: real64, width: real64, label: text, on: bool): Canvas — Draw a compact on/off toggle.
  • fn slider(x: real64, y: real64, width: real64, min: real64, max: real64, value: real64, label: text): Canvas — Draw a slider with a label and current value. — e.g. canvas.new("s", 200, 60).slider(10.0, 30.0, 150.0, 0.0, 100.0, 42.0, "zoom").to_svg().contains(">zoom<") // 1
  • fn progress(x: real64, y: real64, width: real64, height: real64, value: real64, color: text): Canvas — Draw a 0..1 progress bar.
  • fn tabs(x: real64, y: real64, width: real64, labels: [text], active_index: int): Canvas — Draw a row of tabs. The active index is zero-based.
  • fn toolbar(x: real64, y: real64, labels: [text], active_index: int): Canvas — Draw a toolbar as a sequence of compact buttons.
  • fn badge(x: real64, y: real64, label: text, color: text): Canvas — Draw a small status badge.
  • fn input(x: real64, y: real64, width: real64, value: text, placeholder: text): Canvas — Draw a text input-like field with a placeholder.
  • fn select_box(x: real64, y: real64, width: real64, label: text, value: text): Canvas — Draw a read-only select box.
  • fn metric(x: real64, y: real64, width: real64, height: real64, label: text, value: text): Canvas — Draw a metric tile with a label and large value.
  • fn table(x: real64, y: real64, column_widths: [real64], headers: [text], rows: [[text]]): Canvas — Draw a simple table with fixed column widths.
  • fn to_svg(): text — Render to deterministic SVG text. — e.g. canvas.new("s", 100, 80).to_svg().starts_with("<svg") // 1
  • fn save_svg(path: text): outcome.Outcome<text, text> — Save the SVG document to disk. — e.g. canvas.new("s", 100, 80).save_svg("scene.svg").is_done() // 1
  • fn show_native(): outcome.Outcome<text, text> — Open the SVG in the VM native canvas window when supported.

fn new(title: text, width: int, height: int): Canvas

Create an empty canvas with an explicit pixel size.

Example:

canvas.svg(canvas.new("scene", 320, 200)).contains("width=\"320\"")  // 1

fn new(title: text): Canvas

Create an empty canvas with the default size.

Example:

canvas.new("scene")

fn style(stroke_color: text, fill_color: text, stroke_width: real64): Style

Build a custom style from stroke, fill, and stroke width.

Example:

canvas.style("#0f172a", "#eef2ff", 2.0)

fn stroke(color: text): Style

Build a stroke-only style.

Example:

canvas.stroke("#2563eb").with_width(2.0)

fn fill(color: text): Style

Build a fill-only style.

Example:

canvas.fill("#dbeafe")

fn svg(scene: Canvas): text

Render a canvas to deterministic SVG text.

Example:

canvas.svg(canvas.new("s", 100, 80)).starts_with("<svg")  // 1

fn native_tools(): [text]

List the interactive tools exposed by the native canvas window.

Example:

canvas.native_tools().len()  // 7

cli

Command-line argument parsing and help output.

cli is a declarative command-line argument parser written in pure Dune. You describe a command once — its options, flags, and positionals — and cli parses a raw [text] argument list into a typed ParseResult, generates --help and --version output, and reports argument errors as an Outcome.

The pieces

  • cli.command(name) starts a builder. Chain .about(...) and .version(...) for help/version text.
  • Options take a value: .option(name, short, description, default) (uses the default when omitted) or .required_option(name, short, description) (fails to parse when missing). Read them back with result.text(name), .as_int(name), .as_real64(name), or .as_bool(name) — each returns a Maybe.
  • Flags are boolean switches: .flag(name, short, description), read with result.flag(name)bool.
  • Positionals are order-based: .positional(name, description) (required) or .optional_positional(...), read with result.positional(name) or result.positional(index)Maybe<text>.
  • .parse(args) returns Outcome<ParseResult, text>: Done with the parsed result, or Failed with a message like unexpected positional argument 'extra'. --help/--version short-circuit; check result.is_help() / result.is_version() and print result.output_text().

Example

import io;
import cli;

parser = cli.command("greet")
    .about("Greet someone by name")
    .version("1.0.0")
    .option("name", "n", "Who to greet", "world")
    .flag("shout", "s", "Upper-case the greeting")
    .positional("count", "How many times to greet");

result = parser.parse(["--name", "Ada", "-s", "3"]).value_or(cli.empty_result());

io.println(result.text("name").value_or("?"));       // Ada
io.println(result.flag("shout"));                     // 1  (true)
io.println(result.positional("count").value_or("?")); // 3

A missing required option or a stray positional is reported instead of parsed:

import io;
import cli;

parser = cli.command("greet").positional("count", "count");
io.println(parser.parse(["one", "two"]).failure_or("ok")); // unexpected positional argument 'two'

Auto-generated from stdlib/cli.dn by tools/gen_stdlib_docs.py.

record OptionSpec

A command-line option that accepts a text value.

record FlagSpec

A command-line flag that is either present or absent.

record PositionalSpec

A positional argument accepted after options and flags.

record ParsedValue

Parsed value for a named option.

record ParsedFlag

Parsed value for a named flag.

record ParsedPositional

Parsed value for a positional argument.

record ParseResult

Result returned by Command.parse().

Methods:

  • fn text(name: text): maybe.Maybe<text> — Return the option value named name, if it was supplied or defaulted. — e.g. cli.command("g").option("name", "n", "Name", "world").parse(["--name", "dune"]).value_or(cli.empty_result()).text("name").value_or("") // dune
  • fn as_int(name: text): maybe.Maybe<int> — Parse a named option as an int. Invalid or absent values return Absent. — e.g. cli.command("g").option("count", "c", "Count", "1").parse(["--count", "3"]).value_or(cli.empty_result()).as_int("count").value_or(0) // 3
  • fn as_bool(name: text): maybe.Maybe<bool> — Parse a named option or flag as a bool. — e.g. cli.command("t").option("wide", "w", "", "false").parse(["--wide", "true"]).value_or(cli.empty_result()).as_bool("wide").value_or(false) // 1
  • fn as_real64(name: text): maybe.Maybe<real64> — Parse a named option as a real64. Invalid or absent values return Absent. — e.g. cli.command("t").option("ratio", "r", "", "1.0").parse(["--ratio", "2.5"]).value_or(cli.empty_result()).as_real64("ratio").value_or(0.0) // 2.5
  • fn flag(name: text): bool — True when a flag was supplied. — e.g. cli.command("g").flag("v", "v", "").parse(["-v"]).value_or(cli.empty_result()).flag("v") // 1
  • fn positional(index: int): maybe.Maybe<text> — Positional value by index. — e.g. cli.command("t").positional("path", "").parse(["src"]).value_or(cli.empty_result()).positional(0).value_or("?") // src
  • fn positional(name: text): maybe.Maybe<text> — Positional value by declared name. — e.g. cli.command("t").positional("path", "").parse(["src"]).value_or(cli.empty_result()).positional("path").value_or("?") // src
  • fn is_help(): bool — True when parsing stopped for --help or -h. — e.g. cli.command("t").parse(["--help"]).value_or(cli.empty_result()).is_help() // 1
  • fn is_version(): bool — True when parsing stopped for --version. — e.g. cli.command("t").version("1.0").parse(["--version"]).value_or(cli.empty_result()).is_version() // 1
  • fn output_text(): text — Help or version text produced by --help / --version. — e.g. cli.command("greet").version("1.0.0").parse(["--version"]).value_or(cli.empty_result()).output_text() // greet 1.0.0

record Command

Builder for command-line parsers.

Methods:

  • fn about(description: text): Command — Set the one-line command summary shown at the top of --help. — e.g. cli.command("greet").about("Greet someone by name")
  • fn version(version_text: text): Command — Set the version string; --version prints " ". — e.g. cli.command("greet").version("1.0.0").version_output() // greet 1.0.0
  • fn option(name: text, short: text, description: text, default_value: text): Command — Add an option with a default value. — e.g. cli.command("g").flag("v", "v", "").option("name", "n", "Name", "world").parse(["-v"]).value_or(cli.empty_result()).text("name").value_or("") // world
  • fn required_option(name: text, short: text, description: text): Command — Add a required option that has no default.
  • fn flag(name: text, short: text, description: text): Command — Add a boolean flag. — e.g. cli.command("g").flag("verbose", "v", "Verbose").parse(["-v"]).value_or(cli.empty_result()).flag("verbose") // 1
  • fn positional(name: text, description: text): Command — Add a required positional argument, read back by name or index. — e.g. cli.command("greet").positional("count", "How many times to greet")
  • fn optional_positional(name: text, description: text): Command — Add an optional positional argument (no parse error when omitted). — e.g. cli.command("greet").optional_positional("suffix", "Optional suffix")
  • fn help_text(): text — Deterministic help text for this command. — e.g. cli.command("greet").help_text().len() // 69
  • fn version_output(): text — Deterministic version text for this command. — e.g. cli.command("tool").version("1.0").version_output() // tool 1.0
  • fn parse(args: [text]): outcome.Outcome<ParseResult, text> — Parse command-line arguments. — e.g. cli.command("g").parse(["extra"]).failure_or("") // unexpected positional argument 'extra'

fn command(name: text): Command

Start a builder for a named command.

Example:

cli.command("greet").version_output()  // greet

fn empty_result(): ParseResult

Empty parse result useful as a value_or fallback.

Example:

cli.empty_result().text("missing").value_or("none")  // none

collections

Shared collection utilities.

Small convenience constructors for building short arrays of ints and text. These are handy shorthands so callers do not have to write array literals by hand for the most common one- and two-element cases.

collections provides a few small constructors for common array shapes. It is intentionally narrow: one- and two-element arrays for int and text, plus a simple repeat_int helper.

Use these helpers in tests, examples, and small programs when named construction is clearer than spelling out an array literal or a loop.

import collections;

pair = collections.pair_int(2, 3);
words = collections.singleton_text("dune");
repeated = collections.repeat_int(4, 3);

print(pair[0] + pair[1]);
print(words[0]);
print(repeated.len());

Auto-generated from stdlib/collections.dn by tools/gen_stdlib_docs.py.

fn singleton_int(value: int): [int]

A one-element int array holding value.

Example:

collections.singleton_int(5)[0]  // 5

fn pair_int(left: int, right: int): [int]

A two-element int array holding left then right.

Example:

collections.pair_int(1, 2)[1]  // 2

fn singleton_text(value: text): [text]

A one-element text array holding value.

Example:

collections.singleton_text("hi")[0]  // hi

fn pair_text(left: text, right: text): [text]

A two-element text array holding left then right.

Example:

collections.pair_text("a", "b")[1]  // b

fn repeat_int(value: int, count: int): [int]

Build an int array that repeats value exactly count times.

Example:

collections.repeat_int(9, 3)[2]  // 9

csv

CSV parsing and numeric-matrix I/O.

Minimal CSV support, layered purely on top of fs and text — no new VM primitives. The first iteration handles comma-separated cells and newline-separated rows (LF or CRLF); quoting and escaping are a follow-up.

csv reads and writes simple comma-separated data using only the Dune standard library. It handles comma-separated cells, LF or CRLF row endings, and trailing newlines; quoted fields and escaped commas are not part of this first implementation.

Use parse_rows when you already have CSV text, read_rows and write_rows for text-cell files, and the matrix helpers when a rectangular numeric file should become a matrix.Matrix<int> or matrix.Matrix<real64>. File and parse failures are returned as Outcome values rather than hidden exceptions.

import csv;

rows = csv.parse_rows("name,score\nada,42\ngrace,37\n");

print(rows.len());
print(rows[1][0]);
print(rows[1][1]);

Auto-generated from stdlib/csv.dn by tools/gen_stdlib_docs.py.

fn parse_rows(content: text): [[text]]

Parse CSV content into rows of text cells. A trailing newline is ignored.

Example:

csv.parse_rows("a,b\n1,2\n")  // [["a", "b"], ["1", "2"]]

fn read_rows(path: text): outcome.Outcome<[[text]], text>

Read a CSV file into rows of text cells. On success returns Done(rows), otherwise Failed(message).

Example:

csv.read_rows("data.csv")

fn read_matrix_real64(path: text): outcome.Outcome<matrix.Matrix<real64>, text>

Convert parsed text rows into a rectangular real64 matrix. Trims each cell, enforces a consistent row width, and reports the first parse or shape error. Returns Done(Matrix) or Failed(message).

Example:

csv.read_matrix_real64("data.csv")

fn read_matrix_int(path: text): outcome.Outcome<matrix.Matrix<int>, text>

Same as read_matrix_real64 but parses cells as integers.

fn write_rows(path: text, rows: [[text]]): outcome.Outcome<text, text>

Write rows of text cells back out as CSV (comma-separated, newline-terminated). Returns Done(path-like text) or Failed(message) from the underlying write.

Example:

csv.write_rows("out.csv", [["a", "b"], ["1", "2"]])

fn write_matrix_real64(path: text, data: matrix.Matrix<real64>): outcome.Outcome<text, text>

Serialize a real64 matrix to CSV and write it to path.

Example:

csv.write_matrix_real64("out.csv", matrix.from_rows([[1.0, 2.0], [3.0, 4.0]]))

dict

A hash-map style dictionary built in Dune.

dict is a generic dictionary from text keys to values of type V. It keeps keys and values in insertion order and stores them in parallel arrays, so the behavior is simple and deterministic across backends.

Use Dict<V> when you need a mutable string-keyed map with explicit optional lookups. set inserts or overwrites, get returns Maybe<V>, and keys and values return fresh arrays in insertion order. copy() creates independent dictionary structure but copies generic values shallowly, following Dune's normal value semantics.

import dict;
import maybe;

scores: dict.Dict<int> = dict.Dict.new();
scores.set("ada", 10);
scores.set("grace", 7);
scores.set("ada", 12);

print(scores.get("ada").value_or(0));
print(scores.contains("grace"));
print(scores.keys().len());

Auto-generated from stdlib/dict.dn by tools/gen_stdlib_docs.py.

record Dict<V>

A small associative collection mapping text keys to V values.

The first iteration keeps keys as text and stores entries in two parallel arrays, so lookups are linear. That is simple, predictable, and identical across backends; hashing can come later without changing this API. Generic over the value type V; keys are always text.

Methods:

  • fn new(): Dict<V> — Construct an empty dictionary. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.len() // 0
  • fn set(key: text, value: V): unit — Insert or overwrite the value for key. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.get("a").value_or(0) // 1
  • fn get(key: text): maybe.Maybe<V> — Look up key, returning Present(value) or Absent. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.get("missing").value_or(0) // 0
  • fn contains(key: text): bool — True when key has an associated value. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.contains("a") // 1
  • fn remove(key: text): bool — Remove key; returns whether a value was actually removed. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.remove("a") // 1
  • fn len(): int — The number of key/value pairs stored.
  • fn is_empty(): bool — True when the dictionary holds no entries.
  • fn clear(): unit — Drop all entries, leaving an empty dictionary.
  • fn copy(): Dict<V> — A shallow copy with fresh key/value arrays. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.copy().len() // 1
  • fn keys(): [text] — A copy of the keys in insertion order. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.set("b", 2); d.keys()
  • fn values(): [V] — A copy of the values in insertion order. — e.g. d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.set("b", 2); d.values()

display

The Display contract and show helper.

display defines the Display contract: a type that promises a to_text(): text method. Records with to_text can already be printed and formatted directly, but declaring with display.Display lets generic functions require that capability explicitly.

Use show when you want the rendered text value instead of printing immediately, especially inside generic code bounded by T is Display.

import display;

record Label with display.Display {
    name: text,

    fn to_text(): text {
        return format("label: {}", this.name);
    }
}

label = Label { name: "core" };
print(display.show(label));

Auto-generated from stdlib/display.dn by tools/gen_stdlib_docs.py.

fn show<T is Display>(value: T): text

Render any Display value to text. Useful when a function wants the text instead of printing it directly. The bound T is Display guarantees value has a to_text method to call.

Example:

display.show(Label { name: "core" })  // label: core

fmt

String formatting with {} placeholders.

Auto-generated from stdlib/fmt.dn by tools/gen_stdlib_docs.py.

const __fmt_module_marker: int

Formatting is compiler-backed for now because Dune does not have variadic user functions yet. Import this module and call fmt.format(...) to use it.

fmt.format(template, ...values) returns a text value: each {} placeholder is filled left-to-right by the next argument. Any value type is accepted — int, real, bool (rendered as 1/0), text, and glyph — and text with no {} is returned unchanged.

Example:

fmt.format("{}-{}", 4, 2)  // "4-2"
fmt.format("{} + {} = {}", 2, 3, 5)  // "2 + 3 = 5"
fmt.format("Hello, {}!", "world")  // "Hello, world!"
fmt.format("half is {}", 1.5)  // "half is 1.5"
fmt.format("flag is {}", true)  // "flag is 1"
fmt.format("letter {}", 'A')  // "letter A"

fs

File-system access (read, write, list).

Minimal file-system access built on the __read_file / __write_file VM intrinsics (dedicated opcodes, not C/C++ foreign functions). This module is pure Dune: it only shapes the primitive (ok, payload) results into the Outcome type so errors stay explicit and compose with ?.

fs provides minimal whole-file text I/O. It wraps the VM file intrinsics in Outcome<text, text> so successful reads and writes are explicit and filesystem errors stay in normal Dune values.

Use read_text to load a file as one text value and write_text to replace a file's contents. Both functions return Done(...) on success and Failed(message) on error, so they compose with Outcome helpers and the ? operator.

import fs;
import outcome;

path = "dune_stdlib_example.txt";
written = fs.write_text(path, "hello from dune\n");

if written.is_done() {
    read_back = fs.read_text(written.value_or(""));
    print(read_back.value_or(""));
} else {
    print(written.failure_or("write failed"));
}

Auto-generated from stdlib/fs.dn by tools/gen_stdlib_docs.py.

fn read_text(path: text): outcome.Outcome<text, text>

Read the whole file at path as text. On success returns Done(contents), otherwise Failed(message).

Example:

fs.read_text("dune_example_tmp.txt")

fn write_text(path: text, content: text): outcome.Outcome<text, text>

Write content to path, replacing any existing file. On success returns Done(path), otherwise Failed(message).

Example:

fs.write_text("dune_example_tmp.txt", "hello")

io

Standard input/output: print, read a line, and flush streams.

Auto-generated from stdlib/io.dn by tools/gen_stdlib_docs.py.

fn write(message: text): outcome.Outcome<text, text>

Write text to stdout without adding a newline.

Example:

io.write("hi")  // prints: hi (no trailing newline)

fn writeln(message: text): outcome.Outcome<text, text>

Write text to stdout followed by \n.

Example:

io.writeln("hi")  // prints: hi

fn err_write(message: text): outcome.Outcome<text, text>

Write text to stderr without adding a newline.

Example:

io.err_write("oops")  // prints to stderr: oops (no trailing newline)

fn err_writeln(message: text): outcome.Outcome<text, text>

Write text to stderr followed by \n.

Example:

io.err_writeln("oops")  // prints to stderr: oops

fn flush(): outcome.Outcome<text, text>

Flush stdout.

Example:

io.flush()  // flushes buffered stdout; returns Done("")

fn flush_err(): outcome.Outcome<text, text>

Flush stderr.

Example:

io.flush_err()  // flushes buffered stderr; returns Done("")

fn read_line(): outcome.Outcome<text, text>

Read one line from stdin without the trailing newline.

EOF is reported as Failed("end of input"); other stream errors are reported as Failed("could not read from stdin").

Example:

io.read_line()  // reads one line from stdin, e.g. Done("hi there")

fn print<T>(value: T): unit

Print a printable value to stdout without newline, ignoring output errors for convenience.

Example:

io.print("hi")  // prints: hi (no trailing newline)

fn println<T>(value: T): unit

Print a printable value to stdout with a trailing newline, ignoring output errors.

Example:

io.println("hello, world")  // prints: hello, world

fn eprint<T>(value: T): unit

Print a printable value to stderr without newline, ignoring output errors.

Example:

io.eprint("oops")  // prints to stderr: oops (no trailing newline)

fn eprintln<T>(value: T): unit

Print a printable value to stderr with a trailing newline, ignoring output errors.

Example:

io.eprintln("oops")  // prints to stderr: oops

fn prompt(message: text): outcome.Outcome<text, text>

Write a prompt to stdout, then read one line from stdin.

Example:

io.prompt("name? ")  // prints "name? ", then reads a line from stdin

log

Levelled diagnostics with stderr output and filtering.

Structured diagnostics for Dune programs.

The first stage exposes levelled human logging with process-wide filtering. Logs are written to stderr by the VM __log_emit intrinsic so normal program output on stdout stays separate. The active level defaults to info, can be set from DUNE_LOG or DUNE_LOG_LEVEL, and can be changed at runtime with set_level.

log provides a small diagnostics API for command-line tools and longer-running programs. It keeps logs on stderr, separate from regular io.println output on stdout, and filters messages below the active level.

The default level is info. Set DUNE_LOG or DUNE_LOG_LEVEL to trace, debug, info, warn, error, or off, or call log.set_level from Dune code.

import log;

log.set_level(log.DEBUG);
log.info("building target");
log.warn("using fallback config");

Auto-generated from stdlib/log.dn by tools/gen_stdlib_docs.py.

const TRACE: int

The most verbose level.

Example:

log.set_level(log.TRACE)

const DEBUG: int

Debug-level diagnostics.

const INFO: int

Informational diagnostics; this is the default active level.

const WARN: int

Warnings for recoverable problems.

const ERROR: int

Errors for failed operations that the program can still report.

const OFF: int

Disable all log output.

fn set_level(level: int): unit

Set the active minimum level. Messages below this level are suppressed.

Example:

log.set_level(log.WARN)

fn level(): int

Return the active minimum level (INFO = 2 by default).

Example:

log.level()  // 2

fn enabled(level: int): bool

True when a message at level would be emitted at the active level.

Example:

log.enabled(log.ERROR)  // 1

fn trace(message: text): unit

Emit a trace message (suppressed unless the level is lowered to TRACE).

Example:

log.set_level(log.TRACE); log.trace("parsing input")  // stderr: [trace] parsing input

fn debug(message: text): unit

Emit a debug message (suppressed unless the level is lowered to DEBUG).

Example:

log.set_level(log.DEBUG); log.debug("cache miss")  // stderr: [debug] cache miss

fn info(message: text): unit

Emit an informational message (shown at the default level).

Example:

log.info("building target")  // stderr: [info] building target

fn warn(message: text): unit

Emit a warning message.

Example:

log.warn("using fallback config")  // stderr: [warn] using fallback config

fn error(message: text): unit

Emit an error message.

Example:

log.error("connection refused")  // stderr: [error] connection refused

math

Numeric constants and generic math functions.

Pure-Dune math: named constants plus elementary functions approximated with Taylor/Maclaurin series, Newton iteration, and range reduction. Nothing here calls native code, so results are identical across backends.

math is a pure-Dune numeric module. It exposes real constants such as PI, TAU, and E, generic helpers such as square, cube, abs, min, max, and clamp, and elementary real functions implemented with series expansion, Newton iteration, and range reduction.

Use it when you need portable, deterministic numeric behavior. The functions are intentionally small and self-contained; they do not call a native math library.

import io;
import math;

io.println(math.square(7));
io.println(math.clamp(15, 0, 10));
io.println(math.sqrt(81.0));
io.println(math.round(math.PI));

Auto-generated from stdlib/math.dn by tools/gen_stdlib_docs.py.

const PI: real64

High-precision real64 constants.

const TAU: real64

const E: real64

const INVERSE_E: real64

const PI32: real32

Lower-precision real32 mirrors of the same constants.

const TAU32: real32

const E32: real32

const INVERSE_E32: real32

fn square<T is numeric>(value: T): T

Square of value (generic over any numeric type T).

Example:

math.square(7)  // 49

fn cube<T is numeric>(value: T): T

Cube of value.

Example:

math.cube(3)  // 27

fn abs<T is numeric>(value: T): T

Absolute value: negate when the input is negative.

Example:

math.abs(0 - 5)  // 5

fn min<T is numeric>(left: T, right: T): T

The smaller of two numbers.

Example:

math.min(3, 8)  // 3

fn max<T is numeric>(left: T, right: T): T

The larger of two numbers.

Example:

math.max(3, 8)  // 8

fn clamp<T is numeric>(value: T, lower: T, upper: T): T

Constrain value to the inclusive range [lower, upper].

Example:

math.clamp(15, 0, 10)  // 10

fn sqrt<T is real>(value: T): T

Square root via Newton's method (real types only).

Example:

math.sqrt(81.0)  // 9

fn normalize_radians<T is real>(value: T): T

Reduce an angle into (-pi, pi] so the sin/cos series converge quickly.

fn sin<T is real>(value: T): T

Sine via the Maclaurin series after range reduction.

Example:

math.sin(0.0)  // 0

fn cos<T is real>(value: T): T

Cosine via the Maclaurin series after range reduction.

Example:

math.cos(0.0)  // 1

fn tan<T is real>(value: T): T

Tangent as sine over cosine.

fn exp<T is real>(value: T): T

Exponential e^value using range reduction plus the Taylor series.

Example:

math.exp(0.0)  // 1

fn ln<T is real>(value: T): T

Natural logarithm via range reduction and the artanh series.

Example:

math.ln(1.0)  // 0

fn pow<T is real>(base: T, exponent: int): T

Integer power: base raised to an integer exponent by repeated multiplication.

Example:

math.pow(2.0, 10)  // 1024

fn pow<T is real>(base: T, exponent: T): T

Real power: base^exponent for a real exponent via exp(exponent * ln(base)). This overload is chosen when the exponent has the same real type as the base.

fn floor<T is real>(value: T): T

Largest whole number not greater than value.

Example:

math.floor(3.7)  // 3

fn ceil<T is real>(value: T): T

Smallest whole number not less than value.

Example:

math.ceil(3.2)  // 4

fn round<T is real>(value: T): T

Round to the nearest whole number (halves round away from zero).

Example:

math.round(2.5)  // 3

matrix

A small NumPy-style foundation: vectors and matrices.

matrix provides dense numeric Vector<T> and Matrix<T> records backed by flat arrays. It covers construction, shape checks, indexing, copying, reshaping, slicing, transposition, elementwise arithmetic, reductions, dot products, matrix multiplication, norms, and small determinants.

Use vectors for one-dimensional numeric data and matrices for row-major two-dimensional data. Constructors such as vector, from_rows, from_flat, zeros, ones, identity, and diagonal keep setup explicit, while methods perform shape validation before operations that require compatible dimensions.

Ordinary assignment aliases a vector or matrix record. Use copy() when the numeric backing array must be independent; see Dune's value semantics. vector(data) and from_flat(rows, cols, data) intentionally wrap data without copying it, while from_rows(rows) creates a fresh flat backing array.

import io;
import matrix;

left = matrix.from_rows([[1, 2, 3], [4, 5, 6]]);
right = matrix.from_rows([[7, 8], [9, 10], [11, 12]]);

product = matrix.dot(left, right);
id: matrix.Matrix<int> = matrix.identity(2);

io.println(product.rows());
io.println(product.get(0, 0));
io.println(id.trace());

Operators

Vector and Matrix implement the operator methods, so arithmetic reads naturally (see operator overloading):

import io;
import matrix;

v = matrix.vector([1, 2, 3]);
w = matrix.vector([4, 5, 6]);

io.println((v + w).get(2));   // 9   -> Vector.add
io.println((v * w).get(1));   // 10  -> Vector.mul (element-wise)
io.println((v * 10).get(0));  // 10  -> Vector.mul (scalar)

+/- add and subtract same-shape vectors/matrices; * is element-wise (or a scalar scale). Matrix multiplication stays explicit as matrix.dot(a, b) / a.matmul(b) so it is never confused with element-wise *.

Auto-generated from stdlib/matrix.dn by tools/gen_stdlib_docs.py.

record Vector<T is numeric>

A fixed-length numeric vector backed by a flat array data.

Methods:

  • fn new(data: [T]): Vector<T> — Wrap an existing array as a Vector (no copy).
  • fn len(): int — Number of elements.
  • fn shape(): [int] — Shape as a one-element array [length], mirroring Matrix.shape().
  • fn is_empty(): bool — True when the vector has no elements.
  • fn get(index: int): T — Element at index.
  • fn set(index: int, value: T): unit — Overwrite the element at index in place.
  • fn to_array(): [T] — A plain array copy of the elements.
  • fn copy(): Vector<T> — A copy of this vector with a fresh numeric backing array.
  • fn equals(other: Vector<T>): bool — Element-wise equality with other (same length and same values).
  • fn same_shape(other: Vector<T>): bool — True when other has the same length (shape) as this vector.
  • fn slice(start: int, end: int): Vector<T> — A sub-vector over [start, end) as a new vector.
  • fn concat(other: Vector<T>): Vector<T> — This vector followed by other, as a new vector.
  • fn fill(value: T): unit — Overwrite every element with value in place.
  • fn add(other: Vector<T>): Vector<T> — Element-wise addition of two equal-length vectors. — e.g. matrix.vector([1, 2, 3]).add(matrix.vector([10, 20, 30]))
  • fn add(value: T): Vector<T> — Add a scalar value to every element (broadcast).
  • fn sub(other: Vector<T>): Vector<T> — Element-wise subtraction of two equal-length vectors.
  • fn sub(value: T): Vector<T> — Subtract a scalar value from every element (broadcast).
  • fn rsub(value: T): Vector<T> — Reverse-subtract: each element becomes value minus the element.
  • fn mul(other: Vector<T>): Vector<T> — Element-wise (Hadamard) product of two equal-length vectors.
  • fn mul(value: T): Vector<T> — Multiply every element by a scalar (delegates to scale).
  • fn div(other: Vector<T>): Vector<T> — Element-wise division of two equal-length vectors.
  • fn div(value: T): Vector<T> — Divide every element by a scalar (broadcast).
  • fn rdiv(value: T): Vector<T> — Reverse-divide: each element becomes value divided by the element.
  • fn scale(factor: T): Vector<T> — Multiply every element by scalar factor (the core of mul-by-scalar). — e.g. matrix.vector([1, 2, 3]).scale(2)
  • fn neg(): Vector<T> — Negate every element.
  • fn abs(): Vector<T> — Absolute value of every element.
  • fn clip(lower: T, upper: T): Vector<T> — Clamp every element into the inclusive range [lower, upper].
  • fn dot(other: Vector<T>): T — Dot product with other: sum of element-wise products. — e.g. matrix.vector([1, 2, 3]).dot(matrix.vector([4, 5, 6])) // 32
  • fn norm_squared(): T — Squared Euclidean length (dot product with itself).
  • fn norm(): real64 — Euclidean length: sqrt of the squared norm, as real64. — e.g. matrix.vector([3, 4]).norm() // 5
  • fn distance_squared(other: Vector<T>): T — Squared distance to other (norm_squared of the difference).
  • fn distance(other: Vector<T>): real64 — Euclidean distance to other, as real64.
  • fn sum(): T — Sum of all elements (starts from additive identity 0). — e.g. matrix.vector([1, 2, 3, 4]).sum() // 10
  • fn product(): T — Product of all elements (starts from multiplicative identity 1).
  • fn mean(): real64 — Arithmetic mean as real64 (panics on an empty vector). — e.g. matrix.vector([1, 2, 3, 4]).mean() // 2.5
  • fn min(): T — Minimum element (seeded with element 0; panics if empty).
  • fn max(): T — Maximum element (seeded with element 0; panics if empty).
  • fn argmin(): int — Index of the minimum element (argmin).
  • fn argmax(): int — Index of the maximum element (argmax).
  • fn to_row_matrix(): Matrix<T> — View this vector as a 1-by-n row matrix.
  • fn to_column_matrix(): Matrix<T> — View this vector as an n-by-1 column matrix.
  • fn reshape(rows: int, cols: int): Matrix<T> — Reshape the elements into a rows-by-cols matrix (sizes must match).
  • fn dot(other: Matrix<T>): Vector<T> — Row-vector times matrix, spelled as dot (delegates to matmul).
  • fn matmul(other: Matrix<T>): Vector<T> — Treat this vector as a row and multiply by matrix other (1xn * nxm).
  • fn outer(other: Vector<T>): Matrix<T> — Outer product: an m-by-n matrix where entry (i,j) = this[i] * other[j].

record Matrix<T is numeric>

A dense 2-D matrix stored row-major in a flat array of length rows*cols.

Methods:

  • fn new(rows: int, cols: int, data: [T]): Matrix<T> — Construct a matrix, validating dimensions and data length.
  • fn rows(): int — Number of rows.
  • fn cols(): int — Number of columns.
  • fn shape(): [int] — Shape as [rows, cols].
  • fn is_empty(): bool — True when the matrix holds no elements.
  • fn is_square(): bool — True when the matrix is square (rows == cols).
  • fn len(): int — Total number of elements (rows*cols).
  • fn get(row: int, col: int): T — Element at (row, col).
  • fn set(row: int, col: int, value: T): unit — Overwrite the element at (row, col) in place.
  • fn to_array(): [T] — A plain row-major array copy of all elements.
  • fn copy(): Matrix<T> — A copy of this matrix with a fresh numeric backing array.
  • fn equals(other: Matrix<T>): bool — Element-wise equality with other (same shape and same values).
  • fn same_shape(other: Matrix<T>): bool — True when other has the same rows and cols.
  • fn can_matmul(other: Matrix<T>): bool — True when this matrix can be multiplied by other (cols == other.rows).
  • fn fill(value: T): unit — Overwrite every element with value in place.
  • fn row(row: int): Vector<T> — Extract row row as a Vector.
  • fn column(col: int): Vector<T> — Extract column col as a Vector.
  • fn flatten(): Vector<T> — Flatten all elements into a single Vector (row-major order).
  • fn reshape(rows: int, cols: int): Matrix<T> — Reshape into new dimensions preserving the element order (sizes must match).
  • fn diagonal(): Vector<T> — The main diagonal (i,i) as a Vector, truncated to the shorter dimension.
  • fn diag(): Vector<T> — Alias for diagonal.
  • fn trace(): T — Trace: the sum of the diagonal entries. — e.g. matrix.from_rows([[1, 2], [3, 4]]).trace() // 5
  • fn add(other: Matrix<T>): Matrix<T> — Element-wise matrix addition (same shape required). — e.g. matrix.from_rows([[1, 2], [3, 4]]).add(matrix.from_rows([[10, 20], [30, 40]]))
  • fn add(value: T): Matrix<T> — Add a scalar to every element (broadcast).
  • fn sub(other: Matrix<T>): Matrix<T> — Element-wise matrix subtraction (same shape required).
  • fn sub(value: T): Matrix<T> — Subtract a scalar from every element (broadcast).
  • fn rsub(value: T): Matrix<T> — Reverse-subtract: each element becomes value minus the element.
  • fn mul(other: Matrix<T>): Matrix<T> — Element-wise (Hadamard) product of two same-shape matrices.
  • fn mul(value: T): Matrix<T> — Multiply every element by a scalar (delegates to scale).
  • fn hadamard(other: Matrix<T>): Matrix<T> — Named alias for element-wise multiplication.
  • fn div(other: Matrix<T>): Matrix<T> — Element-wise matrix division (same shape required).
  • fn div(value: T): Matrix<T> — Divide every element by a scalar (broadcast).
  • fn rdiv(value: T): Matrix<T> — Reverse-divide: each element becomes value divided by the element.
  • fn scale(factor: T): Matrix<T> — Multiply every element by scalar factor (core of scalar multiply).
  • fn neg(): Matrix<T> — Negate every element.
  • fn abs(): Matrix<T> — Absolute value of every element.
  • fn clip(lower: T, upper: T): Matrix<T> — Clamp every element into the inclusive range [lower, upper].
  • fn transpose(): Matrix<T> — Transpose: swap rows and columns into a new cols-by-rows matrix. — e.g. matrix.from_rows([[1, 2, 3], [4, 5, 6]]).transpose()
  • fn matmul(other: Matrix<T>): Matrix<T> — Matrix product: (rows x cols) * (cols x other.cols) -> (rows x other.cols). — e.g. matrix.from_rows([[1, 2], [3, 4]]).matmul(matrix.from_rows([[5, 6], [7, 8]]))
  • fn dot(other: Matrix<T>): Matrix<T> — Matrix-times-matrix spelled as dot.
  • fn dot(vector: Vector<T>): Vector<T> — Matrix-times-vector spelled as dot.
  • fn mul_vector(vector: Vector<T>): Vector<T> — Multiply this matrix by a column vector, producing a vector. — e.g. matrix.from_rows([[1, 2], [3, 4]]).mul_vector(matrix.vector([1, 1]))
  • fn sum_rows(): Vector<T> — Vector of per-row sums (one entry per row).
  • fn sum_columns(): Vector<T> — Vector of per-column sums (one entry per column).
  • fn mean_rows(): Vector<real64> — Vector of per-row means as real64 (panics if there are no columns).
  • fn mean_columns(): Vector<real64> — Vector of per-column means as real64 (panics if there are no rows).
  • fn sum(): T — Sum of every element in the matrix. — e.g. matrix.from_rows([[1, 2], [3, 4]]).sum() // 10
  • fn product(): T — Product of every element in the matrix.
  • fn mean(): real64 — Mean of every element as real64 (panics if empty). — e.g. matrix.from_rows([[1, 2], [3, 4]]).mean() // 2.5
  • fn norm_squared(): T — Sum of squares of all elements (the squared Frobenius norm).
  • fn norm(): real64 — Frobenius norm: sqrt of the sum of squares, as real64.
  • fn min(): T — Minimum element over the whole matrix (via flatten; panics if empty).
  • fn max(): T — Maximum element over the whole matrix (panics if empty).
  • fn argmin(): int — Flat index of the minimum element (row-major).
  • fn argmax(): int — Flat index of the maximum element (row-major).
  • fn det2(): T — Determinant of a 2x2 matrix: ad - bc. — e.g. matrix.from_rows([[1, 2], [3, 4]]).det2() // -2
  • fn det3(): T — Determinant of a 3x3 matrix via cofactor expansion along the first row.

fn vector<T is numeric>(data: [T]): Vector<T>

Build a Vector that wraps the supplied array without copying it.

Example:

matrix.vector([1, 2, 3])

fn from_flat<T is numeric>(rows: int, cols: int, data: [T]): Matrix<T>

Build a Matrix that wraps the supplied flat row-major array without copying it.

Example:

matrix.from_flat(2, 2, [1, 2, 3, 4])

fn from_rows<T is numeric>(rows: [[T]]): Matrix<T>

Build a Matrix from an array of row arrays (all rows must be equal length).

Example:

matrix.from_rows([[1, 2], [3, 4]])

fn zeros<T is numeric>(size: int): Vector<T>

A zero vector of the given size (the literal 0 takes on type T).

Example:

matrix.zeros(3)

fn zeros<T is numeric>(rows: int, cols: int): Matrix<T>

A zero matrix of the given dimensions.

Example:

matrix.zeros(2, 3)

fn ones<T is numeric>(size: int): Vector<T>

A ones vector of the given size.

fn ones<T is numeric>(rows: int, cols: int): Matrix<T>

A ones matrix of the given dimensions.

Example:

matrix.ones(2, 2)

fn full<T is numeric>(size: int, value: T): Vector<T>

A vector of size copies of value.

fn full<T is numeric>(rows: int, cols: int, value: T): Matrix<T>

A matrix of the given dimensions filled with value.

fn arange<T is numeric>(end: T): Vector<T>

arange overload: [0, end) with step 1.

fn arange<T is numeric>(start: T, end: T): Vector<T>

arange overload: [start, end) with step 1.

fn arange<T is numeric>(start: T, end: T, step: T): Vector<T>

A vector of evenly spaced values over [start, end) advancing by step.

Example:

matrix.arange(0, 10, 2)

fn identity<T is numeric>(size: int): Matrix<T>

The size-by-size identity matrix (1 on the diagonal, 0 elsewhere).

Example:

matrix.identity(3)

fn eye<T is numeric>(size: int): Matrix<T>

Alias for identity.

fn diagonal<T is numeric>(values: Vector<T>): Matrix<T>

A square matrix with values on the diagonal and zeros elsewhere.

Example:

matrix.diagonal(matrix.vector([1, 2, 3]))

fn diag<T is numeric>(values: Vector<T>): Matrix<T>

Alias for diagonal.

fn dot<T is numeric>(left: Vector<T>, right: Vector<T>): T

Free-function dot product of two vectors.

Example:

matrix.dot(matrix.vector([1, 2, 3]), matrix.vector([4, 5, 6]))  // 32

fn dot<T is numeric>(left: Matrix<T>, right: Matrix<T>): Matrix<T>

Free-function matrix product of two matrices.

fn dot<T is numeric>(left: Matrix<T>, right: Vector<T>): Vector<T>

Free-function matrix-times-vector product.

fn matmul<T is numeric>(left: Matrix<T>, right: Matrix<T>): Matrix<T>

Free-function matrix multiplication (alias of matmul method).

fn outer<T is numeric>(left: Vector<T>, right: Vector<T>): Matrix<T>

Free-function outer product of two vectors.

maybe

The optional Maybe<T> choice and helpers.

maybe defines Maybe<T>, the standard optional value type. A value is either Present(T) or Absent, which makes missing data explicit without choosing a sentinel value such as 0 or an empty string.

Use Maybe<T> for lookups and operations that may not return a value. Constructors create present or absent values, while has_value, is_absent, and value_or cover the common checks and fallback path.

import maybe;

found: maybe.Maybe<int> = maybe.present(42);
missing: maybe.Maybe<int> = maybe.absent(0);

print(found.has_value());
print(found.value_or(0));
print(missing.is_absent());
print(missing.value_or(7));

Auto-generated from stdlib/maybe.dn by tools/gen_stdlib_docs.py.

choice Maybe<T>

Maybe<T> is an optional value: either a present value or nothing. A choice is a tagged union; here it has two variants: Present(T) wraps a value of type T, and Absent carries no payload.

fn present<T>(value: T): Maybe<T>

Wrap a concrete value as Present.

Example:

maybe.present(7).has_value()  // 1

fn absent<T>(default: T): Maybe<T>

Produce an Absent. The default argument is only there to pin down the generic type T (Dune infers T from it); the value itself is discarded.

Example:

maybe.absent(0).has_value()  // 0

fn absent_int(): Maybe<int>

An Absent specialised to Maybe<int> (uses 0 just to fix T = int).

fn absent_text(): Maybe<text>

An Absent specialised to Maybe<text> (uses "" just to fix T = text).

method<T> Maybe<T>.has_value(): bool

Method: true when this Maybe holds a value. when this { ... } pattern-matches on the choice's variant.

Example:

maybe.present("hi").has_value()  // 1

method<T> Maybe<T>.is_absent(): bool

Method: true when this Maybe is empty (the inverse of has_value).

Example:

maybe.absent(0).is_absent()  // 1

method<T> Maybe<T>.value_or(default: T): T

Method: return the contained value, or default when Absent.

Example:

maybe.absent(0).value_or(9)  // 9

outcome

The result-style Outcome<T, E> choice and helpers.

outcome defines Outcome<T, E>, the standard success-or-failure type. Done(T) carries a successful value and Failed(E) carries an error value; the ? operator understands this type and short-circuits failures from functions that also return an Outcome.

Use it for recoverable errors such as parsing, file I/O, and validation. The specialized helpers for int and text reduce boilerplate in common cases, and value_or and failure_or make simple fallback handling concise.

import outcome;

fn checked(value: int): outcome.Outcome<int, text> {
    if value > 0 {
        return outcome.done_int(value);
    }

    return outcome.failed_int("not positive");
}

fn doubled(value: int): outcome.Outcome<int, text> {
    number: int = checked(value)?;
    return outcome.done_int(number * 2);
}

print(doubled(21).value_or(0));
print(doubled(0).failure_or("ok"));

Auto-generated from stdlib/outcome.dn by tools/gen_stdlib_docs.py.

choice Outcome<T, E>

Outcome<T, E> is a result type: either success carrying a T or failure carrying an error E. It is the explicit error-handling type that the ? operator understands. The two variants are Done(T) and Failed(E).

fn done<T, E>(value: T, error_default: E): Outcome<T, E>

Build a success. error_default is only present to fix the error type E (Dune needs a value of E to infer it); it is not stored.

Example:

outcome.done(7, "").is_done()  // 1

fn failed<T, E>(value_default: T, error: E): Outcome<T, E>

Build a failure. value_default is only present to fix the success type T; it is not stored.

Example:

outcome.failed(0, "bad").is_failed()  // 1

fn done_int(value: int): Outcome<int, text>

A success specialised to Outcome<int, text> (empty text pins E = text).

fn failed_int(error: text): Outcome<int, text>

A failure specialised to Outcome<int, text> (0 pins T = int).

fn done_text(value: text): Outcome<text, text>

A success specialised to Outcome<text, text>.

fn failed_text(error: text): Outcome<text, text>

A failure specialised to Outcome<text, text>.

method<T, E> Outcome<T, E>.is_done(): bool

Method: true when this Outcome is a success.

Example:

outcome.failed(0, "bad").is_done()  // 0

method<T, E> Outcome<T, E>.is_failed(): bool

Method: true when this Outcome is a failure.

Example:

outcome.done(7, "").is_failed()  // 0

method<T, E> Outcome<T, E>.value_or(default: T): T

Method: return the success value, or default if this is a failure.

Example:

outcome.failed(0, "bad").value_or(9)  // 9

method<T, E> Outcome<T, E>.failure_or(default: E): E

Method: return the error value, or default if this is a success.

Example:

outcome.done(7, "").failure_or("none")  // none

plot

Deterministic SVG/HTML chart rendering.

plot builds deterministic chart specifications in pure Dune and renders them to SVG or HTML — no external plotting library. It covers the common chart types (line, area, step, scatter, bar, grouped bars, histogram, and pie), multi-series overlays, grids, subplot figures, file output via fs.write_text, and a platform-native display window through show() where the VM supports it.

Chart types

Every builder returns a Chart you refine with chained methods (.title(...), .x_label(...), .grid(...), .legend(true), .size(w, h)) and then render with plot.svg(chart).

Line chart

The default for a trend over time. plot.line(xs, ys) (or plot.line(ys) for index-based x). Chain .grid(...) / .minor_grid(...) for a background mesh.

import plot;

months = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
users  = [120.0, 135.0, 148.0, 172.0, 168.0, 205.0, 233.0, 258.0];

chart = plot.line(months, users)
    .title("Monthly active users").x_label("month").y_label("users")
    .grid(7).minor_grid(2);
Monthly active users 1 8 0 258 Monthly active users month users

Multiple series

Overlay series by chaining add_line / add_scatter / add_bar on one chart. .label(...) names the most recently added series and legend(true) draws the key.

import plot;

chart = plot.line([30.0, 42.0, 39.0, 55.0, 61.0, 74.0]).label("revenue")
    .add_line([22.0, 28.0, 31.0, 36.0, 41.0, 48.0]).label("cost")
    .title("Revenue vs cost").x_label("quarter").y_label("$k")
    .legend(true).grid(6).minor_grid(2);
Revenue vs cost 1 6 0 74 Revenue vs cost quarter $k revenue cost

Area chart

plot.area(...) is a line filled down to the baseline — good for showing a cumulative quantity or emphasising volume under a curve.

import plot;

chart = plot.area([4.0, 9.0, 7.0, 14.0, 20.0, 18.0, 27.0])
    .title("Daily downloads").x_label("day").y_label("thousands")
    .grid(6).minor_grid(2);
Daily downloads 1 7 0 27 Daily downloads day thousands

Step chart

plot.step(...) holds each value constant until the next sample — the right shape for quantities that change in discrete jumps (counts, levels, states).

import plot;

hours    = [0.0, 3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0];
replicas = [2.0, 2.0, 4.0, 8.0, 8.0, 6.0, 3.0, 2.0];

chart = plot.step(hours, replicas)
    .title("Autoscaled replicas").x_label("hour").y_label("pods").grid(7);
Autoscaled replicas 0 21 0 8 Autoscaled replicas hour pods

Scatter plot

plot.scatter(...) draws points instead of a connecting line — use it to show the relationship between two variables.

import plot;

chart = plot.scatter(study_hours, exam_scores)
    .title("Study hours vs score").x_label("hours").y_label("score")
    .grid(6).minor_grid(2);
Study hours vs score 1 6.5 0 92 Study hours vs score hours score

Bar chart

plot.bar(...) for categorical comparisons. Bars are centred on their x value and the x-domain is padded so the outer bars stay inside the axes.

import plot;

chart = plot.bar([1.0, 2.0, 3.0, 4.0, 5.0], [42.0, 58.0, 33.0, 71.0, 25.0])
    .title("Units sold by product").x_label("product").y_label("units").grid(6);
Units sold by product 0.5 5.5 0 71 Units sold by product product units

Grouped bars

Add more than one bar series and they are drawn side by side within each slot, so you can compare categories across groups.

import plot;

qs = [1.0, 2.0, 3.0, 4.0];

chart = plot.bar(qs, [18.0, 24.0, 21.0, 30.0]).label("2024")
    .add_bar(qs, [22.0, 20.0, 27.0, 34.0]).label("2025")
    .title("Quarterly sales").x_label("quarter").y_label("units")
    .legend(true).grid(6);
Quarterly sales 0.5 4.5 0 34 Quarterly sales quarter units 2024 2025

Histogram

plot.histogram(values, bins) buckets raw samples into equal-width bars to show a distribution.

import plot;

samples = [ /* 30 response times in ms */ ];

chart = plot.histogram(samples, 8)
    .title("Response-time distribution").x_label("ms").y_label("count").grid(6);
Response-time distribution 12 30 0 8 Response-time distribution ms count

Pie chart

plot.pie(values) — or plot.pie(values, labels) — shows parts of a whole. Each wedge is sized by its share of the total and labelled with its percentage; pass labels and legend(true) to name the categories.

import plot;

chart = plot.pie([35.0, 25.0, 20.0, 15.0, 5.0],
    ["Rent", "Food", "Transport", "Savings", "Other"])
    .title("Monthly budget").legend(true);
Monthly budget 35% 25% 20% 15% 5% Monthly budget Rent Food Transport Savings Other

Subplots: one figure, many charts

plot.subplots(charts, cols) composes several charts into a single figure laid out as a grid of cols columns. Each chart keeps its own type, title, grid, and axes and is scaled into an equal cell. The result is a normal SVG, so it works with every output path — save_subplots_svg / save_subplots_html to a file, or show_subplots_native to open it in the native window.

import plot;

users    = [120.0, 135.0, 148.0, 172.0, 168.0, 205.0, 233.0, 258.0];
visits   = [4.0, 9.0, 7.0, 14.0, 20.0, 18.0, 27.0];
sold     = [42.0, 58.0, 33.0, 71.0, 25.0];
replicas = [2.0, 2.0, 4.0, 8.0, 8.0, 6.0, 3.0, 2.0];

figure = plot.subplots([
    plot.line(users).title("line").grid(4).size(300, 200),
    plot.area(visits).title("area").grid(4).size(300, 200),
    plot.scatter(study_hours, exam_scores).title("scatter").grid(4).size(300, 200),
    plot.bar(sold).title("bar").grid(4).size(300, 200),
    plot.step(replicas).title("step").grid(4).size(300, 200),
    plot.pie([35.0, 25.0, 20.0, 15.0, 5.0]).title("pie").size(300, 200),
], 3); // 3 columns -> a 3x2 grid, one SVG
line 1 8 0 258 line area 1 7 0 27 area scatter 1 6.5 0 92 scatter bar 0.5 5.5 0 71 bar step 1 8 0 8 step pie 35% 25% 20% 15% 5% pie

Auto-generated from stdlib/plot.dn by tools/gen_stdlib_docs.py.

record Chart

Methods:

  • static fn empty(): Chart
  • fn title(value: text): Chart — Return a copy of the chart with its title set. — e.g. plot.svg(plot.line([1.0, 2.0]).title("Sales")).contains("Sales") // 1
  • fn x_label(value: text): Chart — Return a copy with the x-axis label set. — e.g. plot.line([1.0, 2.0]).x_label("time")
  • fn y_label(value: text): Chart — Return a copy with the y-axis label set. — e.g. plot.line([1.0, 2.0]).y_label("value")
  • fn legend(enabled: bool): Chart — Return a copy with the legend shown or hidden. — e.g. plot.line([1.0, 2.0]).label("trend").legend(true)
  • fn size(width: int, height: int): Chart — Return a copy sized to width x height pixels (both must be positive). — e.g. plot.svg(plot.line([1.0, 2.0]).size(640, 480)).contains("width=\"640\"") // 1
  • fn grid(cells: int): Chart — Return a copy with a background grid of cells major divisions per axis — e.g. plot.svg(plot.line([1.0, 2.0]).grid(5)).contains("plot-grid") // 1
  • fn minor_grid(subdivisions: int): Chart — Return a copy that also draws subdivisions minor grid lines inside each — e.g. plot.svg(plot.line([1.0, 2.0]).grid(4).minor_grid(5)).contains("plot-grid-minor") // 1
  • fn label(value: text): Chart
  • fn add_line(xs: [real64], ys: [real64]): Chart
  • fn add_line(ys: [real64]): Chart
  • fn add_scatter(xs: [real64], ys: [real64]): Chart
  • fn add_scatter(ys: [real64]): Chart
  • fn add_bar(xs: [real64], ys: [real64]): Chart
  • fn add_bar(ys: [real64]): Chart
  • fn add_area(xs: [real64], ys: [real64]): Chart
  • fn add_area(ys: [real64]): Chart
  • fn add_step(xs: [real64], ys: [real64]): Chart
  • fn add_step(ys: [real64]): Chart

fn empty(): Chart

fn line(xs: [real64], ys: [real64]): Chart

A line chart from paired x/y data.

Example:

plot.svg(plot.line([0.0, 1.0, 2.0], [3.0, 7.0, 5.0])).contains("<svg")  // 1

fn line(ys: [real64]): Chart

A line chart from y values, with x taken as the index 0, 1, 2, ...

Example:

plot.svg(plot.line([1.0, 3.0, 2.0])).contains("<svg")  // 1

fn scatter(xs: [real64], ys: [real64]): Chart

A scatter chart from paired x/y data.

Example:

plot.svg(plot.scatter([1.0, 2.0], [3.0, 4.0])).contains("<circle")  // 1

fn scatter(ys: [real64]): Chart

A scatter chart from y values against their index.

Example:

plot.scatter([3.0, 1.0, 4.0])

fn bar(xs: [real64], ys: [real64]): Chart

A bar chart from paired x/y data.

Example:

plot.svg(plot.bar([1.0, 2.0], [3.0, 5.0])).contains("<rect")  // 1

fn bar(ys: [real64]): Chart

A bar chart from y values against their index.

Example:

plot.svg(plot.bar([3.0, 1.0, 2.0])).contains("<rect")  // 1

fn area(xs: [real64], ys: [real64]): Chart

An area chart: a line filled down to the baseline.

Example:

plot.svg(plot.area([0.0, 1.0, 2.0], [3.0, 7.0, 5.0])).contains("<polygon")  // 1

fn area(ys: [real64]): Chart

An area chart from y values against their index.

Example:

plot.svg(plot.area([3.0, 1.0, 2.0])).contains("<polygon")  // 1

fn step(xs: [real64], ys: [real64]): Chart

A step chart: values held constant between samples (like a staircase).

Example:

plot.svg(plot.step([0.0, 1.0, 2.0], [3.0, 7.0, 5.0])).contains("<polyline")  // 1

fn step(ys: [real64]): Chart

A step chart from y values against their index.

Example:

plot.svg(plot.step([3.0, 1.0, 2.0])).contains("<polyline")  // 1

fn pie(values: [real64]): Chart

A pie chart. Each value becomes a wedge sized by its share of the total and coloured from the palette; wedges are labelled with their percentage.

Example:

plot.svg(plot.pie([3.0, 5.0, 2.0])).contains("<path")  // 1

fn pie(values: [real64], labels: [text]): Chart

A pie chart with a category name for each wedge (shown in the legend).

Example:

plot.svg(plot.pie([3.0, 5.0], ["a", "b"]).legend(true)).contains("<path")  // 1

fn histogram(values: [real64], bins: int): Chart

A histogram: bucket values into bins equal-width bars.

Example:

plot.svg(plot.histogram([1.0, 2.0, 2.0, 3.0], 3)).contains("<svg")  // 1

fn histogram(data: stats.Histogram): Chart

Build a bar chart from a validated stats.Histogram. This preserves custom bin edges and lets data workflows compute/count once in stats, then render the same result in scripts and notebooks.

Example:

plot.svg(plot.histogram(stats.histogram([1.0, 2.0, 3.0], 2).value_or(stats.empty_histogram()))).contains("<svg")  // 1

fn use_backend(name: text): unit

Select the display backend for show. Supported MVP backends are:

  • "none": return a clear unsupported/headless error;
  • "svg": return the deterministic SVG text;
  • "html": return a deterministic HTML wrapper containing the SVG.
  • "native": open a platform-native plot window when the VM supports it.

fn backend(): text

The active backend used by show. Reads DUNE_PLOT_BACKEND if set, otherwise the value last passed to use_backend.

Example:

plot.use_backend("svg"); plot.backend()  // "svg"

fn available_backends(): [text]

The backend names show understands.

Example:

plot.available_backends().len()  // 4

fn svg(chart: Chart): text

Render chart to deterministic standalone SVG text.

Example:

plot.svg(plot.line([1.0, 2.0])).starts_with("<svg")  // 1

fn subplots(charts: [Chart], cols: int): text

Arrange several charts into one SVG as a grid of cols columns (rows are filled left-to-right, top-to-bottom). Each chart keeps its own size and is scaled into an equal cell taken from the first chart's dimensions.

Example:

plot.subplots([plot.line([1.0, 2.0]), plot.bar([3.0, 1.0])], 2).contains("<svg")  // 1

fn html(chart: Chart): text

Render chart to a deterministic standalone HTML document wrapping the SVG.

Example:

plot.html(plot.line([1.0, 2.0])).contains("<!doctype")  // 1

fn save_svg(chart: Chart, path: text): outcome.Outcome<text, text>

Write chart as SVG to path, returning an Outcome for the write.

Example:

plot.save_svg(plot.line([1.0, 2.0]), "chart.svg").is_done()  // 1

fn save_html(chart: Chart, path: text): outcome.Outcome<text, text>

Write chart as an HTML document to path, returning an Outcome.

Example:

plot.save_html(plot.line([1.0, 2.0]), "chart.html").is_done()  // 1

fn show_native(chart: Chart): outcome.Outcome<text, text>

Open chart in a native plot window where the VM supports it; returns an Outcome describing whether the window opened (a headless-safe error if not).

fn subplots_html(charts: [Chart], cols: int): text

Wrap a grid of charts in a standalone HTML document.

Example:

plot.subplots_html([plot.line([1.0, 2.0])], 1).contains("<!doctype")  // 1

fn save_subplots_svg(charts: [Chart], cols: int, path: text): outcome.Outcome<text, text>

Write a grid of charts as SVG to path.

Example:

plot.save_subplots_svg([plot.line([1.0, 2.0]), plot.bar([2.0, 1.0])], 2, "grid.svg").is_done()  // 1

fn save_subplots_html(charts: [Chart], cols: int, path: text): outcome.Outcome<text, text>

Write a grid of charts as an HTML document to path.

Example:

plot.save_subplots_html([plot.line([1.0, 2.0])], 1, "grid.html").is_done()  // 1

fn show_subplots_native(charts: [Chart], cols: int): outcome.Outcome<text, text>

Open a grid of charts in the native plot window where the VM supports it. The window renders the composed SVG, so subplots display exactly as they save.

fn show(chart: Chart): outcome.Outcome<text, text>

process

Process access: arguments and environment.

Access to command-line arguments and environment variables.

The heavy lifting is done by the __process_args, __env_get, and __process_cwd VM intrinsics (dedicated opcodes, not C/C++ foreign functions). This module is pure Dune: it only shapes the primitive results into the Maybe type.

process exposes the current program's command-line arguments, environment variables, and working directory. It wraps VM process intrinsics in ordinary Dune values so missing arguments and unavailable environment values are represented with Maybe<text>.

Use args, arg_count, and arg for CLI programs, env or env_or for environment-driven configuration, and cwd when code needs to report or resolve paths relative to the current directory.

import io;
import maybe;
import process;

io.println(process.arg_count());
io.println(process.arg(0).value_or("no argument"));
io.println(process.env_or("DUNE_PROFILE", "dev"));
io.println(process.cwd().has_value());

Auto-generated from stdlib/process.dn by tools/gen_stdlib_docs.py.

fn args(): [text]

The arguments passed after the script path (dune script.dn a b c).

Example:

process.args()

fn arg_count(): int

The number of command-line arguments.

Example:

process.arg_count()

fn arg(index: int): maybe.Maybe<text>

The argument at index, or Absent when out of range.

Example:

process.arg(0)

fn env(name: text): maybe.Maybe<text>

The value of environment variable name, or Absent when unset.

Example:

process.env("PATH")

fn env_or(name: text, default: text): text

The value of environment variable name, or default when unset.

Example:

process.env_or("DUNE_EXAMPLE_UNSET_VAR", "fallback")

fn cwd(): maybe.Maybe<text>

The current working directory, or Absent when it cannot be determined.

Example:

process.cwd()

random

A small deterministic pseudo-random generator.

random provides deterministic pseudo-random numbers from a seedable Random record. It uses the Park-Miller minimal standard generator, so a given seed produces the same sequence across backends.

Use next_int and next_real for raw draws, between and real_between for bounded values, and uniform or normal when you want arrays of samples. The generator is mutable, so repeated method calls advance its state.

import random;

rng: random.Random = random.seed(42);

print(rng.next_int());
print(rng.between(1, 7));

samples = random.uniform(random.seed(7), 3, 0.0, 1.0);
print(samples.len());

Auto-generated from stdlib/random.dn by tools/gen_stdlib_docs.py.

record Random

Deterministic, seedable pseudo-random numbers.

Random uses the Park-Miller "minimal standard" generator (state = 16807 * state mod 2147483647). The multiplication never overflows a 64-bit integer, so the sequence is identical on every backend for a given seed.

Method names avoid the reserved type names int/real64, so the raw-uniform helpers are next_int/next_real and the bounded integer helper is between.

Methods:

  • fn new(seed: int): Random — Build a generator from seed, normalising it into the valid range.
  • fn next_int(): int — Advance the generator and return the raw state in [1, 2147483646]. — e.g. random.seed(1).next_int()
  • fn next_real(): real64 — Uniform real64 in [0.0, 1.0). Never returns exactly 0.0 or 1.0.
  • fn between(lo: int, hi: int): int — Uniform integer in [lo, hi); hi is exclusive. — e.g. random.seed(42).between(1, 10)
  • fn real_between(lo: real64, hi: real64): real64 — Uniform real64 in [lo, hi). — e.g. random.seed(42).real_between(0.0, 1.0)
  • fn normal(mean: real64, stddev: real64): real64 — One sample from a normal distribution via the Box-Muller transform. — e.g. random.seed(42).normal(0.0, 1.0)

fn seed(value: int): Random

Convenience constructor matching random.seed(42).

Example:

random.seed(42).next_int()

fn uniform(rng: Random, count: int, lo: real64, hi: real64): [real64]

count uniform real64 values in [lo, hi).

Example:

random.uniform(random.seed(42), 3, 0.0, 1.0)

fn normal(rng: Random, count: int, mean: real64, stddev: real64): [real64]

count samples from a normal distribution.

regex

Safe ASCII regular expressions for validation and text cleanup.

regex provides a small, safe ASCII regular-expression engine written in Dune.

Use it for validation and text cleanup when a predictable subset is enough: literals, ., *, +, ?, character classes and ranges, anchors, and capturing groups. compile returns Outcome<Regex, text> so invalid or unsupported syntax stays explicit.

import regex;

ident = regex.compile(r"^[a-z_][a-z0-9_]*$").value_or(regex.never());
print(ident.is_match("hello_1"));

pairs = regex.compile(r"([a-z]+)([0-9]+)").value_or(regex.never());
print(pairs.replace_all("a1 b22", "$2:$1"));

Unsupported syntax returns a compile error: alternation, lookaround, backreferences, counted repetition, flags, word-boundary escapes, absolute anchor escapes, and quantified capture groups.

Auto-generated from stdlib/regex.dn by tools/gen_stdlib_docs.py.

record Capture

One captured subgroup. Captures that did not participate are omitted from Match.captures; use Match.capture(index) to get a Maybe.

Fields:

  • index: int
  • start: int
  • end: int
  • value: text

record Match

A single match with byte/glyph offsets into the searched text.

Fields:

  • start: int
  • end: int
  • value: text
  • captures: [Capture]

Methods:

  • fn len(): int
  • fn is_empty(): bool
  • fn capture(index: int): maybe.Maybe<Capture>
  • fn capture_text(index: int, default: text): text — The text of capture group index, or default when it did not match. — e.g. regex.compile(r"([a-z]+)([0-9]+)").value_or(regex.never()).find("id42").value_or(regex.empty_match()).capture_text(1, "") // id

record Regex

A compiled regular expression. The engine is ASCII-oriented and supports a bounded, safe subset: literals, '.', '*', '+', '?', classes, anchors, and capturing groups. It intentionally rejects alternation, lookaround, backreferences, counted repetition, flags, and quantified groups.

Fields:

  • pattern: text

Methods:

  • fn is_match(input: text): bool — True when the pattern matches anywhere in the input. — e.g. regex.compile(r"\d+").value_or(regex.never()).is_match("abc123") // 1
  • fn match(input: text): maybe.Maybe<Match> — Alias for find: the first match as a Maybe. — e.g. regex.compile(r"\d+").value_or(regex.never()).match("abc123").value_or(regex.empty_match()).value // 123
  • fn find(input: text): maybe.Maybe<Match> — The leftmost match as a Maybe, or absent when there is none. — e.g. regex.compile(r"\d+").value_or(regex.never()).find("abc123def").value_or(regex.empty_match()).value // 123
  • fn find_all(input: text): [Match] — Every non-overlapping match, from left to right. — e.g. regex.compile(r"\d+").value_or(regex.never()).find_all("a1 b22 c333").len() // 3
  • fn split(input: text): [text] — Split the input around each match, returning the pieces in between. — e.g. regex.compile(r"\s+").value_or(regex.never()).split("a b c").len() // 3
  • fn replace(input: text, replacement: text): text — Replace the first match; $1..$9 in replacement expand captures. — e.g. regex.compile(r"\d+").value_or(regex.never()).replace("abc123def456", "#") // abc#def456
  • fn replace_all(input: text, replacement: text): text — Replace every match; $1..$9 in replacement expand captures. — e.g. regex.compile(r"\d+").value_or(regex.never()).replace_all("abc123def456", "#") // abc#def#

fn empty_match(): Match

A default empty match, useful for Maybe.value_or.

Example:

regex.empty_match().is_empty()  // 1

fn never(): Regex

A regex that never matches. Useful with Outcome.value_or after compile.

Example:

regex.never().is_match("anything")  // 0

fn compile(pattern: text): outcome.Outcome<Regex, text>

Compile a pattern into a Regex. Errors are explicit Outcome failures rather than panics.

Example:

regex.compile(r"\d+").is_done()  // 1

runtime

Runtime helpers such as panic.

runtime contains the standard library's single sanctioned native primitive: panic. It aborts execution with a message and exists because that behavior cannot be implemented in pure Dune.

Use it for unrecoverable internal errors and argument validation where returning Maybe or Outcome would hide a programming mistake. Other standard-library modules should remain pure Dune and should not add new foreign declarations.

import runtime;

fn require_positive(value: int): int {
    if value <= 0 {
        runtime.panic("expected a positive value");
    }

    return value;
}

print(require_positive(3));

Auto-generated from stdlib/runtime.dn by tools/gen_stdlib_docs.py.

foreign fn panic(message: text): unit

The one sanctioned native primitive in the standard library. panic aborts execution with a message and cannot be expressed in Dune, so it is bound to the C runtime symbol "dune_panic". Everything else in the stdlib is pure Dune; new foreign fn declarations are forbidden here. export makes it visible to other modules. It takes a text message. The VM reports the message, a panic category, source location, and Dune stack frames before unwinding. See the runtime-errors language guide.

Returns: unit (no meaningful value) because it never returns normally.

Example:

runtime.panic("index out of range")

set

A hash-set built in Dune.

set is a mutable collection of unique text values. It keeps values in insertion order and stores them in a simple array, giving deterministic behavior with a small API.

Use Set when you need membership checks, duplicate suppression, and removal for strings. add ignores duplicates, contains reports membership, remove tells you whether anything was removed, and values returns a fresh array of the stored values. copy() creates a set with an independent backing array.

import io;
import set;

seen: set.Set = set.Set.new();
seen.add("lexer");
seen.add("parser");
seen.add("lexer");

io.println(seen.len());
io.println(seen.contains("parser"));
io.println(seen.values().len());

Auto-generated from stdlib/set.dn by tools/gen_stdlib_docs.py.

record Set

A collection of unique text values.

Like dict, the first iteration stores items in a single array and scans linearly, which keeps behaviour simple and identical across backends. Insertion order is preserved. A record bundles data (the items field) with methods that operate on it.

Methods:

  • fn new(): Set — Construct an empty set. — e.g. set.Set.new().len() // 0
  • fn add(value: text): unit — Add value; duplicates are ignored. — e.g. s = set.Set.new(); s.add("a"); s.contains("a") // 1
  • fn contains(value: text): bool — True when value is a member of the set. — e.g. s = set.Set.new(); s.add("a"); s.contains("b") // 0
  • fn remove(value: text): bool — Remove value; returns whether it was present. — e.g. s = set.Set.new(); s.add("a"); s.remove("a") // 1
  • fn len(): int — The number of elements currently in the set.
  • fn is_empty(): bool — True when the set holds no elements.
  • fn clear(): unit — Drop all elements, leaving an empty set. — e.g. s = set.Set.new(); s.add("a"); s.clear(); s.is_empty() // 1
  • fn copy(): Set — A copy with a fresh backing array. — e.g. s = set.Set.new(); s.add("a"); s.copy().contains("a") // 1
  • fn values(): [text] — A copy of the values in insertion order. — e.g. s = set.Set.new(); s.add("a"); s.add("b"); s.values()

stats

Descriptive statistics, relationships, models, histograms, and probability helpers.

The module is designed for end-to-end data and notebook workflows rather than only scalar reductions. All implementation code is ordinary Dune and runs through the canonical bytecode VM; it adds no native primitive or dependency.

API conventions

  • Functions accept numeric arrays ([int], [real32], [real64], and other numeric element types) and accumulate in real64. Core operations have matching matrix.Vector<T> overloads.
  • Partial operations return outcome.Outcome<value, text>. Empty data, invalid probabilities, mismatched shapes, constant correlation/regression inputs, negative weights, and invalid distribution parameters are explicit failures.
  • variance and stddev are sample estimators (denominator n-1), while pvariance and pstdev are population estimators (denominator n). This mirrors Python statistics and Julia's corrected estimator terminology.
  • quantile(data, q) uses q in [0, 1]; percentile(data, p) uses p in [0, 100]. The default is Hyndman-Fan Type 7 linear interpolation, the common default in R, NumPy, and Julia. quantile_with also exposes lower, higher, nearest, and midpoint interpolation.
  • Inputs are copied before sorting. A caller's array or vector is never reordered as a side effect.

These choices are informed by the public APIs of Python statistics, NumPy statistics, SciPy stats, and Julia Statistics.

Feature groups

  • Descriptive: compensated sum, mean, extrema/range, population/sample variance and standard deviation, standard error, RMS, geometric/harmonic means, five-number summary, Type-7 quantiles, modes/frequencies, percentile rank, MAD, trimmed/winsorized means, skewness, kurtosis, and a comprehensive Summary record.
  • Weighted: mean, population/sample variance and standard deviation, and inverse-empirical-CDF weighted quantiles with non-negative weights.
  • Relationships and models: population/sample covariance, Pearson, Spearman, Kendall tau-b, autocorrelation, MAE/MSE/RMSE/R², and ordinary least squares with predictions and regression diagnostics.
  • Binning and series: equal/custom-edge histograms with density and under/overflow accounting, digitize, bincount, cumulative values, moving mean/variance/stddev, exponential moving average, z-scores, and min-max scaling.
  • Probability: normal PDF/CDF/quantiles, normal-approximation mean confidence intervals, uniform and exponential PDF/CDF, and Bernoulli/binomial/Poisson mass functions.

Dune module integration

matrix.Vector<T> works with the same names as arrays. For observation matrices (rows = observations, columns = variables), use describe_columns, column_means, column_variances, covariance_matrix, correlation_matrix, standardize_columns, or linear_regression_columns. This layout connects directly to the value returned by csv.read_matrix_real64.

stats.histogram returns a reusable Histogram; pass it to plot.histogram(histogram) to chart the validated counts without recomputing bins. See examples/statistical_analysis.dn and examples/notebooks/statistical_analysis.dnb for complete script and notebook workflows using matrix, random, stats, and plot together.

Numerical behavior and limits

Sums use Neumaier compensation. Variance and covariance use stable online recurrences. Quantile sorting is a deterministic insertion sort, currently O(n²) because Dune has no native sorting primitive; rank and Kendall operations are also O(n²). These are appropriate for current notebook-sized data but should be replaced by a pure-Dune O(n log n) sorting implementation before treating very large arrays as a primary use case.

Probability functions use deterministic pure-Dune approximations. The normal CDF is accurate to roughly 1e-7; the inverse normal CDF uses Acklam's rational approximation. Mean confidence intervals use a normal critical value, not a Student-t correction. Inputs are expected to be finite real64 values because the language does not yet expose a standard NaN/missing-data policy.

Auto-generated from stdlib/stats.dn by tools/gen_stdlib_docs.py.

record FiveNumberSummary derive eq, copy, debug

A compact five-number summary, using Type-7 linear quantiles.

Fields:

  • minimum: real64
  • first_quartile: real64
  • median: real64
  • third_quartile: real64
  • maximum: real64

Methods:

  • fn iqr(): real64 — Interquartile range (Q3 - Q1).

record Summary derive eq, copy, debug

A comprehensive summary for a sample containing at least two values.

Fields:

  • count: int
  • sum: real64
  • mean: real64
  • minimum: real64
  • maximum: real64
  • range: real64
  • population_variance: real64
  • sample_variance: real64
  • population_stddev: real64
  • sample_stddev: real64
  • standard_error: real64
  • median: real64
  • first_quartile: real64
  • third_quartile: real64
  • interquartile_range: real64
  • skewness: real64
  • excess_kurtosis: real64

Methods:

  • fn to_text(): text — A concise human-readable rendering suitable for notebooks.

record FrequencyTable derive copy

Sorted distinct numeric values and their occurrence counts.

Fields:

  • values: [real64]
  • counts: [int]
  • relative_frequencies: [real64]
  • total: int

Methods:

  • fn len(): int — Number of distinct values.

record LinearRegression derive eq, copy, debug

Ordinary least-squares fit for y = slope*x + intercept, together with diagnostics that are commonly needed in analysis notebooks.

Fields:

  • slope: real64
  • intercept: real64
  • correlation: real64
  • r_squared: real64
  • residual_sum_squares: real64
  • mean_squared_error: real64
  • root_mean_squared_error: real64
  • residual_standard_error: real64
  • sample_count: int

Methods:

  • fn predict(x: real64): real64 — Predict one response.
  • fn predict_all<T is numeric>(xs: [T]): [real64] — Predict responses for several numeric inputs.

record Histogram

Equal- or variable-width histogram. Intervals are left-closed and right-open, except the final interval also includes its right edge.

Fields:

  • edges: [real64]
  • counts: [int]
  • densities: [real64]
  • sample_count: int
  • included_count: int
  • underflow: int
  • overflow: int

Methods:

  • fn len(): int — Number of bins.
  • fn bin_centers(): [real64] — Midpoint of each bin, useful as x coordinates for plot.bar.
  • fn counts_as_real64(): [real64] — Counts converted to real64 for plotting and arithmetic.
  • fn relative_frequencies(): [real64] — Included counts divided by the number of included observations.
  • fn to_text(): text — Concise notebook rendering; full data remains available in public fields.

record ConfidenceInterval derive eq, copy, debug

Symmetric confidence interval around an estimate.

Fields:

  • estimate: real64
  • lower: real64
  • upper: real64
  • margin: real64
  • confidence: real64

fn empty_five_number_summary(): FiveNumberSummary

Return a zero-valued FiveNumberSummary for use with Outcome.value_or.

fn empty_summary(): Summary

Return a zero-valued Summary for use with Outcome.value_or.

fn empty_frequency_table(): FrequencyTable

Return an empty FrequencyTable for use with Outcome.value_or.

fn count<T>(values: [T]): int

Number of observations. Unlike reductions, this is defined for empty data.

Example:

stats.count([1.0, 2.0, 3.0])  // 3

fn sum<T is numeric>(values: [T]): real64

Compensated sum in real64. The empty sum is 0.

Example:

stats.sum([1, 2, 3])  // 6

fn mean<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Arithmetic mean. Empty input returns Failed.

Example:

stats.mean([1.0, 2.0, 3.0]).value_or(0.0)  // 2

fn minimum<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Smallest value as real64. Empty input returns Failed.

fn maximum<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Largest value as real64. Empty input returns Failed.

fn data_range<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Difference between the largest and smallest observations.

fn midrange<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Midpoint between the largest and smallest observations.

fn min<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Conventional alias for minimum.

fn max<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Conventional alias for maximum.

fn range<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Conventional alias for data_range.

fn pvariance<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Population variance (divide by n), computed with Welford's stable one-pass recurrence. A single observation has population variance 0.

Example:

stats.pvariance([1.0, 2.0, 3.0]).value_or(0.0)  // 0.666667

fn variance<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Sample variance (divide by n-1), matching Python statistics.variance and Julia var(corrected=true). At least two observations are required.

Example:

stats.variance([1.0, 2.0, 3.0]).value_or(0.0)  // 1

fn population_variance<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Explicitly named alias for pvariance.

fn sample_variance<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Explicitly named alias for variance.

fn pstdev<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Population standard deviation.

fn stddev<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Sample standard deviation. At least two observations are required.

Example:

stats.stddev([1.0, 2.0, 3.0]).value_or(0.0)  // 1

fn population_stddev<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Explicitly named alias for pstdev.

fn sample_stddev<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Explicitly named alias for stddev.

fn standard_error<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Standard error of the arithmetic mean, using the sample standard deviation.

fn root_mean_square<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Root mean square, useful for signal magnitude.

fn geometric_mean<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Geometric mean. All observations must be strictly positive.

fn harmonic_mean<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Harmonic mean. All observations must be strictly positive.

fn median<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Median with linear averaging for an even-sized sample.

Example:

stats.median([4.0, 1.0, 3.0, 2.0]).value_or(0.0)  // 2.5

fn median_low<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Lower middle observation (never averages for even-sized input).

fn median_high<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Upper middle observation (never averages for even-sized input).

fn quantile<T is numeric>(values: [T], probability: real64): outcome.Outcome<real64, text>

Type-7 linear quantile for probability in [0, 1]. This is the default used by R, NumPy, and Julia Statistics.

Example:

stats.quantile([0.0, 10.0, 20.0], 0.25).value_or(0.0)  // 5

fn quantile_with<T is numeric>(values: [T], probability: real64, interpolation: text): outcome.Outcome<real64, text>

Quantile with one of: linear, lower, higher, nearest, midpoint.

fn quantiles<T is numeric>(values: [T], probabilities: [real64]): outcome.Outcome<[real64], text>

Several Type-7 quantiles in one call. Probabilities retain caller order.

fn percentile<T is numeric>(values: [T], percent: real64): outcome.Outcome<real64, text>

Conventional percentile for p in [0, 100], matching NumPy. Use quantile for probabilities in [0, 1].

Example:

stats.percentile([0.0, 10.0, 20.0], 25.0).value_or(0.0)  // 5

fn five_number_summary<T is numeric>(values: [T]): outcome.Outcome<FiveNumberSummary, text>

The minimum, quartiles, and maximum. Input is copied before sorting.

fn interquartile_range<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Interquartile range (Q3 - Q1).

fn median_absolute_deviation<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Median absolute deviation from the sample median.

fn mean_absolute_deviation<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Mean absolute deviation from the arithmetic mean.

fn trimmed_mean<T is numeric>(values: [T], fraction: real64): outcome.Outcome<real64, text>

Mean after removing fraction from each tail. The fraction must be in [0, 0.5), and at least one observation must remain.

fn winsorized_mean<T is numeric>(values: [T], fraction: real64): outcome.Outcome<real64, text>

Winsorized mean: values in each trimmed tail are replaced with the nearest retained boundary instead of removed.

fn frequencies<T is numeric>(values: [T]): outcome.Outcome<FrequencyTable, text>

Sorted frequency table for numeric data.

fn mode<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Smallest mode when several values share the maximum frequency.

fn modes<T is numeric>(values: [T]): outcome.Outcome<[real64], text>

Every mode, sorted ascending.

fn percentile_rank<T is numeric>(values: [T], value: real64): outcome.Outcome<real64, text>

Fraction of observations less than or equal to value, in [0, 1].

fn population_skewness<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Population skewness (third standardized central moment). Constant data

Returns: 0; at least one observation is required.

fn skewness<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Bias-corrected sample skewness. At least three observations are required.

fn population_excess_kurtosis<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Population excess kurtosis (fourth standardized moment minus 3). Constant data returns 0.

fn excess_kurtosis<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Bias-corrected Fisher sample excess kurtosis. At least four observations are required.

fn coefficient_of_variation<T is numeric>(values: [T]): outcome.Outcome<real64, text>

Coefficient of variation: sample standard deviation divided by |mean|.

fn describe<T is numeric>(values: [T]): outcome.Outcome<Summary, text>

Comprehensive descriptive summary. At least two observations are required because the record includes sample variance and standard error. Skewness and kurtosis are reported as 0 when the sample is too small or constant.

Example:

stats.describe([1.0, 2.0, 3.0]).value_or(stats.empty_summary()).mean  // 2

fn weighted_mean<T is numeric, W is numeric>(values: [T], weights: [W]): outcome.Outcome<real64, text>

Arithmetic mean with non-negative reliability/frequency weights.

Example:

stats.weighted_mean([10.0, 20.0], [1.0, 3.0]).value_or(0.0)  // 17.5

fn weighted_pvariance<T is numeric, W is numeric>(values: [T], weights: [W]): outcome.Outcome<real64, text>

Weighted population variance, dividing by the total weight.

fn weighted_variance<T is numeric, W is numeric>(values: [T], weights: [W]): outcome.Outcome<real64, text>

Unbiased weighted sample variance for reliability weights. The denominator is sum(w) - sum(w^2)/sum(w); it must be positive.

fn weighted_pstdev<T is numeric, W is numeric>(values: [T], weights: [W]): outcome.Outcome<real64, text>

Weighted population standard deviation.

fn weighted_stddev<T is numeric, W is numeric>(values: [T], weights: [W]): outcome.Outcome<real64, text>

Unbiased weighted sample standard deviation.

fn weighted_quantile<T is numeric, W is numeric>(values: [T], weights: [W], probability: real64): outcome.Outcome<real64, text>

Weighted inverse-empirical-CDF quantile. Zero-weight observations do not affect the threshold; q must be in [0, 1].

fn pcovariance<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Population covariance (divide by n), using a stable online co-moment.

fn covariance<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Sample covariance (divide by n-1), matching Python statistics.covariance.

Example:

stats.covariance([1.0, 2.0, 3.0], [2.0, 4.0, 6.0]).value_or(0.0)  // 2

fn population_covariance<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Explicitly named alias for population covariance.

fn sample_covariance<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Explicitly named alias for sample covariance.

fn correlation<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Pearson product-moment correlation. Constant input is reported explicitly.

Example:

stats.correlation([1.0, 2.0, 3.0], [2.0, 4.0, 6.0]).value_or(0.0)  // 1

fn pearson_correlation<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Explicit alias for Pearson product-moment correlation.

fn spearman_correlation<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Spearman rank correlation with average ranks for ties.

fn kendall_tau<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<real64, text>

Kendall tau-b rank correlation, correcting the denominator for ties in each input. O(n^2), deterministic, and suitable for notebook-sized data.

fn autocorrelation<T is numeric>(values: [T], lag: int): outcome.Outcome<real64, text>

Correlation between a series and itself shifted by lag observations.

fn mean_absolute_error<Y is numeric, P is numeric>(observed: [Y], predicted: [P]): outcome.Outcome<real64, text>

Mean absolute error between observed and predicted values.

fn mean_squared_error<Y is numeric, P is numeric>(observed: [Y], predicted: [P]): outcome.Outcome<real64, text>

Mean squared error between observed and predicted values.

fn root_mean_squared_error<Y is numeric, P is numeric>(observed: [Y], predicted: [P]): outcome.Outcome<real64, text>

Root mean squared error.

fn coefficient_of_determination<Y is numeric, P is numeric>(observed: [Y], predicted: [P]): outcome.Outcome<real64, text>

Coefficient of determination R^2. Constant observed data is rejected.

fn linear_regression<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<LinearRegression, text>

Ordinary least-squares regression with an intercept and diagnostics.

Example:

stats.linear_regression([1.0, 2.0, 3.0], [3.0, 5.0, 7.0]).value_or(stats.empty_regression()).slope  // 2

fn simple_linear_regression<X is numeric, Y is numeric>(xs: [X], ys: [Y]): outcome.Outcome<LinearRegression, text>

Explicit alias emphasizing that the model has one predictor.

fn empty_regression(): LinearRegression

Zero-valued regression model for Outcome.value_or.

fn empty_histogram(): Histogram

Empty histogram for Outcome.value_or.

fn histogram_with_edges<T is numeric>(values: [T], edges: [real64]): outcome.Outcome<Histogram, text>

Histogram with caller-supplied, strictly increasing edges. Values below the first edge and above the last edge are counted separately.

fn histogram_range<T is numeric>(values: [T], bins: int, lower: real64, upper: real64): outcome.Outcome<Histogram, text>

Equal-width histogram over an explicit [lower, upper] range.

fn histogram<T is numeric>(values: [T], bins: int): outcome.Outcome<Histogram, text>

Equal-width histogram whose range is inferred from the data. Constant data is centered in a synthetic unit-width range so all bins remain meaningful.

Example:

stats.histogram([1.0, 2.0, 3.0, 4.0], 2).value_or(stats.empty_histogram()).counts  // [2, 2]

fn digitize<T is numeric>(values: [T], edges: [real64]): outcome.Outcome<[int], text>

Bin index for each value using histogram edge semantics. Underflow is -1; overflow is bin_count. Exact equality with the final edge lands in the final bin.

fn bincount(values: [int], minimum_length: int): outcome.Outcome<[int], text>

Counts of non-negative integer values, like NumPy bincount. The result has at least minimum_length entries.

fn cumulative_sum<T is numeric>(values: [T]): [real64]

Cumulative compensated sum in real64. Empty input returns an empty array.

fn cumulative_mean<T is numeric>(values: [T]): [real64]

Cumulative arithmetic mean after each observation.

fn moving_sum<T is numeric>(values: [T], window: int): outcome.Outcome<[real64], text>

Sliding-window sums, one value for each complete window.

fn moving_mean<T is numeric>(values: [T], window: int): outcome.Outcome<[real64], text>

Sliding-window arithmetic means.

Example:

stats.moving_mean([1.0, 2.0, 3.0, 4.0], 2).value_or([])  // [1.5, 2.5, 3.5]

fn moving_pvariance<T is numeric>(values: [T], window: int): outcome.Outcome<[real64], text>

Sliding population variance for each complete window.

fn moving_variance<T is numeric>(values: [T], window: int): outcome.Outcome<[real64], text>

Sliding sample variance for each complete window (window >= 2).

fn moving_stddev<T is numeric>(values: [T], window: int): outcome.Outcome<[real64], text>

Sliding sample standard deviation for each complete window.

fn exponential_moving_average<T is numeric>(values: [T], alpha: real64): outcome.Outcome<[real64], text>

Exponentially weighted moving average. alpha is in (0, 1]; the first output equals the first input, and subsequent outputs use alpha*x + (1-alpha)*prev.

fn z_scores<T is numeric>(values: [T]): outcome.Outcome<[real64], text>

Sample z-scores (center by mean, scale by sample standard deviation).

fn population_z_scores<T is numeric>(values: [T]): outcome.Outcome<[real64], text>

Population z-scores (scale by population standard deviation).

fn min_max_scale<T is numeric>(values: [T]): outcome.Outcome<[real64], text>

Scale data linearly into [0, 1]. Constant input is rejected explicitly.

fn standard_normal_pdf(value: real64): real64

Standard-normal probability density.

fn standard_normal_cdf(value: real64): real64

Standard-normal cumulative distribution, approximated to about 1e-7.

fn standard_normal_quantile(probability: real64): outcome.Outcome<real64, text>

Inverse standard-normal CDF using Peter Acklam's rational approximation.

fn normal_pdf(value: real64, location: real64, scale: real64): outcome.Outcome<real64, text>

Normal-distribution density with explicit location and positive scale.

fn normal_cdf(value: real64, location: real64, scale: real64): outcome.Outcome<real64, text>

Normal-distribution cumulative probability.

fn normal_quantile(probability: real64, location: real64, scale: real64): outcome.Outcome<real64, text>

Normal-distribution quantile.

fn mean_confidence_interval<T is numeric>(values: [T], confidence: real64): outcome.Outcome<ConfidenceInterval, text>

Two-sided normal-approximation confidence interval for a sample mean. confidence must be strictly between 0 and 1.

fn uniform_pdf(value: real64, lower: real64, upper: real64): outcome.Outcome<real64, text>

Continuous uniform density over [lower, upper].

fn uniform_cdf(value: real64, lower: real64, upper: real64): outcome.Outcome<real64, text>

Continuous uniform cumulative probability.

fn exponential_pdf(value: real64, rate: real64): outcome.Outcome<real64, text>

Exponential density for non-negative values and a positive rate.

fn exponential_cdf(value: real64, rate: real64): outcome.Outcome<real64, text>

Exponential cumulative probability.

fn bernoulli_pmf(outcome_value: int, probability: real64): outcome.Outcome<real64, text>

Bernoulli probability mass for outcome 0 or 1.

fn binomial_pmf(k: int, n: int, probability: real64): outcome.Outcome<real64, text>

Binomial probability mass for k successes in n independent trials.

fn poisson_pmf(k: int, rate: real64): outcome.Outcome<real64, text>

Poisson probability mass for a non-negative count and positive rate.

fn poisson_cdf(k: int, rate: real64): outcome.Outcome<real64, text>

Poisson cumulative probability P(X <= k), evaluated by a stable recurrence.

fn empty_vector(): matrix.Vector<real64>

Empty real vector for Outcome.value_or in vector-oriented workflows.

fn empty_matrix(): matrix.Matrix<real64>

Empty 0x0 real matrix for Outcome.value_or.

fn count<T is numeric>(values: matrix.Vector<T>): int

Vector overloads keep the same names as array functions, so code can move between raw arrays and matrix.Vector without changing its statistical API.

fn sum<T is numeric>(values: matrix.Vector<T>): real64

fn mean<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn minimum<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn maximum<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn data_range<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn min<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn max<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn range<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn pvariance<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn variance<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn population_variance<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn sample_variance<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn pstdev<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn stddev<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn population_stddev<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn sample_stddev<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn median<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn quantile<T is numeric>(values: matrix.Vector<T>, probability: real64): outcome.Outcome<real64, text>

fn quantile_with<T is numeric>(values: matrix.Vector<T>, probability: real64, interpolation: text): outcome.Outcome<real64, text>

fn percentile<T is numeric>(values: matrix.Vector<T>, percent: real64): outcome.Outcome<real64, text>

fn five_number_summary<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<FiveNumberSummary, text>

fn interquartile_range<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn median_absolute_deviation<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<real64, text>

fn describe<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<Summary, text>

fn frequencies<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<FrequencyTable, text>

fn histogram<T is numeric>(values: matrix.Vector<T>, bins: int): outcome.Outcome<Histogram, text>

fn histogram_range<T is numeric>(values: matrix.Vector<T>, bins: int, lower: real64, upper: real64): outcome.Outcome<Histogram, text>

fn z_scores<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<matrix.Vector<real64>, text>

fn population_z_scores<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<matrix.Vector<real64>, text>

fn min_max_scale<T is numeric>(values: matrix.Vector<T>): outcome.Outcome<matrix.Vector<real64>, text>

fn moving_mean<T is numeric>(values: matrix.Vector<T>, window: int): outcome.Outcome<matrix.Vector<real64>, text>

fn moving_stddev<T is numeric>(values: matrix.Vector<T>, window: int): outcome.Outcome<matrix.Vector<real64>, text>

fn exponential_moving_average<T is numeric>(values: matrix.Vector<T>, alpha: real64): outcome.Outcome<matrix.Vector<real64>, text>

fn weighted_mean<T is numeric, W is numeric>(values: matrix.Vector<T>, weights: matrix.Vector<W>): outcome.Outcome<real64, text>

fn weighted_variance<T is numeric, W is numeric>(values: matrix.Vector<T>, weights: matrix.Vector<W>): outcome.Outcome<real64, text>

fn weighted_quantile<T is numeric, W is numeric>(values: matrix.Vector<T>, weights: matrix.Vector<W>, probability: real64): outcome.Outcome<real64, text>

fn covariance<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn sample_covariance<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn pcovariance<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn population_covariance<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn correlation<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn pearson_correlation<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn spearman_correlation<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn kendall_tau<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<real64, text>

fn linear_regression<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<LinearRegression, text>

fn simple_linear_regression<X is numeric, Y is numeric>(xs: matrix.Vector<X>, ys: matrix.Vector<Y>): outcome.Outcome<LinearRegression, text>

fn describe<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<Summary, text>

Descriptive statistics over all matrix elements in row-major order.

fn histogram<T is numeric>(values: matrix.Matrix<T>, bins: int): outcome.Outcome<Histogram, text>

Histogram over all matrix elements in row-major order.

fn describe_columns<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<[Summary], text>

One comprehensive Summary per matrix column. Rows are observations and columns are variables, matching NumPy/SciPy's conventional data layout.

fn describe_rows<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<[Summary], text>

One comprehensive Summary per matrix row.

fn column_means<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Vector<real64>, text>

Column means as a Vector. Empty matrix axes return Failed, not a VM panic.

fn row_means<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Vector<real64>, text>

Row means as a Vector.

fn column_variances<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Vector<real64>, text>

Sample variance of each matrix column.

fn column_stddevs<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Vector<real64>, text>

Sample standard deviation of each matrix column.

fn column_medians<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Vector<real64>, text>

Median of each matrix column.

fn column_histograms<T is numeric>(values: matrix.Matrix<T>, bins: int): outcome.Outcome<[Histogram], text>

One histogram per matrix column, useful immediately after csv.read_matrix.

fn covariance_matrix<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Matrix<real64>, text>

Sample covariance matrix. Rows are observations, columns are variables.

fn correlation_matrix<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Matrix<real64>, text>

Pearson correlation matrix. Any constant column yields a clear Failed value.

fn standardize_columns<T is numeric>(values: matrix.Matrix<T>): outcome.Outcome<matrix.Matrix<real64>, text>

Standardize every matrix column to sample mean 0 and sample standard deviation 1. Constant columns are rejected explicitly.

fn linear_regression_columns<T is numeric>(values: matrix.Matrix<T>, x_column: int, y_column: int): outcome.Outcome<LinearRegression, text>

Fit y on x using two columns from one observation matrix. This is convenient for matrices loaded by the csv module.

text

Text and glyph helpers.

String utilities implemented as methods on the built-in text type and a few free predicate functions on glyph (a single character). Several methods simply forward to the VM's native text operations of the same name; others are written in Dune on top of indexing and slicing.

text adds string-style helpers to the built-in text type and predicate helpers for glyph values. Some methods forward to VM text operations, while others are implemented in Dune using indexing and slicing.

Use it for length checks, substring tests, slicing, trimming whitespace, glyph search/counting, and ASCII character classification. Importing the module enables receiver-style calls such as value.trim() and value.starts_with(...).

import io;
import text;

raw = "  dune language  ";
clean = raw.trim();

io.println(clean.starts_with("dune"));
io.println(clean.index_of('g'));
io.println(text.is_alpha(clean.char_at(0)));

Auto-generated from stdlib/text.dn by tools/gen_stdlib_docs.py.

method text.len(): int

The length in UTF-8 bytes of this text (forwards to the VM operation).

method text.is_empty(): bool

True when the text has zero length (forwards to the native operation).

method text.contains(needle: text): bool

True when needle occurs somewhere in this text.

Example:

"hello world".contains("wor")  // 1

method text.starts_with(prefix: text): bool

True when this text begins with prefix.

Example:

"hello".starts_with("he")  // 1

method text.ends_with(suffix: text): bool

True when this text ends with suffix.

method text.char_at(index: int): glyph

The glyph at index (0-based) via indexing.

method text.slice(start: int, end: int): text

The substring from start (inclusive) to end (exclusive).

Example:

"hello world".slice(0, 5)  // hello

method text.prefix(end: int): text

The first end UTF-8 bytes of the text.

method text.suffix(start: int): text

Everything from start to the end of the text.

method text.index_of(needle: glyph): int

The index of the first occurrence of glyph needle, or -1 if absent.

Example:

"hello".index_of('l')  // 2

method text.count(needle: glyph): int

How many times glyph needle occurs in the text.

method text.trim_start(): text

Drop leading whitespace and return the remainder.

method text.trim_end(): text

Drop trailing whitespace and return the remainder.

method text.trim(): text

Drop whitespace from both ends by composing the two trims.

Example:

"  hi  ".trim()  // hi

fn is_space(value: glyph): bool

True when value is a space, newline, carriage return, or tab.

fn is_digit(value: glyph): bool

True when value is an ASCII decimal digit '0'..'9'.

Example:

text.is_digit('5')  // 1

fn is_lower(value: glyph): bool

True when value is a lowercase ASCII letter 'a'..'z'.

fn is_upper(value: glyph): bool

True when value is an uppercase ASCII letter 'A'..'Z'.

fn is_alpha(value: glyph): bool

True when value is any ASCII letter (upper or lower case).

Example:

text.is_alpha('a')  // 1

method text.concat(other: text): text

This text followed by other (the method form of this + other).

Example:

"foo".concat("bar")  // foobar

method text.repeat(count: int): text

This text repeated count times ("" for count <= 0).

Example:

"ab".repeat(3)  // ababab

method text.reverse(): text

A new text with the glyphs in reverse order.

Example:

"abc".reverse()  // cba

method text.to_upper(): text

A copy of this text with every ASCII lowercase letter upper-cased.

Example:

"Hello, World!".to_upper()  // HELLO, WORLD!

method text.to_lower(): text

A copy of this text with every ASCII uppercase letter lower-cased.

Example:

"Hello, World!".to_lower()  // hello, world!

method text.replace(target: text, replacement: text): text

Every occurrence of target replaced by replacement. Returns the text unchanged when target is empty (which would otherwise never advance).

Example:

"a-b-c".replace("-", "+")  // a+b+c

method text.split(separator: glyph): [text]

Split into the pieces separated by glyph separator. Adjacent separators yield empty pieces, and the result always has one more piece than the number of separators.

Example:

"a,b,c".split(',').len()  // 3

method text.pad_start(width: int, fill: glyph): text

Left-pad with fill until the text is at least width glyphs wide.

Example:

"42".pad_start(5, '0')  // 00042

method text.pad_end(width: int, fill: glyph): text

Right-pad with fill until the text is at least width glyphs wide.

Example:

"42".pad_end(5, '.')  // 42...

fn join(parts: [text], separator: text): text

Join parts into a single text, inserting separator between them.

Example:

join(["a", "b", "c"], ", ")  // a, b, c

Installation and building

Dune is built from source with CMake. It targets C++23.

Build

git clone https://github.com/ComradeMashkov/dune.git
cd dune
cmake -S . -B build -D DUNE_ENABLE_LINT=OFF
cmake --build build -j

This produces the dune binary at build/dune. The bytecode virtual machine runs every program and needs no external toolchain.

Run a program

./build/dune examples/matrix_basics.dn

Run the tests

ctest --test-dir build

The standard library is discovered relative to the binary (and via the DUNE_STDLIB_PATH environment variable), so import math; and friends work out of the box.

Next: the command-line tool.

The dune command-line tool

The dune binary has a small set of commands.

Run a program

dune path/to/program.dn [args...]

Runs a program on the bytecode VM. Any trailing arguments are exposed to the program through the process module's args().

Type-check without running

dune check path/to/program.dn

Type-checks the program and reports diagnostics without executing it. Exits non-zero if there are errors.

Interactive REPL

dune repl

Starts an interactive session on the bytecode VM. Successful input remains in scope for later entries, including bindings, imports, functions, records, choices, and type aliases. Bare expressions print their result automatically:

> value = 40 + 2;
> value
42
> import math;
> math.square(9)
81

Blocks and declarations may span multiple lines; the prompt changes to ... until the entry is complete. Parser, type-checker, and runtime errors are reported without ending the session.

The built-in commands are:

  • :help — show the command list.
  • :reset — clear all accumulated source and values.
  • :quit — exit successfully.

The first implementation recompiles and re-executes accumulated successful source for every entry. Stable repeated console output is hidden, but external side effects such as file writes can run again. Program reads from stdin are not available because stdin belongs to the REPL command loop. True incremental compiler and VM state can replace this model later without changing the command surface.

Notebooks

dune notebook new tutorial.dnb --title "Tutorial"
dune notebook serve tutorial.dnb

Notebooks are versioned .dnb JSON documents with Markdown cells, Dune code cells, execution counts, and structured outputs. The local server opens a token-protected browser workspace for editing and executing cells. The same files can be run, checked, and exported without starting the server:

dune notebook run tutorial.dnb
dune notebook run tutorial.dnb --update
dune notebook check tutorial.dnb
dune notebook export tutorial.dnb --html -o tutorial.html

See the notebook guide for the file format, server options, kernel behavior, and CI workflow.

Run tests

dune test path/to/program.dn

Runs every @test function and test "..." { ... } block in the file and prints a per-test ok/FAILED/ignored line plus a summary. Each test runs in isolation — the file's top-level code is skipped, so only the tests execute — while top-level functions, constants, and imports remain in scope. A failed assertion aborts just that test; @should_panic and @should_fail can make a matching failure the expected result, and @ignore skips a test. The command exits non-zero if any non-ignored test fails.

Language server

dune lsp

Starts the Language Server over stdio. Editors launch this for diagnostics, completions, hover, and go-to-definition — see Editor integration.

Generate API documentation

dune doc path/to/module.dn            # print Markdown to stdout
dune doc path/to/module.dn -o out.md  # write one page
dune doc path/to/modules -o out/      # a page per module, plus index.md
dune doc path/to/modules -o out/ --check   # fail if out/ is out of date

Renders a module's public API — functions, constants, type aliases, records (with their fields and methods), choices, and contracts — to Markdown, using the real parser so signatures and doc-comments match the source exactly. Only exported declarations appear (a module with no export is treated as fully public). --check regenerates in memory and exits non-zero on any drift, which keeps generated docs current in CI.

Diagnostics

When a command fails to lex, parse, or type-check the main file, dune prints a source snippet that points at the exact span the error refers to:

error: expected type 'int' but got 'text'
  --> program.dn:1:10
  |
1 | x: int = "hello";
  |          ^^^^^^^

Runtime failures use a category plus an innermost-first Dune stack trace:

panic: invalid state
stack trace:
  0: validate
      at program.dn:4:5
  1: <top-level>
      at program.dn:7:1

Imported pure-Dune modules keep their own file locations. The test runner, REPL, and notebook kernel use the same format; notebook locations include the cell ID. See Runtime errors and stack traces.

The --> line gives file:line:column, and the caret underline marks the offending token or expression. Lexer, parser, and type-check errors all use this format; dune check shows it beneath its per-stage progress trace. Errors from imported modules and runtime failures fall back to a single-line message (source snippets for other files are a follow-up). The same locations are sent to editors over the language server, so squiggles land on the right span.

Version

dune --version

Dune notebooks

Dune notebooks are interactive documents stored in the versioned .dnb format. A notebook contains Markdown cells, Dune code cells, execution counts, and captured output. Code runs through the same lexer, parser, type checker, compiler, and bytecode VM as a .dn program.

Create and open a notebook

dune notebook new notebooks/tutorial.dnb --title "Dune tutorial"
dune notebook serve notebooks/tutorial.dnb

serve starts Dune's dependency-free HTTP server and normally opens the browser workspace. The workspace includes:

  • a .dnb file browser rooted at the selected directory;
  • a classic Jupyter-style menu, toolbar, prompt gutter, and cell selection;
  • persistent light and dark themes that follow the system on first launch;
  • Markdown editing with inline/display LaTeX formulas and live Dune syntax highlighting for code cells;
  • adding, moving, and deleting cells;
  • toolbar and keyboard cell-type switching (Y for code, M for Markdown);
  • Shift+Enter runs a cell and selects the next one, creating a Code cell at the end; Cmd/Ctrl+Enter runs without moving;
  • Run Cell and Run All actions;
  • persistent kernel sessions with Restart Kernel;
  • clearing the selected cell output or all saved outputs, plus a combined Restart Kernel and Clear All Outputs action;
  • structured stdout and stderr output;
  • inline SVG output from the pure-Dune plot module;
  • saving and standalone HTML export.

The server listens only on 127.0.0.1:8888 by default. It generates a random token and includes it in the printed browser URL. Every workspace API request must provide that token. Paths are confined to the selected root, and only .dnb files can be read or written.

Server options:

dune notebook serve notebooks/ --port 9000
dune notebook serve tutorial.dnb --no-open
dune notebook serve notebooks/ --token private_token
dune notebook serve notebooks/ --host 0.0.0.0

Binding to 0.0.0.0 exposes the server to the network and prints a warning. Keep the token private. Explicit tokens may contain letters, digits, -, and _ (up to 128 characters). The server intentionally has no package, Jupyter, Node.js, or browser-framework dependency.

The .dnb format

.dnb is JSON with an explicit format version. Its structure follows the useful parts of .ipynb while keeping the Dune schema small:

{
  "dune_notebook": 1,
  "metadata": {
    "title": "A tiny notebook"
  },
  "cells": [
    {
      "id": "intro",
      "cell_type": "markdown",
      "source": "# Hello\n\nThis is **Markdown**."
    },
    {
      "id": "answer",
      "cell_type": "code",
      "source": "x = 40 + 2;\nx",
      "execution_count": 1,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": "42\n"
        }
      ]
    }
  ]
}

Cell IDs are stable across edits. source may be a single string or an array of strings when importing data from ipynb-style tooling. Code outputs use stdout and stderr streams, so saved notebooks remain deterministic and easy to diff.

Unknown object fields are ignored for forward compatibility. A newer dune_notebook version is rejected with a clear error instead of being silently misread.

Markdown and LaTeX formulas

Markdown cells render mathematical notation directly in the notebook and in standalone HTML exports. Use either Jupyter-style dollar delimiters or the equivalent LaTeX delimiters:

The sample mean is $\bar{x} = \frac{1}{n}\sum_{i=1}^{n}x_i$.

$$
s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i-\bar{x})^2
$$

\[
A = \begin{pmatrix}a & b \\ c & d\end{pmatrix}
\]

Inline formulas accept $...$ and \(...\). Display formulas accept $$...$$ and \[...\]; multiline display delimiters should begin and end on their own lines. Escape a literal dollar sign as \$. Formula delimiters inside inline code or fenced code blocks are left untouched.

The built-in renderer covers subscripts and superscripts, fractions, roots, binomial coefficients, Greek letters, relations, arrows, sets, named functions, large operators with limits, accents, common math font variants, stretchy delimiters, and matrix, pmatrix, bmatrix, Bmatrix, vmatrix, Vmatrix, array, aligned, and cases environments. Unsupported commands remain visible as TeX instead of disappearing.

Rendering is offline and dependency-free. Dune converts the supported TeX math syntax to native MathML, preserves the original source in an accessible application/x-tex annotation, follows the notebook's light/dark theme, and escapes formula text before inserting it into the page.

Kernel and cell execution

Cells execute in document order and share state: bindings, imports, functions, records, choices, aliases, and mutations from earlier successful cells are available later. A final bare expression is printed automatically, using to_text() for displayable records.

The kernel continues an unchanged prefix. Editing or rerunning an earlier cell rebuilds its dependent prefix so declarations do not become duplicated. Parser, type-checker, and runtime failures stop Run All at the failed cell. Front-end diagnostics include the notebook path and stable cell ID:

error: expected type 'int' but got 'bool'
  --> tutorial.dnb#cell-types:1:12

The current kernel shares the REPL's accumulated-source implementation. Stable repeated console output is hidden, but external side effects such as file writes may run again when an edited prefix is rebuilt. Program stdin is not available inside notebook cells.

CLI and CI

Run a notebook and print each cell's captured output:

dune notebook run notebooks/tutorial.dnb

Refresh outputs and execution counts in the file:

dune notebook run notebooks/tutorial.dnb --update

Check saved outputs without modifying the notebook:

dune notebook check notebooks/tutorial.dnb

check exits non-zero when a cell fails or its saved stdout/stderr differs from a fresh run, making notebooks reproducible CI artifacts.

To return an interactive notebook to a clean state, use Cell → Clear selected output, Cell → Clear all outputs, or Kernel → Restart kernel and clear all outputs. The toolbar also exposes Clear output for the selected cell. The shortcuts are Alt+O for the selected output and Shift+Alt+O for all outputs. Clearing marks the notebook as changed; use Save (Cmd/Ctrl+S) to persist the empty outputs and execution counters.

Export the saved document as standalone HTML:

dune notebook export notebooks/tutorial.dnb --html
dune notebook export notebooks/tutorial.dnb --html -o reports/tutorial.html

The export embeds its responsive Jupyter-style layout and SVG chart outputs, follows the reader's light/dark system preference, escapes notebook content, and needs no running server or external assets.

See examples/notebooks/scientific_workflow.dnb for a stateful matrix and automatic-differentiation notebook, and examples/notebooks/plot_gallery.dnb for inline line, scatter, bar, histogram, and pie charts. The statistical_analysis.dnb notebook combines seeded sampling, descriptive statistics, confidence intervals, matrix regression, rolling windows, and inline diagnostic charts.

Writing tests

Dune has tests built into the language. Tests can be written as a named test block or as a zero-argument unit function marked with @test. The dune test command discovers both forms and reports the results.

A first test

from assert import assert_eq;

fn double(x: int): int {
    return x * 2;
}

test "double multiplies by two" {
    assert_eq(double(2), 4);
    assert_eq(double(0), 0);
}

Run it with:

dune test path/to/file.dn
running 1 test
test "double multiplies by two" ... ok

test result: ok. 1 passed; 0 failed

The name after test is an ordinary string literal, so it can contain spaces and punctuation. The body is a normal block: it can declare bindings, call functions, and loop, just like a function body.

The function form is useful when a test should also have an ordinary declaration name:

@test
fn double_handles_zero(): unit {
    assert_eq(double(0), 0);
}

An attributed test function must take no parameters, explicitly return unit, and cannot be generic, foreign, or foreknown.

Use @ignore to keep a temporarily disabled test discoverable, optionally with a reason:

@test
@ignore("requires a local database")
fn database_round_trip(): unit { }

Use @should_panic when failure is the behavior under test. An optional text argument requires the panic report to contain that text:

import runtime;

@test
@should_panic("index out of bounds")
fn rejects_invalid_index(): unit {
    runtime.panic("index out of bounds");
}

An expected-panic test fails if it returns normally or produces a different message. Use @should_fail("message") instead when any typed runtime failure, such as a bounds or arithmetic error, is acceptable. @ignore, @should_panic, and @should_fail are mutually exclusive.

Assertions

The assert module provides the helpers that fail a test. Each one calls runtime.panic when its check does not hold, which aborts the current test and marks it as failed:

from assert import assert_eq, assert_true, assert_false;

test "assertions" {
    assert_eq(1 + 1, 2);          // any comparable type
    assert_true(1 < 2);
    assert_false(2 < 1);
}

assert_eq<T> works for any type whose values can be compared with ==, so it handles integers, reals, text, and booleans alike. Import the helpers with from assert import ... to call them unqualified, or import assert; and write assert.assert_eq(...).

How tests run

  • Isolation. dune test runs only attributed test functions and test blocks. A file's top-level code (statements outside any function or test) does not run, so a script and its tests can live in the same file.
  • Shared declarations. Top-level functions, constants, records, and imports are all in scope inside every test, so tests exercise the same code the rest of the file uses.
  • Independent failures. A failing assertion aborts only the test it is in. The remaining tests still run, and the final line summarises how many passed and failed.
  • Exit code. dune test exits non-zero if any test fails, which lets it gate a CI pipeline.
  • Ignored tests. @ignore tests are not executed and are reported in a separate ignored count; they do not make the command fail.

A failing run looks like this:

running 2 tests
test "this assertion holds" ... ok
test "this assertion fails" ... FAILED
    panic: assertion failed: values are not equal
    stack trace:
      0: assert.assert_eq
          at stdlib/assert.dn:54:9
      1: test "this assertion fails"
          at tests/example.dn:8:5

test result: FAILED. 1 passed; 1 failed

The named test "..." block is a real outer stack frame, so failures retain the assertion helper, user functions, imported module files, and the exact line inside the test. See Runtime errors and stack traces.

Rules

  • test blocks and @test functions are only allowed at the top level of a file. A test inside a function or another block is a compile-time error.
  • test blocks are ignored when a file is run normally. An @test function is not run automatically, but remains callable like any other named function.

Editor integration

Dune ships a Language Server (dune lsp) and a Zed extension.

What the language server provides

  • Diagnostics — type errors plus @deprecated, @experimental, and @must_use warnings with precise source ranges.
  • Completions — keywords, built-in attributes, local symbols, imported module members, and typed receiver methods.
  • Hover — the signature, attributes, and doc-comment of a symbol, including symbols from other modules.
  • Go-to-definition — jumps to a local declaration or into the module file for imported symbols, aliases, and from ... import symbols.
  • Semantic highlighting — distinguishes functions, methods, types, generic parameters, constants, variables, fields, modules, literals, operators, and doc comments and attribute decorators. The server reports standard LSP token types and modifiers such as declaration, readonly, static, and defaultLibrary.

Any editor that speaks LSP can talk to dune lsp over stdio.

Zed extension

The Zed extension lives in editors/zed/. It provides tree-sitter syntax highlighting, a symbol outline, and wires up the dune lsp server. Semantic tokens from the language server refine symbol roles when the server is running; tree-sitter remains the lexical fallback while the server starts or is unavailable. The extension pins the tree-sitter grammar to a commit of this repository, so fallback highlighting always matches the language version.

To use it, install the extension as a Zed dev extension pointing at editors/zed/, and make sure the dune binary (which also serves dune lsp) is built and on your PATH. After pulling changes that touch the LSP, rebuild the dune binary so the editor picks up new behavior.

Examples

Runnable examples live in the examples/ directory and are covered by golden output tests. Run any of them with the dune binary:

dune examples/matrix_basics.dn
ExampleWhat it shows
matrix_basics.dnVectors and matrices from the matrix module.
vector_stats.dnReductions and statistics over a vector.
linear_regression.dnA small numerical program: least-squares fit.
statistical_analysis.dnDescriptive statistics, regression, histograms, probability helpers, and matrix/plot integration.
collection_pipeline.dnThe higher-order filter/map/sum pipeline with function values.
functions_and_closures.dnTyped lambdas, capture snapshots, shared aggregate handles, nested/generic closures, stored closures, composition, callbacks, and callable-expression chains.
defer_cleanup.dnDeterministic resource cleanup with defer: LIFO, captures, early exits, ?, and loop scopes.
geometry.dn + geometry_demo.dnA two-file program showing module declarations, aliases, and selective imports.
documented.dnEvery comment form plus brief/param/returns doc-comments on a function, record fields, and a method.

The documented.dn example is a good starting point for seeing how doc-comments render on hover in an editor — open it with the Zed extension and hover the documented names.

Interactive examples live under examples/notebooks/. Open functions_and_closures.dnb for an unexecuted, cell-by-cell tour of lambdas, capture semantics, nested and generic closures, callback pipelines, and plot integration.