Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 3 additions & 4 deletions book/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
4. Runs zensical build
"""

import subprocess
from pathlib import Path
import urllib.parse
import os
import subprocess
import sys

import urllib.parse
from pathlib import Path

SCRIPT_DIR = Path(__file__).parent.resolve()

Expand Down
48 changes: 48 additions & 0 deletions book/src/basics/structs.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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 +82,26 @@ 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,

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

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

let v1 = Vec { x: 1 m, y: 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 dp_m = v1.dot_product(v2) # 11 m²
let dp_cm = v1.dot_product(v2_cm) # 110_000 cm²
```
18 changes: 18 additions & 0 deletions 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,23 @@ 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,

# 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 }
fn scale(self, factor: Scalar) -> Self = Self { x: self.x * factor, y: self.y * factor }
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)

assert_eq(v.dot(w) -> m², 25 m²) # Method generic + unit conversion
```
42 changes: 22 additions & 20 deletions book/src/examples/example-paper_size.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Paper sizes

[:material-play-circle: Run this example](https://numbat.dev/?q=%23+Compute+ISO+216+paper+sizes+for+the+A+series%0A%23%0A%23+https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FISO_216%0A%0Astruct+PaperSize+%7B%0A++++width%3A+Length%2C%0A++++height%3A+Length%2C%0A%7D%0A%0Afn+paper_size_A%28n%3A+Scalar%29+-%3E+PaperSize+%3D%0A++if+n+%3D%3D+0%0A++++then%0A++++++PaperSize+%7B%0A++++++++width%3A+841+mm%2C%0A++++++++height%3A+1189+mm%0A++++++%7D%0A++++else%0A++++++PaperSize+%7B%0A++++++++width%3A+floor_in%28mm%2C+paper_size_A%28n+-+1%29.height+%2F+2%29%2C%0A++++++++height%3A+paper_size_A%28n+-+1%29.width%2C%0A++++++%7D%0A%0A%0Afn+paper_area%28size%3A+PaperSize%29+-%3E+Area+%3D%0A++++size.width+%2A+size.height%0A%0A%0Afn+size_as_string%28size%3A+PaperSize%29+%3D+%22%7Bsize.width%3A%3E4%7D+%C3%97+%7Bsize.height%3A%3E5%7D+++%7Bpaper_area%28size%29+-%3E+cm%C2%B2%3A%3E6.1f%7D%22%0Afn+row%28n%29+%3D+%22A%7Bn%3A%3C3%7D+++%7Bsize_as_string%28paper_size_A%28n%29%29%7D%22%0A%0Aprint%28%22Name++++Width+++++Height++++++++Area++%22%29%0Aprint%28%22----+++-------+++--------+++----------%22%29%0Aprint%28join%28map%28row%2C+range%280%2C+10%29%29%2C+%22%5Cn%22%29%29%0A){ .md-button .md-button--primary }
[:material-play-circle: Run this example](https://numbat.dev/?q=%23+Compute+ISO+216+paper+sizes+for+the+A+series%0A%23%0A%23+https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FISO_216%0A%0Astruct+PaperSize+%7B%0A++++width%3A+Length%2C%0A++++height%3A+Length%2C%0A%0A++++fn+a%28n%3A+Scalar%29+-%3E+Self+%3D%0A++++++if+n+%3D%3D+0%0A++++++++then%0A++++++++++PaperSize+%7B%0A++++++++++++width%3A+841+mm%2C%0A++++++++++++height%3A+1189+mm%0A++++++++++%7D%0A++++++++else%0A++++++++++PaperSize+%7B%0A++++++++++++width%3A+floor_in%28mm%2C+PaperSize%3A%3Aa%28n+-+1%29.height+%2F+2%29%2C%0A++++++++++++height%3A+PaperSize%3A%3Aa%28n+-+1%29.width%2C%0A++++++++++%7D%0A%0A++++fn+area%28self%29+-%3E+Area+%3D%0A++++++++self.width+%2A+self.height%0A%0A++++fn+as_string%28self%29+-%3E+String+%3D%0A++++++++%22%7Bself.width%3A%3E4%7D+%C3%97+%7Bself.height%3A%3E5%7D+++%7Bself.area%28%29+-%3E+cm%C2%B2%3A%3E6.1f%7D%22%0A%7D%0A%0A%0Afn+row%28n%29+%3D+%22A%7Bn%3A%3C3%7D+++%7Bsize.as_string%28%29%7D%22%0A++where+size+%3D+PaperSize%3A%3Aa%28n%29%0A%0Aprint%28%22Name++++Width+++++Height++++++++Area++%22%29%0Aprint%28%22----+++-------+++--------+++----------%22%29%0Aprint%28join%28map%28row%2C+range%280%2C+10%29%29%2C+%22%5Cn%22%29%29%0A){ .md-button .md-button--primary }

```numbat
# Compute ISO 216 paper sizes for the A series
Expand All @@ -12,28 +12,30 @@
struct PaperSize {
width: Length,
height: Length,
}

fn paper_size_A(n: Scalar) -> PaperSize =
if n == 0
then
PaperSize {
width: 841 mm,
height: 1189 mm
}
else
PaperSize {
width: floor_in(mm, paper_size_A(n - 1).height / 2),
height: paper_size_A(n - 1).width,
}


fn paper_area(size: PaperSize) -> Area =
size.width * size.height
fn a(n: Scalar) -> Self =
if n == 0
then
PaperSize {
width: 841 mm,
height: 1189 mm
}
else
PaperSize {
width: floor_in(mm, PaperSize::a(n - 1).height / 2),
height: PaperSize::a(n - 1).width,
}

fn area(self) -> Area =
self.width * self.height

fn as_string(self) -> String =
"{self.width:>4} × {self.height:>5} {self.area() -> cm²:>6.1f}"
}


fn size_as_string(size: PaperSize) = "{size.width:>4} × {size.height:>5} {paper_area(size) -> cm²:>6.1f}"
fn row(n) = "A{n:<3} {size_as_string(paper_size_A(n))}"
fn row(n) = "A{n:<3} {size.as_string()}"
where size = PaperSize::a(n)

print("Name Width Height Area ")
print("---- ------- -------- ----------")
Expand Down
18 changes: 9 additions & 9 deletions examples/3d_printing.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ struct Material {
diameter: Length,
density: MassDensity,
price: Money / Mass,

fn print_cost(self, mass: Mass) -> Money = self.price × mass

fn filament_length(self, mass: Mass) -> Length = volume / cross_section -> meter
where r = self.diameter / 2
and cross_section: Area = π r²
and volume: Volume = mass / self.density
}

let PLA = Material {
Expand All @@ -10,18 +17,11 @@ let PLA = Material {
price: 16.99 €/kg,
}

fn print_cost(material: Material, mass: Mass) -> Money = material.price × mass

fn filament_length(material: Material, mass: Mass) -> Length = volume / cross_section -> meter
where r = material.diameter / 2
and cross_section: Area = π r²
and volume: Volume = mass / material.density


# Print parameters
let mass_model = 80 g
let material = PLA

print("Mass of model: {mass_model}")
print("Filament length: {filament_length(material, mass_model):.2}")
print("Cost of model: {print_cost(material, mass_model):.2}")
print("Filament length: {material.filament_length(mass_model):.2}")
print("Cost of model: {material.print_cost(mass_model):.2}")
8 changes: 4 additions & 4 deletions examples/interactive/tidal_chart.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ struct Constituent {
period: Time,
amplitude: Length,
phase: Angle,
}

fn height(c: Constituent, t: Time) -> Length =
c.amplitude cos(2π t / c.period + c.phase)
fn height(self, t: Time) -> Length =
self.amplitude cos(2π t / self.period + self.phase)
}

# The Gulf of Thailand has predominantly DIURNAL tides, meaning
# K1 and O1 dominate over the semidiurnal M2 and S2 components.
Expand Down Expand Up @@ -48,7 +48,7 @@ let O1 = Constituent {

let mean_sea_level = 0.5 m

fn tide_height(t: Time) -> Length = mean_sea_level + height(M2, t) + height(S2, t) + height(K1, t) + height(O1, t)
fn tide_height(t: Time) -> Length = mean_sea_level + M2.height(t) + S2.height(t) + K1.height(t) + O1.height(t)

let t_start = 0 days
let duration = 30 days
Expand Down
18 changes: 18 additions & 0 deletions examples/numbat_syntax.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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 @@ -137,7 +140,22 @@ 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,

# 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 }
fn scale(self, factor: Scalar) -> Self = Self { x: self.x * factor, y: self.y * factor }
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)

assert_eq(v.dot(w) -> m², 25 m²) # Method generic + unit conversion
40 changes: 21 additions & 19 deletions examples/paper_size.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@
struct PaperSize {
width: Length,
height: Length,
}

fn paper_size_A(n: Scalar) -> PaperSize =
if n == 0
then
PaperSize {
width: 841 mm,
height: 1189 mm
}
else
PaperSize {
width: floor_in(mm, paper_size_A(n - 1).height / 2),
height: paper_size_A(n - 1).width,
}
fn a(n: Scalar) -> Self =
if n == 0
then
PaperSize {
width: 841 mm,
height: 1189 mm
}
else
PaperSize {
width: floor_in(mm, PaperSize::a(n - 1).height / 2),
height: PaperSize::a(n - 1).width,
}

assert_eq(paper_size_A(4).width, 210 mm)
assert_eq(paper_size_A(4).height, 297 mm)
fn area(self) -> Area =
self.width * self.height

fn paper_area(size: PaperSize) -> Area =
size.width * size.height
fn as_string(self) -> String =
"{self.width:>4} × {self.height:>5} {self.area() -> cm²:>6.1f}"
}

assert_eq(PaperSize::a(4).width, 210 mm)
assert_eq(PaperSize::a(4).height, 297 mm)

fn size_as_string(size: PaperSize) = "{size.width:>4} × {size.height:>5} {paper_area(size) -> cm²:>6.1f}"
fn row(n) = "A{n:<3} {size_as_string(paper_size_A(n))}"
fn row(n) = "A{n:<3} {size.as_string()}"
where size = PaperSize::a(n)

print("Name Width Height Area ")
print("---- ------- -------- ----------")
Expand Down
27 changes: 27 additions & 0 deletions numbat/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ pub enum Expression<'a> {
expr: Box<Expression<'a>>,
field_name: &'a str,
},
MethodCall {
receiver: Box<Expression<'a>>,
method_name_span: Span,
method_name: &'a str,
args: Vec<Expression<'a>>,
full_span: Span,
},
List(Span, Vec<Expression<'a>>),
}

Expand Down Expand Up @@ -156,6 +163,7 @@ impl Expression<'_> {
Expression::String(span, _) => *span,
Expression::InstantiateStruct { full_span, .. } => *full_span,
Expression::AccessField { full_span, .. } => *full_span,
Expression::MethodCall { full_span, .. } => *full_span,
Expression::List(span, _) => *span,
Expression::TypedHole(span) => *span,
}
Expand Down Expand Up @@ -496,6 +504,7 @@ pub enum Statement<'a> {
struct_name: &'a str,
type_parameters: Vec<(Span, &'a str, Option<TypeParameterBound>)>,
fields: Vec<(Span, &'a str, TypeAnnotation)>,
methods: Vec<Statement<'a>>,
},
}

Expand Down Expand Up @@ -554,12 +563,16 @@ impl Statement<'_> {
Statement::DefineStruct {
struct_name_span,
fields,
methods,
..
} => {
let mut span = *struct_name_span;
if let Some((last_span, _, annotation)) = fields.last() {
span = span.extend(last_span).extend(&annotation.full_span());
}
if let Some(method) = methods.last() {
span = span.extend(&method.full_span());
}
span
}
}
Expand Down Expand Up @@ -716,6 +729,18 @@ impl ReplaceSpans for Expression<'_> {
expr: Box::new(expr.replace_spans()),
field_name,
},
Expression::MethodCall {
receiver,
method_name,
args,
..
} => Expression::MethodCall {
receiver: Box::new(receiver.replace_spans()),
method_name_span: Span::dummy(),
method_name,
args: args.iter().map(|a| a.replace_spans()).collect(),
full_span: Span::dummy(),
},
Expression::List(_, elements) => Expression::List(
Span::dummy(),
elements.iter().map(|e| e.replace_spans()).collect(),
Expand Down Expand Up @@ -819,6 +844,7 @@ impl ReplaceSpans for Statement<'_> {
struct_name,
type_parameters,
fields,
methods,
..
} => Statement::DefineStruct {
struct_name_span: Span::dummy(),
Expand All @@ -831,6 +857,7 @@ impl ReplaceSpans for Statement<'_> {
.iter()
.map(|(_span, name, type_)| (Span::dummy(), *name, type_.replace_spans()))
.collect(),
methods: methods.iter().map(|m| m.replace_spans()).collect(),
},
}
}
Expand Down
Loading