Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5d7b0c2
Add inline struct methods with eager where semantics
Ryan-D-Gast Feb 28, 2026
bb74788
Refactor struct method dispatch and metadata plumbing
Ryan-D-Gast Mar 2, 2026
926fffe
Add DefineMethod statement and split method compilation path
Ryan-D-Gast Mar 2, 2026
cca48c7
Fix struct method compilation via pre-registration
Ryan-D-Gast Mar 4, 2026
034663c
Support Self struct instantiation in method bodies
Ryan-D-Gast Mar 4, 2026
d590370
Optimize struct instance representation and instantiation reuse
Ryan-D-Gast Mar 4, 2026
d5ba575
Deduplicate struct-method tests and sync docs/examples
Ryan-D-Gast Mar 4, 2026
5217192
Add struct field defaults with parser/typechecker support
Ryan-D-Gast Mar 2, 2026
ee543d6
Add struct-owned operator methods
Ryan-D-Gast Mar 13, 2026
9a5819a
Fix formatting and clippy warnings
Ryan-D-Gast Mar 13, 2026
000c044
Infer operator decorator types from methods
Ryan-D-Gast Mar 13, 2026
5cf293f
Add reverse struct operator decorators
Ryan-D-Gast Mar 13, 2026
6a1b71b
Expand reverse operator coverage
Ryan-D-Gast Mar 13, 2026
0d5da59
Allow multiline grouped infix expressions
Ryan-D-Gast Mar 13, 2026
cbe8354
Stabilize parser and example CI coverage
Ryan-D-Gast Mar 13, 2026
82bcec1
Fix generic struct operator dispatch
Ryan-D-Gast Mar 13, 2026
513a0fb
Document generic struct operators
Ryan-D-Gast Mar 13, 2026
ba0594e
Add indexing support for lists and structs
Ryan-D-Gast Mar 13, 2026
e61cff4
Add matrix indexing and multiplication example
Ryan-D-Gast Mar 13, 2026
3ad3d7a
Fix deferred indexing after operator overloads
Ryan-D-Gast Mar 13, 2026
9be89e2
Merge origin/main into feature/struct-operator-methods
Ryan-D-Gast Mar 13, 2026
af62e61
Regenerate docs for lists prelude
Ryan-D-Gast Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import os
import sys


SCRIPT_DIR = Path(__file__).parent.resolve()
WORKSPACE_DIR = SCRIPT_DIR.parent
NUMBAT_CRATE_DIR = WORKSPACE_DIR / "numbat"
Expand Down Expand Up @@ -137,6 +136,7 @@ def generate_all_examples():
generate_example("body_mass_index", "Body mass index")
generate_example("factorial", "Factorial", strip_asserts=False)
generate_example("medication_dosage", "Medication dosage")
generate_example("matrix", "Matrices")
generate_example("molarity", "Molarity")
generate_example("musical_note_frequency", "Musical note frequency")
generate_example("paper_size", "Paper sizes")
Expand Down
11 changes: 11 additions & 0 deletions book/src/basics/lists.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ Lists can be created using the `[…]` syntax. For example:
The type of a list is written as `List<T>`, where `T` is the type of the elements. The types of the lists
above are `List<Length>`, `List<String>`, and `List<List<Scalar>>`, respectively.

Lists can be indexed with square brackets:

```nbt
let xs = [30 cm, 110 cm, 2 m]

xs[0] # 30 cm
xs[2] # 2 m
```

List indices must be non-negative integers. An out-of-bounds index yields a runtime error.

The standard library provides a [number of functions](../prelude/functions/lists.md) to work with lists. Some
useful things to do with lists are:
```nbt
Expand Down
134 changes: 134 additions & 0 deletions book/src/basics/structs.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ let tungsten = Element {
}
```

Struct fields can also define default values:

```nbt
struct PaperSize {
width: Length = 210 mm,
height: Length = 297 mm,
}
```

When instantiating, fields with defaults may be omitted:

```nbt
let a4 = PaperSize {}
let custom = PaperSize { width: 100 mm } # height defaults to 297 mm
let swapped = PaperSize { height: 400 mm } # order does not matter
```

Fields can be accessed using dot notation:

```nbt
Expand All @@ -33,6 +50,31 @@ let side_length = cbrt(mass / tungsten.density) -> cm
print("A tungsten cube with a mass of {mass} has a side length of {side_length:.2}.")
```

Field access is often enough for simple data, but structs can also define behavior with methods:

```nbt
struct Element {
name: String,
atomic_number: Scalar,
density: MassDensity,

fn tungsten() -> Self = Self {
name: "Tungsten",
atomic_number: 74,
density: 19.25 g/cm³,
}

fn cube_side_length(self, mass: Mass) -> Length =
cbrt(mass / self.density)
}

let tungsten = Element::tungsten()
let side_length = tungsten.cube_side_length(1 kg) -> cm
print("A 1 kg tungsten cube has side length {side_length:.2}.")
```

Note that constructor methods can return `Self` or `Element` in this case, but the former is more concise and is preferred by convention.

## Generic structs

Structs can be generic over type parameters. Type parameters are declared in angle brackets after the struct name:
Expand All @@ -57,3 +99,95 @@ struct Vec<X: Dim> {
let position = Vec { x: 1 m, y: 2 m }
let velocity: Vec<Velocity> = Vec { x: 1 m/s, y: 2 m/s }
```

Structs with generic type parameters can also have methods that use those type parameters, and methods can introduce additional type parameters of their own:

```nbt
struct Vec<X: Dim> {
x: X,
y: X,

@add
fn add(self, rhs: Self) -> Self =
Vec { x: self.x + rhs.x, y: self.y + rhs.y }

@mul
fn scale(self, factor: Scalar) -> Self =
Vec { x: self.x * factor, y: self.y * factor }

@rmul
fn scale_from_left(self, lhs: Scalar) -> Self =
Vec { x: lhs * self.x, y: lhs * self.y }

fn dot<Y: Dim>(self, other: Vec<Y>) -> X * Y =
self.x * other.x + self.y * other.y

fn new(x: X, y: X) -> Self =
Vec { x: x, y: y }
}

let v1 = Vec::new(1 m, 2 m)
let v2 = Vec { x: 3 m, y: 4 m }
let v2_cm = Vec { x: 300 cm, y: 400 cm }

let v3 = v1.scale(2) # Vec { x: 2 m, y: 4 m }
let combined = v1 + v2
let v4 = 2 * v1
let dp_m = v1.dot(v2) # 11 m²
let dp_cm = v1.dot(v2_cm) # 110_000 cm²
```

Operator overloads are declared on struct instance methods with decorators such as `@add`, `@sub`, `@mul`, and `@div`. The decorator itself is bare; the compiler infers the left-hand side from `self`, the right-hand side from the second parameter, and the result type from the return type:

```nbt
struct Vec<X: Dim> {
x: X,
y: X,

@add
fn add(self, rhs: Self) -> Self =
Vec { x: self.x + rhs.x, y: self.y + rhs.y }

@mul
fn scale(self, factor: Scalar) -> Self =
Vec { x: self.x * factor, y: self.y * factor }

@rmul
fn scale_from_left(self, lhs: Scalar) -> Self =
Vec { x: lhs * self.x, y: lhs * self.y }
}

let v = Vec { x: 3 m, y: 4 m }
let w = Vec { x: 300 cm, y: 400 cm }

let combined = v + w
let scaled = v * 2
let scaled_from_left = 2 * v
```

Reverse decorators `@radd`, `@rsub`, `@rmul`, and `@rdiv` allow the struct to handle expressions where it appears on the right-hand side. In the example above, `@rmul fn scale_from_left(self, lhs: Scalar)` enables `2 * v`.

Structs can also define indexing behavior with the `@index` decorator on an instance method. The parameters after `self` determine how many index arguments the struct accepts:

```nbt
struct Grid2 {
a: Scalar,
b: Scalar,
c: Scalar,
d: Scalar,

@index
fn get(self, row: Scalar, col: Scalar) -> Scalar =
if row == 0 then
if col == 0 then self.a else self.b
else
if col == 0 then self.c else self.d
}

let g = Grid2 { a: 1, b: 2, c: 3, d: 4 }

assert_eq(g[0, 1], 2)
assert_eq(g[1, 0], 3)
```

This is directional and struct-owned, just like operator methods. Builtin lists also support indexing with `xs[i]`.
86 changes: 86 additions & 0 deletions book/src/examples/example-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!-- This file is autogenerated! Do not modify it -->

# Matrices

[:material-play-circle: Run this example](https://numbat.dev/?q=%23+A+dynamically+sized+matrix+backed+by+nested+lists.%0A%23%0A%23+This+example+uses%3A%0A%23+-+a+struct+with+List%3CList%3CScalar%3E%3E+storage%0A%23+-+builtin+list+indexing+%28%60xs%5Bi%5D%60%29%0A%23+-+custom+struct+indexing+via+%60matrix%5Brow%2C+col%5D%60%0A%23+-+struct-owned+matrix+multiplication+via+%60%40mul%60%0A%0Astruct+Matrix+%7B%0A++++rows%3A+Scalar%2C%0A++++cols%3A+Scalar%2C%0A++++data%3A+List%3CList%3CScalar%3E%3E%2C%0A%0A++++fn+from_rows%28data%3A+List%3CList%3CScalar%3E%3E%29+-%3E+Self+%3D%0A++++++++Self+%7B%0A++++++++++++rows%3A+len%28data%29%2C%0A++++++++++++cols%3A+if+is_empty%28data%29+then+0+else+len%28data%5B0%5D%29%2C%0A++++++++++++data%3A+data%2C%0A++++++++%7D%0A%0A++++%40index%0A++++fn+get%28self%2C+row%3A+Scalar%2C+col%3A+Scalar%29+-%3E+Scalar+%3D%0A++++++++self.data%5Brow%5D%5Bcol%5D%0A%0A++++fn+dot_product%28lhs%3A+Self%2C+rhs%3A+Self%2C+row%3A+Scalar%2C+col%3A+Scalar%2C+i%3A+Scalar%29+-%3E+Scalar+%3D%0A++++++++if+i+%3D%3D+lhs.cols%0A++++++++++++then+0%0A++++++++++++else+lhs%5Brow%2C+i%5D+%2A+rhs%5Bi%2C+col%5D+%2B+Matrix%3A%3Adot_product%28lhs%2C+rhs%2C+row%2C+col%2C+i+%2B+1%29%0A%0A++++fn+product_row%28lhs%3A+Self%2C+rhs%3A+Self%2C+row%3A+Scalar%2C+col%3A+Scalar%29+-%3E+List%3CScalar%3E+%3D%0A++++++++if+col+%3D%3D+rhs.cols%0A++++++++++++then+%5B%5D%0A++++++++++++else+cons%28%0A++++++++++++++++Matrix%3A%3Adot_product%28lhs%2C+rhs%2C+row%2C+col%2C+0%29%2C%0A++++++++++++++++Matrix%3A%3Aproduct_row%28lhs%2C+rhs%2C+row%2C+col+%2B+1%29%2C%0A++++++++++++%29%0A%0A++++fn+product_rows%28lhs%3A+Self%2C+rhs%3A+Self%2C+row%3A+Scalar%29+-%3E+List%3CList%3CScalar%3E%3E+%3D%0A++++++++if+row+%3D%3D+lhs.rows%0A++++++++++++then+%5B%5D%0A++++++++++++else+cons%28%0A++++++++++++++++Matrix%3A%3Aproduct_row%28lhs%2C+rhs%2C+row%2C+0%29%2C%0A++++++++++++++++Matrix%3A%3Aproduct_rows%28lhs%2C+rhs%2C+row+%2B+1%29%2C%0A++++++++++++%29%0A%0A++++%40mul%0A++++fn+mul%28self%2C+rhs%3A+Self%29+-%3E+Self+%3D%0A++++++++Matrix%3A%3Afrom_rows%28Matrix%3A%3Aproduct_rows%28self%2C+rhs%2C+0%29%29%0A%7D%0A%0Alet+identity_2+%3D+Matrix%3A%3Afrom_rows%28%5B%0A++++%5B1%2C+0%5D%2C%0A++++%5B0%2C+1%5D%2C%0A%5D%29%0A%0Alet+wide+%3D+Matrix%3A%3Afrom_rows%28%5B%0A++++%5B1%2C+2%2C+3%5D%2C%0A++++%5B4%2C+5%2C+6%5D%2C%0A%5D%29%0A%0Alet+tall+%3D+Matrix%3A%3Afrom_rows%28%5B%0A++++%5B1%2C+2%5D%2C%0A++++%5B3%2C+4%5D%2C%0A++++%5B5%2C+6%5D%2C%0A%5D%29%0A%0Alet+product+%3D+wide+%2A+tall%0Alet+unchanged+%3D+identity_2+%2A+identity_2%0A%0A%0A%0A%0A%0A%0Aprint%28%222x2+identity%3A+%7Bidentity_2%7D%22%29%0Aprint%28%222x3+matrix%3A+++%7Bwide%7D%22%29%0Aprint%28%223x2+matrix%3A+++%7Btall%7D%22%29%0Aprint%28%22product%3A++++++%7Bproduct%7D%22%29%0A){ .md-button .md-button--primary }

```numbat
# A dynamically sized matrix backed by nested lists.
#
# This example uses:
# - a struct with List<List<Scalar>> storage
# - builtin list indexing (`xs[i]`)
# - custom struct indexing via `matrix[row, col]`
# - struct-owned matrix multiplication via `@mul`

struct Matrix {
rows: Scalar,
cols: Scalar,
data: List<List<Scalar>>,

fn from_rows(data: List<List<Scalar>>) -> Self =
Self {
rows: len(data),
cols: if is_empty(data) then 0 else len(data[0]),
data: data,
}

@index
fn get(self, row: Scalar, col: Scalar) -> Scalar =
self.data[row][col]

fn dot_product(lhs: Self, rhs: Self, row: Scalar, col: Scalar, i: Scalar) -> Scalar =
if i == lhs.cols
then 0
else lhs[row, i] * rhs[i, col] + Matrix::dot_product(lhs, rhs, row, col, i + 1)

fn product_row(lhs: Self, rhs: Self, row: Scalar, col: Scalar) -> List<Scalar> =
if col == rhs.cols
then []
else cons(
Matrix::dot_product(lhs, rhs, row, col, 0),
Matrix::product_row(lhs, rhs, row, col + 1),
)

fn product_rows(lhs: Self, rhs: Self, row: Scalar) -> List<List<Scalar>> =
if row == lhs.rows
then []
else cons(
Matrix::product_row(lhs, rhs, row, 0),
Matrix::product_rows(lhs, rhs, row + 1),
)

@mul
fn mul(self, rhs: Self) -> Self =
Matrix::from_rows(Matrix::product_rows(self, rhs, 0))
}

let identity_2 = Matrix::from_rows([
[1, 0],
[0, 1],
])

let wide = Matrix::from_rows([
[1, 2, 3],
[4, 5, 6],
])

let tall = Matrix::from_rows([
[1, 2],
[3, 4],
[5, 6],
])

let product = wide * tall
let unchanged = identity_2 * identity_2






print("2x2 identity: {identity_2}")
print("2x3 matrix: {wide}")
print("3x2 matrix: {tall}")
print("product: {product}")
```
54 changes: 53 additions & 1 deletion book/src/examples/example-numbat_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ struct Element { # Define a struct
name: String,
atomic_number: Scalar,
density: MassDensity,

fn neptunium() -> Self =
Self { name: "Neptunium", atomic_number: 93, density: 20.45 g/cm³ }
}

let hydrogen = Element { # Instantiate it
Expand All @@ -146,8 +149,57 @@ let hydrogen = Element { # Instantiate it

hydrogen.density # Access the field of a struct

let neptunium = # Instantiate using a constructor method
Element::neptunium()
print(neptunium)

struct Vec2<D: Dim> { # A generic struct with type parameter
x: D,
y: D,
y: D = 0 m, # Struct defaults can be provided per field

# Methods can have their own type generics and inherit the struct's generics
fn new(x: D, y: D) -> Self = Self { x: x, y: y }
@add
fn add(self, rhs: Self) -> Self = Self { x: self.x + rhs.x, y: self.y + rhs.y }
@mul
fn scale(self, factor: Scalar) -> Self = Self { x: self.x * factor, y: self.y * factor }
@rmul
fn scale_from_left(self, lhs: Scalar) -> Self = Self { x: lhs * self.x, y: lhs * self.y }
fn dot<E: Dim>(self, other: Vec2<E>) -> D * E = self.x * other.x + self.y * other.y
}

let v = Vec2::new(3 m, 4 m) # Constructor call
let w = Vec2::new(300 cm, 400 cm)
let scaled_v = v.scale(2) # Returning `Self` creates a new value (no in-place mutation)
let combined = v + w # Operator methods can provide struct-owned arithmetic
let left_scaled = 2 * v # Reverse operator methods handle lhs-owned scalar syntax

assert_eq(v.dot(w) -> m², 25 m²) # Method generic + unit conversion
assert_eq(combined.x -> m, 6 m)
assert_eq(left_scaled.y -> m, 8 m)

let only_x = Vec2 { x: 5 m } # Omitted fields with defaults are auto-filled

assert_eq(only_x.x, 5 m)
assert_eq(only_x.y, 0 m)

let distances = [3 m, 4 m, 5 m] # Builtin lists support indexing
assert_eq(distances[1], 4 m)

struct Grid2 { # Struct methods can define custom indexing syntax
a: Scalar,
b: Scalar,
c: Scalar,
d: Scalar,

@index
fn get(self, row: Scalar, col: Scalar) -> Scalar =
if row == 0 then
if col == 0 then self.a else self.b
else
if col == 0 then self.c else self.d
}

let grid = Grid2 { a: 1, b: 2, c: 3, d: 4 }
assert_eq(grid[1, 0], 3)
```
Loading
Loading