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
| Category | Types |
|---|---|
| Signed integers | int, i8, i16, i32, i64, isize |
| Unsigned integers | u8, u16, u32, u64, usize, uint8, uint16, uint32, uint64 |
| Reals | real, real32, real64 |
| Other | bool, 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 kind | Copy result | Mutable through the value? |
|---|---|---|
| integers and reals | independent value | no |
bool, glyph, unit | independent value | no |
text | independent immutable value | no |
fn(...) | independent function handle | no |
array [T] | handle to the same array | yes |
| record | handle to the same record | yes |
| tuple | immutable tuple value; nested handles remain shared | no outer mutation |
| choice | immutable tag/payload value; a handle in the payload remains shared | no 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:
| Type | Mutates existing object | Returns fresh structure |
|---|---|---|
| array | indexed assignment, push, pop, clear | copy, slicing, append, prepend, concat |
Dict<V> | set, remove, clear | copy, keys, values |
Set | add, remove, clear | copy, values |
Vector<T> | set, fill | copy, slice, concat, arithmetic, reshape |
Matrix<T> | set, fill | copy, 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 copygenerates a new outer record and copies each field using normal Dune value semantics;Dict<V>.copy()creates fresh key/value arrays, whileVvalues are copied shallowly;Set.copy()creates a fresh backing array (its elements are immutabletext);Vector.copy()andMatrix.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
moveexpression; - 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 theinmembership 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:
eqgeneratesequals(structural equality).copygeneratescopy(a shallow copy: the record is new, but nested arrays and records remain shared).debuggeneratesto_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:
| Operator | Method |
|---|---|
a + b | a.add(b) |
a - b | a.sub(b) |
a * b | a.mul(b) |
a / b | a.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:
maybe—Maybe<T>withpresent(value),absent(default), andvalue_or().outcome—Outcome<T, E>withdone,failed, andfailure_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
deferare 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:
- provide an idempotent
close(),release(), or similarly explicitunitoperation; - show
defer resource.close();immediately after successful acquisition in their documentation; - use an
outcome.Outcomeresult 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:
| Category | Meaning |
|---|---|
panic | An explicit runtime.panic call. |
bounds error | Invalid array, tuple, record, text, or slice position. |
arithmetic error | An arithmetic operation cannot produce a value, such as division by zero. |
runtime type error | Invalid operands reached the VM or a dynamic callable had the wrong shape. |
I/O error | A non-recoverable stream/runtime I/O failure. Normal filesystem APIs return Outcome values. |
foreign function error | A C FFI symbol or invocation is unsupported or invalid. |
VM error | A 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.
| Attribute | Purpose |
|---|---|
@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. |
@test | Register 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— TheDisplaycontract andshowhelper.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 optionalMaybe<T>choice and helpers.outcome— The result-styleOutcome<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 aspanic.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.dnbytools/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.dnbytools/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.dnbytools/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) valuegrad: 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():
Auto-generated from
stdlib/canvas.dnbytools/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") // 1fn 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") // 1fn 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") // 1fn 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") // 1fn 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<") // 1fn 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") // 1fn 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<") // 1fn 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<") // 1fn 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<") // 1fn 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<") // 1fn 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") // 1fn 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() // 1fn 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 withresult.text(name),.as_int(name),.as_real64(name), or.as_bool(name)— each returns aMaybe. - Flags are boolean switches:
.flag(name, short, description), read withresult.flag(name)→bool. - Positionals are order-based:
.positional(name, description)(required) or.optional_positional(...), read withresult.positional(name)orresult.positional(index)→Maybe<text>. .parse(args)returnsOutcome<ParseResult, text>:Donewith the parsed result, orFailedwith a message likeunexpected positional argument 'extra'.--help/--versionshort-circuit; checkresult.is_help()/result.is_version()and printresult.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.dnbytools/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 namedname, 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("") // dunefn 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) // 3fn 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) // 1fn 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.5fn 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") // 1fn 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("?") // srcfn 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("?") // srcfn 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() // 1fn 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() // 1fn 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;--versionprints "". — e.g. cli.command("greet").version("1.0.0").version_output() // greet 1.0.0fn 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("") // worldfn 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") // 1fn 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() // 69fn version_output(): text— Deterministic version text for this command. — e.g.cli.command("tool").version("1.0").version_output() // tool 1.0fn 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.dnbytools/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.dnbytools/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.dnbytools/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() // 0fn set(key: text, value: V): unit— Insert or overwrite the value forkey. — e.g.d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.get("a").value_or(0) // 1fn get(key: text): maybe.Maybe<V>— Look upkey, returningPresent(value)orAbsent. — e.g.d: dict.Dict<int> = dict.Dict.new(); d.get("missing").value_or(0) // 0fn contains(key: text): bool— True whenkeyhas an associated value. — e.g.d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.contains("a") // 1fn remove(key: text): bool— Removekey; returns whether a value was actually removed. — e.g.d: dict.Dict<int> = dict.Dict.new(); d.set("a", 1); d.remove("a") // 1fn 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() // 1fn 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.dnbytools/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.dnbytools/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.dnbytools/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.dnbytools/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.dnbytools/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.dnbytools/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.dnbytools/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 atindex.fn set(index: int, value: T): unit— Overwrite the element atindexin 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 withother(same length and same values).fn same_shape(other: Vector<T>): bool— True whenotherhas 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 byother, as a new vector.fn fill(value: T): unit— Overwrite every element withvaluein 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 scalarvalueto 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 scalarvaluefrom every element (broadcast).fn rsub(value: T): Vector<T>— Reverse-subtract: each element becomesvalueminus 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 becomesvaluedivided by the element.fn scale(factor: T): Vector<T>— Multiply every element by scalarfactor(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 withother: sum of element-wise products. — e.g.matrix.vector([1, 2, 3]).dot(matrix.vector([4, 5, 6])) // 32fn 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() // 5fn distance_squared(other: Vector<T>): T— Squared distance toother(norm_squared of the difference).fn distance(other: Vector<T>): real64— Euclidean distance toother, as real64.fn sum(): T— Sum of all elements (starts from additive identity 0). — e.g.matrix.vector([1, 2, 3, 4]).sum() // 10fn 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.5fn 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 asdot(delegates to matmul).fn matmul(other: Matrix<T>): Vector<T>— Treat this vector as a row and multiply by matrixother(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 withother(same shape and same values).fn same_shape(other: Matrix<T>): bool— True whenotherhas the same rows and cols.fn can_matmul(other: Matrix<T>): bool— True when this matrix can be multiplied byother(cols == other.rows).fn fill(value: T): unit— Overwrite every element withvaluein place.fn row(row: int): Vector<T>— Extract rowrowas a Vector.fn column(col: int): Vector<T>— Extract columncolas 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 fordiagonal.fn trace(): T— Trace: the sum of the diagonal entries. — e.g.matrix.from_rows([[1, 2], [3, 4]]).trace() // 5fn 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 becomesvalueminus 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 becomesvaluedivided by the element.fn scale(factor: T): Matrix<T>— Multiply every element by scalarfactor(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 asdot.fn dot(vector: Vector<T>): Vector<T>— Matrix-times-vector spelled asdot.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() // 10fn 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.5fn 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() // -2fn 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.dnbytools/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.dnbytools/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);
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);
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);
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);
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);
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);
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);
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);
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);
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
Auto-generated from
stdlib/plot.dnbytools/gen_stdlib_docs.py.
record Chart
Methods:
static fn empty(): Chartfn 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") // 1fn 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 towidthxheightpixels (both must be positive). — e.g.plot.svg(plot.line([1.0, 2.0]).size(640, 480)).contains("width=\"640\"") // 1fn grid(cells: int): Chart— Return a copy with a background grid ofcellsmajor divisions per axis — e.g.plot.svg(plot.line([1.0, 2.0]).grid(5)).contains("plot-grid") // 1fn minor_grid(subdivisions: int): Chart— Return a copy that also drawssubdivisionsminor grid lines inside each — e.g.plot.svg(plot.line([1.0, 2.0]).grid(4).minor_grid(5)).contains("plot-grid-minor") // 1fn label(value: text): Chartfn add_line(xs: [real64], ys: [real64]): Chartfn add_line(ys: [real64]): Chartfn add_scatter(xs: [real64], ys: [real64]): Chartfn add_scatter(ys: [real64]): Chartfn add_bar(xs: [real64], ys: [real64]): Chartfn add_bar(ys: [real64]): Chartfn add_area(xs: [real64], ys: [real64]): Chartfn add_area(ys: [real64]): Chartfn add_step(xs: [real64], ys: [real64]): Chartfn 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.dnbytools/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.dnbytools/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 fromseed, 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);hiis 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.dnbytools/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: intstart: intend: intvalue: text
record Match
A single match with byte/glyph offsets into the searched text.
Fields:
start: intend: intvalue: textcaptures: [Capture]
Methods:
fn len(): intfn is_empty(): boolfn capture(index: int): maybe.Maybe<Capture>fn capture_text(index: int, default: text): text— The text of capture groupindex, ordefaultwhen 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") // 1fn 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 // 123fn 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 // 123fn 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() // 3fn 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() // 3fn replace(input: text, replacement: text): text— Replace the first match;$1..$9inreplacementexpand captures. — e.g.regex.compile(r"\d+").value_or(regex.never()).replace("abc123def456", "#") // abc#def456fn replace_all(input: text, replacement: text): text— Replace every match;$1..$9inreplacementexpand 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.dnbytools/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.dnbytools/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() // 0fn add(value: text): unit— Addvalue; duplicates are ignored. — e.g.s = set.Set.new(); s.add("a"); s.contains("a") // 1fn contains(value: text): bool— True whenvalueis a member of the set. — e.g.s = set.Set.new(); s.add("a"); s.contains("b") // 0fn remove(value: text): bool— Removevalue; returns whether it was present. — e.g.s = set.Set.new(); s.add("a"); s.remove("a") // 1fn 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() // 1fn copy(): Set— A copy with a fresh backing array. — e.g.s = set.Set.new(); s.add("a"); s.copy().contains("a") // 1fn 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 inreal64. Core operations have matchingmatrix.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. varianceandstddevare sample estimators (denominatorn-1), whilepvarianceandpstdevare population estimators (denominatorn). This mirrors Pythonstatisticsand Julia's corrected estimator terminology.quantile(data, q)usesqin[0, 1];percentile(data, p)usespin[0, 100]. The default is Hyndman-Fan Type 7 linear interpolation, the common default in R, NumPy, and Julia.quantile_withalso exposeslower,higher,nearest, andmidpointinterpolation.- 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
Summaryrecord. - 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.dnbytools/gen_stdlib_docs.py.
record FiveNumberSummary derive eq, copy, debug
A compact five-number summary, using Type-7 linear quantiles.
Fields:
minimum: real64first_quartile: real64median: real64third_quartile: real64maximum: 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: intsum: real64mean: real64minimum: real64maximum: real64range: real64population_variance: real64sample_variance: real64population_stddev: real64sample_stddev: real64standard_error: real64median: real64first_quartile: real64third_quartile: real64interquartile_range: real64skewness: real64excess_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: real64intercept: real64correlation: real64r_squared: real64residual_sum_squares: real64mean_squared_error: real64root_mean_squared_error: real64residual_standard_error: real64sample_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: intincluded_count: intunderflow: intoverflow: 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: real64lower: real64upper: real64margin: real64confidence: 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.dnbytools/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
.dnbfile 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 (
Yfor code,Mfor Markdown); Shift+Enterruns a cell and selects the next one, creating a Code cell at the end;Cmd/Ctrl+Enterruns 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
plotmodule; - 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 testruns only attributed test functions andtestblocks. 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 testexits non-zero if any test fails, which lets it gate a CI pipeline. - Ignored tests.
@ignoretests 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
testblocks and@testfunctions are only allowed at the top level of a file. A test inside a function or another block is a compile-time error.testblocks are ignored when a file is run normally. An@testfunction 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_usewarnings 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 ... importsymbols. - 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, anddefaultLibrary.
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
| Example | What it shows |
|---|---|
matrix_basics.dn | Vectors and matrices from the matrix module. |
vector_stats.dn | Reductions and statistics over a vector. |
linear_regression.dn | A small numerical program: least-squares fit. |
statistical_analysis.dn | Descriptive statistics, regression, histograms, probability helpers, and matrix/plot integration. |
collection_pipeline.dn | The higher-order filter/map/sum pipeline with function values. |
functions_and_closures.dn | Typed lambdas, capture snapshots, shared aggregate handles, nested/generic closures, stored closures, composition, callbacks, and callable-expression chains. |
defer_cleanup.dn | Deterministic resource cleanup with defer: LIFO, captures, early exits, ?, and loop scopes. |
geometry.dn + geometry_demo.dn | A two-file program showing module declarations, aliases, and selective imports. |
documented.dn | Every 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.