diff --git a/assets/numbat.sublime-syntax b/assets/numbat.sublime-syntax index 22d49cb8f..a8fa5efdf 100644 --- a/assets/numbat.sublime-syntax +++ b/assets/numbat.sublime-syntax @@ -7,7 +7,7 @@ file_extensions: scope: source.nbt contexts: main: - - match: \b(per|to|let|fn|where|and|dimension|unit|use|struct|long|short|both|none|if|then|else|true|false|print|assert|assert_eq|type)\b + - match: \b(per|to|let|fn|where|and|dimension|unit|use|struct|impl|self|long|short|both|none|if|then|else|true|false|print|assert|assert_eq|type)\b scope: keyword.control.nbt - match: '#(.*)' scope: comment.line.nbt diff --git a/assets/numbat.vim b/assets/numbat.vim index c0fe496e1..dba13bc3f 100644 --- a/assets/numbat.vim +++ b/assets/numbat.vim @@ -5,7 +5,7 @@ if exists("b:current_syntax") endif " Numbat Keywords -syn keyword numbatKeywords per to let fn where and dimension unit use struct long short both none if then else true false NaN inf print assert assert_eq type +syn keyword numbatKeywords per to let fn where and dimension unit use struct impl self long short both none if then else true false NaN inf print assert assert_eq type highlight default link numbatKeywords Keyword " Physical dimensions (every capitalized word) diff --git a/book/src/basics/structs.md b/book/src/basics/structs.md index 4e2e6446e..2885a28be 100644 --- a/book/src/basics/structs.md +++ b/book/src/basics/structs.md @@ -33,6 +33,19 @@ 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}.") ``` +## Methods + +You can define methods on structs using `impl` blocks. Methods take `self` as their first parameter and are called using dot notation: + +```nbt +impl Element { + fn cube_side_length(self, mass: Mass) -> Length = + cbrt(mass / self.density) +} + +tungsten.cube_side_length(1 kg) -> cm # 3.72 cm +``` + ## Generic structs Structs can be generic over type parameters. Type parameters are declared in angle brackets after the struct name: @@ -49,11 +62,29 @@ let t = Tuple { first: "hello", second: 42 } If you want to constrain a type parameter to be a dimension type, use the `Dim` bound: ```nbt -struct Vec { - x: X, - y: X, +struct Vec { + x: D, + y: D, } let position = Vec { x: 1 m, y: 2 m } let velocity: Vec = Vec { x: 1 m/s, y: 2 m/s } ``` + +For generic structs, the type parameters are declared on the `impl` block. Methods can also be generic over additional type parameters: + +```nbt +impl Vec { + fn norm(self) -> D² = self.x² + self.y² + + fn multiply(self, factor: S) -> Vec = + Vec { + x: factor × self.x, + y: factor × self.y, + } +} + +let p = Vec { x: 3 m, y: 4 m } +p.norm() # 25 m² +p.multiply(10 N) # Vec { x: 30 N·m, y: 40 N·m } +``` diff --git a/book/src/examples/example-numbat_syntax.md b/book/src/examples/example-numbat_syntax.md index bf3f8daf6..e0ef7d8d9 100644 --- a/book/src/examples/example-numbat_syntax.md +++ b/book/src/examples/example-numbat_syntax.md @@ -150,4 +150,17 @@ struct Vec2 { # A generic struct with type parameter x: D, y: D, } + +impl Vec2 { # Define methods for a struct + fn norm(self) -> D² = self.x² + self.y² + + fn scale(self, scalar: S) -> Vec2 = + Vec2 { + x: scalar × self.x, + y: scalar × self.y, + } +} + +let v = Vec2 { x: 3 meter, y: 4 meter } +v.norm() # Call a method ``` diff --git a/book/src/examples/example-paper_size.md b/book/src/examples/example-paper_size.md index 5495dc614..8e88a3056 100644 --- a/book/src/examples/example-paper_size.md +++ b/book/src/examples/example-paper_size.md @@ -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%7D%0A%0Aimpl+PaperSize+%7B%0A++++fn+area%28self%29+-%3E+Area+%3D+self.width+%C3%97+self.height%0A%0A++++fn+to_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%0Afn+paper_size_A%28n%3A+Scalar%29+-%3E+PaperSize+%3D%0A++++if+n+%3D%3D+0%0A++++++++then+PaperSize+%7B+width%3A+841+mm%2C+height%3A+1189+mm+%7D%0A++++++++else+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+row%28n%29+%3D+%22A%7Bn%3A%3C3%7D+++%7Bpaper_size_A%28n%29.to_string%28%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 } ```numbat # Compute ISO 216 paper sizes for the A series @@ -14,26 +14,23 @@ struct PaperSize { 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, - } +impl PaperSize { + fn area(self) -> Area = self.width × self.height + fn to_string(self) -> String = + "{self.width:>4} × {self.height:>5} {self.area() -> cm²:>6.1f}" +} -fn paper_area(size: PaperSize) -> Area = - size.width * size.height +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 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} {paper_size_A(n).to_string()}" print("Name Width Height Area ") print("---- ------- -------- ----------") diff --git a/book/src/prelude/functions/other.md b/book/src/prelude/functions/other.md index 851c76241..8afb0948d 100644 --- a/book/src/prelude/functions/other.md +++ b/book/src/prelude/functions/other.md @@ -528,51 +528,3 @@ fn color(rgb_hex: Scalar) -> Color ``` [:material-play-circle: Run this example](https://numbat.dev/?q=use%20extra%3A%3Acolor%0Acolor%280xff7700%29){ .md-button } -### `color_rgb` -Convert a color to its RGB representation. - -```nbt -fn color_rgb(color: Color) -> String -``` - -!!! example "Example" - ```nbt - use extra::color - cyan -> color_rgb - - = "rgb(0, 255, 255)" [String] - ``` - [:material-play-circle: Run this example](https://numbat.dev/?q=use%20extra%3A%3Acolor%0Acyan%20%2D%3E%20color%5Frgb){ .md-button } - -### `color_rgb_float` -Convert a color to its RGB floating point representation. - -```nbt -fn color_rgb_float(color: Color) -> String -``` - -!!! example "Example" - ```nbt - use extra::color - cyan -> color_rgb_float - - = "rgb(0.000, 1.000, 1.000)" [String] - ``` - [:material-play-circle: Run this example](https://numbat.dev/?q=use%20extra%3A%3Acolor%0Acyan%20%2D%3E%20color%5Frgb%5Ffloat){ .md-button } - -### `color_hex` -Convert a color to its hexadecimal representation. - -```nbt -fn color_hex(color: Color) -> String -``` - -!!! example "Example" - ```nbt - use extra::color - rgb(225, 36, 143) -> color_hex - - = "#e1248f" [String] - ``` - [:material-play-circle: Run this example](https://numbat.dev/?q=use%20extra%3A%3Acolor%0Argb%28225%2C%2036%2C%20143%29%20%2D%3E%20color%5Fhex){ .md-button } - diff --git a/examples/3d_printing.nbt b/examples/3d_printing.nbt index 86ec661b2..629004006 100644 --- a/examples/3d_printing.nbt +++ b/examples/3d_printing.nbt @@ -4,24 +4,25 @@ struct Material { price: Money / Mass, } +impl Material { + fn 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 { diameter: 1.75 mm, density: 1.27 g/cm^3, 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.cost(mass_model):.2}") diff --git a/examples/interactive/tidal_chart.nbt b/examples/interactive/tidal_chart.nbt index 9c69fc087..2014abfa6 100644 --- a/examples/interactive/tidal_chart.nbt +++ b/examples/interactive/tidal_chart.nbt @@ -11,8 +11,10 @@ struct Constituent { phase: Angle, } -fn height(c: Constituent, t: Time) -> Length = - c.amplitude cos(2π t / c.period + c.phase) +impl Constituent { + 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. @@ -48,7 +50,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 diff --git a/examples/numbat_syntax.nbt b/examples/numbat_syntax.nbt index 7dbd9a4fc..507638a03 100644 --- a/examples/numbat_syntax.nbt +++ b/examples/numbat_syntax.nbt @@ -141,3 +141,16 @@ struct Vec2 { # A generic struct with type parameter x: D, y: D, } + +impl Vec2 { # Define methods for a struct + fn norm(self) -> D² = self.x² + self.y² + + fn scale(self, scalar: S) -> Vec2 = + Vec2 { + x: scalar × self.x, + y: scalar × self.y, + } +} + +let v = Vec2 { x: 3 meter, y: 4 meter } +v.norm() # Call a method diff --git a/examples/paper_size.nbt b/examples/paper_size.nbt index 21d84411a..60b9fa06d 100644 --- a/examples/paper_size.nbt +++ b/examples/paper_size.nbt @@ -7,28 +7,26 @@ struct PaperSize { height: Length, } +impl PaperSize { + fn area(self) -> Area = self.width × self.height + + fn to_string(self) -> String = + "{self.width:>4} × {self.height:>5} {self.area() -> cm²:>6.1f}" +} + 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, - } + 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, + } assert_eq(paper_size_A(4).width, 210 mm) assert_eq(paper_size_A(4).height, 297 mm) +assert_eq(paper_size_A(4).area(), 62370 mm²) -fn paper_area(size: PaperSize) -> Area = - size.width * size.height - - -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} {paper_size_A(n).to_string()}" print("Name Width Height Area ") print("---- ------- -------- ----------") diff --git a/examples/tests/color.nbt b/examples/tests/color.nbt index 78536607c..ae3c90a18 100644 --- a/examples/tests/color.nbt +++ b/examples/tests/color.nbt @@ -1,31 +1,31 @@ use extra::color -assert_eq(0x000000 -> color, black) -assert_eq(0xffffff -> color, white) -assert_eq(0x123456 -> color, Color { red: 0x12, green: 0x34, blue: 0x56 }) +assert_eq(color(0x000000), black) +assert_eq(color(0xffffff), white) +assert_eq(color(0x123456), Color { red: 0x12, green: 0x34, blue: 0x56 }) -assert_eq(black -> color_rgb, "rgb(0, 0, 0)") -assert_eq(white -> color_rgb, "rgb(255, 255, 255)") -assert_eq(red -> color_rgb, "rgb(255, 0, 0)") -assert_eq(green -> color_rgb, "rgb(0, 255, 0)") -assert_eq(blue -> color_rgb, "rgb(0, 0, 255)") -assert_eq(0x123456 -> color -> color_rgb, "rgb(18, 52, 86)") +assert_eq(black.to_rgb(), "rgb(0, 0, 0)") +assert_eq(white.to_rgb(), "rgb(255, 255, 255)") +assert_eq(red.to_rgb(), "rgb(255, 0, 0)") +assert_eq(green.to_rgb(), "rgb(0, 255, 0)") +assert_eq(blue.to_rgb(), "rgb(0, 0, 255)") +assert_eq(color(0x123456).to_rgb(), "rgb(18, 52, 86)") -assert_eq(black -> color_rgb_float, "rgb(0.000, 0.000, 0.000)") -assert_eq(white -> color_rgb_float, "rgb(1.000, 1.000, 1.000)") -assert_eq(red -> color_rgb_float, "rgb(1.000, 0.000, 0.000)") -assert_eq(green -> color_rgb_float, "rgb(0.000, 1.000, 0.000)") -assert_eq(blue -> color_rgb_float, "rgb(0.000, 0.000, 1.000)") -assert_eq(0x123456 -> color -> color_rgb_float, "rgb(0.071, 0.204, 0.337)") +assert_eq(black.to_rgb_float(), "rgb(0.000, 0.000, 0.000)") +assert_eq(white.to_rgb_float(), "rgb(1.000, 1.000, 1.000)") +assert_eq(red.to_rgb_float(), "rgb(1.000, 0.000, 0.000)") +assert_eq(green.to_rgb_float(), "rgb(0.000, 1.000, 0.000)") +assert_eq(blue.to_rgb_float(), "rgb(0.000, 0.000, 1.000)") +assert_eq(color(0x123456).to_rgb_float(), "rgb(0.071, 0.204, 0.337)") -assert_eq(black -> color_hex, "#000000") -assert_eq(white -> color_hex, "#ffffff") -assert_eq(red -> color_hex, "#ff0000") -assert_eq(green -> color_hex, "#00ff00") -assert_eq(blue -> color_hex, "#0000ff") -assert_eq(0x123456 -> color -> color_hex, "#123456") +assert_eq(black.to_hex(), "#000000") +assert_eq(white.to_hex(), "#ffffff") +assert_eq(red.to_hex(), "#ff0000") +assert_eq(green.to_hex(), "#00ff00") +assert_eq(blue.to_hex(), "#0000ff") +assert_eq(color(0x123456).to_hex(), "#123456") # Examples: -assert_eq(rgb(225, 36, 143) -> color_hex, "#e1248f") -assert_eq(0xe1248f -> color -> color_rgb, "rgb(225, 36, 143)") +assert_eq(rgb(225, 36, 143).to_hex(), "#e1248f") +assert_eq(color(0xe1248f).to_rgb(), "rgb(225, 36, 143)") diff --git a/examples/tests/vector3.nbt b/examples/tests/vector3.nbt index 4806a99d1..fc5590cc6 100644 --- a/examples/tests/vector3.nbt +++ b/examples/tests/vector3.nbt @@ -5,15 +5,25 @@ let r = vec(3 m, 4 m, 0 m) let v = vec(-10 m / s, 0, 0) let mass = 5 kg -let p = multiply(mass, v) +let p = v.multiply(mass) -let l_z = cross(r, p).z +let l_z = r.cross(p).z -# Check values -assert_eq(norm(r), 25 m²) -assert_eq(length(r), 5 m) +# Check values using methods +assert_eq(r.norm(), 25 m²) +assert_eq(r.len(), 5 m) assert_eq(l_z, 200 kg m² / s) +# Test method chaining +let v1 = vec(1 m, 2 m, 3 m) +let v2 = vec(4 m, 5 m, 6 m) +assert_eq(v1.add(v2).x, 5 m) +assert_eq(v1.sub(v2).x, -3 m) +assert_eq(v1.neg().x, -1 m) + +# Test dot product +assert_eq(v1.dot(v2), 32 m²) + # Check types let _r: Vec = r let _v: Vec = v diff --git a/numbat/modules/extra/color.nbt b/numbat/modules/extra/color.nbt index ab39db3e5..ed2bd71f4 100644 --- a/numbat/modules/extra/color.nbt +++ b/numbat/modules/extra/color.nbt @@ -8,39 +8,41 @@ struct Color { blue: Scalar, } +impl Color { + fn _to_scalar(self) -> Scalar = + self.red * 0x010000 + self.green * 0x000100 + self.blue + + @description("Convert this color to its RGB representation.") + @example("cyan.to_rgb()") + fn to_rgb(self) -> String = + "rgb({self.red}, {self.green}, {self.blue})" + + @description("Convert this color to its RGB floating point representation.") + @example("cyan.to_rgb_float()") + fn to_rgb_float(self) -> String = + "rgb({self.red / 255:.3}, {self.green / 255:.3}, {self.blue / 255:.3})" + + @description("Convert this color to its hexadecimal representation.") + @example("rgb(225, 36, 143).to_hex()") + fn to_hex(self) -> String = + "{self._to_scalar() -> hex:>8}" |> + str_replace("0x", "") |> + str_replace(" ", "0") |> + str_append("#") +} + @description("Create a `Color` from RGB (red, green, blue) values in the range $[0, 256)$.") @example("rgb(125, 128, 218)") fn rgb(red: Scalar, green: Scalar, blue: Scalar) -> Color = - Color { red: red, green: green, blue: blue } + Color { red: red, green: green, blue: blue } @description("Create a `Color` from a (hexadecimal) value.") @example("color(0xff7700)") fn color(rgb_hex: Scalar) -> Color = - rgb( - floor(rgb_hex / 256^2), - floor((mod(rgb_hex, 256^2)) / 256), - mod(rgb_hex, 256)) - -fn _color_to_scalar(color: Color) -> Scalar = - color.red * 0x010000 + color.green * 0x000100 + color.blue - -@description("Convert a color to its RGB representation.") -@example("cyan -> color_rgb") -fn color_rgb(color: Color) -> String = - "rgb({color.red}, {color.green}, {color.blue})" - -@description("Convert a color to its RGB floating point representation.") -@example("cyan -> color_rgb_float") -fn color_rgb_float(color: Color) -> String = - "rgb({color.red / 255:.3}, {color.green / 255:.3}, {color.blue / 255:.3})" - -@description("Convert a color to its hexadecimal representation.") -@example("rgb(225, 36, 143) -> color_hex") -fn color_hex(color: Color) -> String = - "{color -> _color_to_scalar -> hex:>8}" |> - str_replace("0x", "") |> - str_replace(" ", "0") |> - str_append("#") + rgb( + floor(rgb_hex / 256^2), + floor((mod(rgb_hex, 256^2)) / 256), + mod(rgb_hex, 256)) let black: Color = rgb(0, 0, 0) let white: Color = rgb(255, 255, 255) diff --git a/numbat/modules/extra/vector3.nbt b/numbat/modules/extra/vector3.nbt index a053eeb03..5ea0a6da9 100644 --- a/numbat/modules/extra/vector3.nbt +++ b/numbat/modules/extra/vector3.nbt @@ -6,32 +6,43 @@ struct Vec { z: D, } +impl Vec { + @description("Compute the norm (squared length) of this vector.") + fn norm(self) -> D^2 = self.x^2 + self.y^2 + self.z^2 + + @description("Compute the length of this vector.") + fn len(self) -> D = sqrt(self.norm()) + + @description("Add another vector to this one.") + fn add(self, other: Vec) -> Vec = + Vec { x: self.x + other.x, y: self.y + other.y, z: self.z + other.z } + + @description("Subtract another vector from this one.") + fn sub(self, other: Vec) -> Vec = + Vec { x: self.x - other.x, y: self.y - other.y, z: self.z - other.z } + + @description("Negate this vector.") + fn neg(self) -> Vec = + Vec { x: -self.x, y: -self.y, z: -self.z } + + @description("Compute the cross product with another vector.") + fn cross(self, other: Vec) -> Vec = + Vec { + x: self.y * other.z - self.z * other.y, + y: self.z * other.x - self.x * other.z, + z: self.x * other.y - self.y * other.x, + } + + @description("Compute the dot product with another vector.") + fn dot(self, other: Vec) -> D * D2 = + self.x * other.x + self.y * other.y + self.z * other.z + + @description("Multiply this vector by a scalar.") + fn multiply(self, scalar: A) -> Vec = + Vec { x: self.x * scalar, y: self.y * scalar, z: self.z * scalar } +} + @description("Create a 3D vector from its components.") fn vec(x: D, y: D, z: D) -> Vec = Vec { x: x, y: y, z: z } -@description("Add two 3D vectors.") -fn add(v1: Vec, v2: Vec) -> Vec = - vec(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z) - -@description("Multiply a 3D vector by a scalar.") -fn multiply(alpha: A, v: Vec) -> Vec = - vec(alpha * v.x, alpha * v.y, alpha * v.z) - -@description("Compute the dot product of two 3D vectors.") -fn dot_product(v1: Vec, v2: Vec) -> A * B = - v1.x * v2.x + v1.y * v2.y + v1.z * v2.z - -@description("Compute the norm (squared length) of a 3D vector.") -fn norm(v: Vec) -> D^2 = dot_product(v, v) - -@description("Compute the length of a 3D vector.") -fn length(v: Vec) -> D = sqrt(norm(v)) - -@description("Compute the cross product of two 3D vectors.") -fn cross(v1: Vec, v2: Vec) -> Vec = - vec( - v1.y * v2.z - v1.z * v2.y, - v1.z * v2.x - v1.x * v2.z, - v1.x * v2.y - v1.y * v2.x, - ) diff --git a/numbat/src/ast.rs b/numbat/src/ast.rs index 88c5b984c..2b8394915 100644 --- a/numbat/src/ast.rs +++ b/numbat/src/ast.rs @@ -107,6 +107,13 @@ pub enum Expression<'a> { fields: Vec<(Span, &'a str, Expression<'a>)>, }, AccessField(Span, Span, Box>, &'a str), + MethodCall { + full_span: Span, + method_span: Span, + receiver: Box>, + method_name: &'a str, + args: Vec>, + }, List(Span, Vec>), } @@ -141,6 +148,7 @@ impl Expression<'_> { Expression::String(span, _) => *span, Expression::InstantiateStruct { full_span, .. } => *full_span, Expression::AccessField(full_span, _ident_span, _, _) => *full_span, + Expression::MethodCall { full_span, .. } => *full_span, Expression::List(span, _) => *span, Expression::TypedHole(span) => *span, } @@ -435,6 +443,23 @@ pub struct DefineVariable<'a> { pub decorators: Vec>, } +#[derive(Debug, Clone, PartialEq)] +pub struct MethodDefinition<'a> { + pub function_name_span: Span, + pub function_name: &'a str, + pub type_parameters: Vec<(Span, &'a str, Option)>, + pub self_span: Span, + /// Parameters excluding self + pub parameters: Vec<(Span, &'a str, Option)>, + /// Method body. If it is absent, the method is implemented via FFI + pub body: Option>, + /// Local variables + pub local_variables: Vec>, + /// Optional annotated return type + pub return_type_annotation: Option, + pub decorators: Vec>, +} + #[derive(Debug, Clone, PartialEq)] pub enum Statement<'a> { Expression(Expression<'a>), @@ -471,6 +496,17 @@ pub enum Statement<'a> { type_parameters: Vec<(Span, &'a str, Option)>, fields: Vec<(Span, &'a str, TypeAnnotation)>, }, + DefineImpl { + impl_span: Span, + /// Type parameters for the impl block (e.g., in `impl Container`) + type_parameters: Vec<(Span, &'a str, Option)>, + struct_name_span: Span, + struct_name: &'a str, + /// Type arguments applied to the struct (e.g., T in `Container`) + struct_type_args: Vec, + /// Methods defined in this impl block + methods: Vec>, + }, } #[cfg(test)] @@ -605,6 +641,18 @@ impl ReplaceSpans for Expression<'_> { Box::new(expr.replace_spans()), attr, ), + Expression::MethodCall { + receiver, + method_name, + args, + .. + } => Expression::MethodCall { + full_span: Span::dummy(), + method_span: Span::dummy(), + receiver: Box::new(receiver.replace_spans()), + method_name, + args: args.iter().map(|a| a.replace_spans()).collect(), + }, Expression::List(_, elements) => Expression::List( Span::dummy(), elements.iter().map(|e| e.replace_spans()).collect(), @@ -627,6 +675,44 @@ impl ReplaceSpans for DefineVariable<'_> { } } +#[cfg(test)] +impl ReplaceSpans for MethodDefinition<'_> { + fn replace_spans(&self) -> Self { + Self { + function_name_span: Span::dummy(), + function_name: self.function_name, + type_parameters: self + .type_parameters + .iter() + .map(|(_, name, bound)| (Span::dummy(), *name, bound.clone())) + .collect(), + self_span: Span::dummy(), + parameters: self + .parameters + .iter() + .map(|(_, name, type_)| { + ( + Span::dummy(), + *name, + type_.as_ref().map(|t| t.replace_spans()), + ) + }) + .collect(), + body: self.body.clone().map(|b| b.replace_spans()), + local_variables: self + .local_variables + .iter() + .map(DefineVariable::replace_spans) + .collect(), + return_type_annotation: self + .return_type_annotation + .as_ref() + .map(|t| t.replace_spans()), + decorators: self.decorators.clone(), + } + } +} + #[cfg(test)] impl ReplaceSpans for Statement<'_> { fn replace_spans(&self) -> Self { @@ -720,6 +806,23 @@ impl ReplaceSpans for Statement<'_> { .map(|(_span, name, type_)| (Span::dummy(), *name, type_.replace_spans())) .collect(), }, + Statement::DefineImpl { + type_parameters, + struct_name, + struct_type_args, + methods, + .. + } => Statement::DefineImpl { + impl_span: Span::dummy(), + type_parameters: type_parameters + .iter() + .map(|(_, name, bound)| (Span::dummy(), *name, bound.clone())) + .collect(), + struct_name_span: Span::dummy(), + struct_name, + struct_type_args: struct_type_args.iter().map(|t| t.replace_spans()).collect(), + methods: methods.iter().map(|m| m.replace_spans()).collect(), + }, } } } diff --git a/numbat/src/bytecode_interpreter.rs b/numbat/src/bytecode_interpreter.rs index 548b19f0d..f9c2cfa06 100644 --- a/numbat/src/bytecode_interpreter.rs +++ b/numbat/src/bytecode_interpreter.rs @@ -323,6 +323,22 @@ impl BytecodeInterpreter { Expression::TypedHole(_, _) => { unreachable!("Typed holes cause type inference errors") } + Expression::MethodCall(_, full_span, receiver, method_name, args, struct_type, _) => { + self.compile_expression(receiver); + for arg in args { + self.compile_expression(arg); + } + let struct_info = match struct_type.to_concrete_type() { + Type::Struct(info) => info, + _ => unreachable!( + "Method call on non-struct type should be prevented by type checker" + ), + }; + let mangled = format!("{}::{}", struct_info.name, method_name); + let idx = self.vm.get_function_idx(&mangled); + self.vm + .add_op2(Op::Call, idx, (args.len() + 1) as u16, *full_span); + } }; } @@ -556,6 +572,42 @@ impl BytecodeInterpreter { Statement::DefineStruct(struct_info) => { self.vm.add_struct_info(struct_info); } + Statement::DefineImpl { + struct_info, + methods, + .. + } => { + self.vm.add_struct_info(struct_info); + + for compiled_method in methods { + if let Some(body) = &compiled_method.body { + self.vm.begin_function(&compiled_method.mangled_name); + + self.locals.push(vec![]); + + let current_depth = self.current_depth(); + for param_name in &compiled_method.parameters { + self.locals[current_depth].push(Local { + identifiers: [param_name.clone()].into(), + metadata: LocalMetadata::default(), + }); + } + + for local_var in &compiled_method.local_variables { + self.compile_define_variable(local_var); + } + + self.compile_expression(body); + self.vm.add_op(Op::Return, body.full_span()); + + self.locals.pop(); + self.vm.end_function(); + + self.functions + .insert(compiled_method.mangled_name.clone(), false); + } + } + } } Ok(()) diff --git a/numbat/src/diagnostic.rs b/numbat/src/diagnostic.rs index ab4a94630..ab4fb9445 100644 --- a/numbat/src/diagnostic.rs +++ b/numbat/src/diagnostic.rs @@ -509,6 +509,47 @@ impl ErrorDiagnostic for TypeCheckError { span.diagnostic_label(LabelStyle::Primary) .with_message(inner_error), ]), + TypeCheckError::MethodCallOnNonStructType(method_span, expr_span, _method, type_) => d + .with_labels(vec![ + method_span + .diagnostic_label(LabelStyle::Primary) + .with_message(inner_error), + expr_span + .diagnostic_label(LabelStyle::Secondary) + .with_message(type_.to_string()), + ]), + TypeCheckError::UnknownMethod(method_span, expr_span, _method, struct_name) => d + .with_labels(vec![ + method_span + .diagnostic_label(LabelStyle::Primary) + .with_message(inner_error), + expr_span + .diagnostic_label(LabelStyle::Secondary) + .with_message(format!("type is '{struct_name}'")), + ]), + TypeCheckError::ImplForUnknownStruct(span, _) => d.with_labels(vec![ + span.diagnostic_label(LabelStyle::Primary) + .with_message(inner_error), + ]), + TypeCheckError::ImplTypeParameterMismatch { + impl_span, + struct_span, + .. + } => d.with_labels(vec![ + impl_span + .diagnostic_label(LabelStyle::Primary) + .with_message(inner_error), + struct_span + .diagnostic_label(LabelStyle::Secondary) + .with_message("struct defined here"), + ]), + TypeCheckError::DuplicateMethodInImpl(span, _, first_span) => d.with_labels(vec![ + span.diagnostic_label(LabelStyle::Primary) + .with_message(inner_error), + first_span + .diagnostic_label(LabelStyle::Secondary) + .with_message("first definition here"), + ]), }; vec![d] } diff --git a/numbat/src/ffi/lookup.rs b/numbat/src/ffi/lookup.rs index e21b953d7..337220835 100644 --- a/numbat/src/ffi/lookup.rs +++ b/numbat/src/ffi/lookup.rs @@ -89,6 +89,7 @@ pub fn _get_chemical_element_data_raw( kind: StructKind::Instance(vec![]), definition_span: unknown_span, fields, + methods: IndexMap::new(), }; Ok(Value::StructInstance( Arc::new(info), diff --git a/numbat/src/keywords.rs b/numbat/src/keywords.rs index 62c69a572..e0891e942 100644 --- a/numbat/src/keywords.rs +++ b/numbat/src/keywords.rs @@ -11,7 +11,9 @@ pub const KEYWORDS: &[&str] = &[ "unit ", "use ", "struct ", + "impl ", // 'inline' keywords + "self", "long", "short", "both", diff --git a/numbat/src/parser.rs b/numbat/src/parser.rs index de5d3180d..d1e8655da 100644 --- a/numbat/src/parser.rs +++ b/numbat/src/parser.rs @@ -68,8 +68,8 @@ use std::num::NonZeroUsize; use crate::arithmetic::{Exponent, Rational}; use crate::ast::{ - BinaryOperator, DefineVariable, Expression, ProcedureKind, Statement, StringPart, - TypeAnnotation, TypeExpression, TypeParameterBound, UnaryOperator, + BinaryOperator, DefineVariable, Expression, MethodDefinition, ProcedureKind, Statement, + StringPart, TypeAnnotation, TypeExpression, TypeParameterBound, UnaryOperator, }; use crate::decorator::{self, Decorator}; use crate::number::Number; @@ -252,6 +252,18 @@ pub enum ParseErrorKind { #[error("Invalid command: {0}")] InvalidCommand(String), + + #[error("Expected '{{' after impl declaration")] + ExpectedLeftCurlyAfterImpl, + + #[error("Expected 'fn' keyword in impl block")] + ExpectedFnInImplBlock, + + #[error("Expected 'self' as first parameter in method")] + ExpectedSelfAsFirstParameter, + + #[error("Expected ')' or ',' after 'self' in method")] + ExpectedRightParenOrCommaAfterSelf, } #[derive(Debug, Clone, Error)] @@ -506,6 +518,8 @@ impl<'a> Parser<'a> { self.parse_use(tokens) } else if self.match_exact(tokens, TokenKind::Struct).is_some() { self.parse_struct(tokens) + } else if self.match_exact(tokens, TokenKind::Impl).is_some() { + self.parse_impl(tokens) } else if self.match_any(tokens, PROCEDURES).is_some() { self.parse_procedure(tokens) } else { @@ -736,6 +750,117 @@ impl<'a> Parser<'a> { } } + /// Parse a single decorator (after the @ has been consumed) + fn parse_single_decorator(&mut self, tokens: &[Token<'a>]) -> Result> { + if let Some(decorator) = self.match_exact(tokens, TokenKind::Identifier) { + match decorator.lexeme { + "metric_prefixes" => Ok(Decorator::MetricPrefixes), + "binary_prefixes" => Ok(Decorator::BinaryPrefixes), + "aliases" => { + if self.match_exact(tokens, TokenKind::LeftParen).is_some() { + let aliases = self.list_of_aliases(tokens)?; + Ok(Decorator::Aliases(aliases)) + } else { + Err(ParseError { + kind: ParseErrorKind::ExpectedLeftParenAfterDecorator, + span: self.peek(tokens).span, + }) + } + } + "url" | "name" | "description" => { + if self.match_exact(tokens, TokenKind::LeftParen).is_some() { + if let Some(token) = self.match_exact(tokens, TokenKind::StringFixed) { + if self.match_exact(tokens, TokenKind::RightParen).is_none() { + return Err(ParseError::new( + ParseErrorKind::MissingClosingParen, + self.peek(tokens).span, + )); + } + + let content = strip_and_escape(token.lexeme); + + match decorator.lexeme { + "url" => Ok(Decorator::Url(content)), + "name" => Ok(Decorator::Name(content)), + "description" => Ok(Decorator::Description(content)), + _ => unreachable!(), + } + } else { + Err(ParseError { + kind: ParseErrorKind::ExpectedString, + span: self.peek(tokens).span, + }) + } + } else { + Err(ParseError { + kind: ParseErrorKind::ExpectedLeftParenAfterDecorator, + span: self.peek(tokens).span, + }) + } + } + "example" => { + if self.match_exact(tokens, TokenKind::LeftParen).is_some() { + if let Some(token_code) = self.match_exact(tokens, TokenKind::StringFixed) { + if self.match_exact(tokens, TokenKind::Comma).is_some() { + if let Some(token_description) = + self.match_exact(tokens, TokenKind::StringFixed) + { + if self.match_exact(tokens, TokenKind::RightParen).is_none() { + return Err(ParseError::new( + ParseErrorKind::MissingClosingParen, + self.peek(tokens).span, + )); + } + + Ok(Decorator::Example( + strip_and_escape(token_code.lexeme), + Some(strip_and_escape(token_description.lexeme)), + )) + } else { + Err(ParseError { + kind: ParseErrorKind::ExpectedString, + span: self.peek(tokens).span, + }) + } + } else { + if self.match_exact(tokens, TokenKind::RightParen).is_none() { + return Err(ParseError::new( + ParseErrorKind::MissingClosingParen, + self.peek(tokens).span, + )); + } + + Ok(Decorator::Example( + strip_and_escape(token_code.lexeme), + None, + )) + } + } else { + Err(ParseError { + kind: ParseErrorKind::ExpectedString, + span: self.peek(tokens).span, + }) + } + } else { + Err(ParseError { + kind: ParseErrorKind::ExpectedLeftParenAfterDecorator, + span: self.peek(tokens).span, + }) + } + } + _ => Err(ParseError { + kind: ParseErrorKind::UnknownDecorator, + span: decorator.span, + }), + } + } else { + Err(ParseError { + kind: ParseErrorKind::ExpectedDecoratorName, + span: self.peek(tokens).span, + }) + } + } + fn parse_decorators(&mut self, tokens: &[Token<'a>]) -> Result> { if let Some(decorator) = self.match_exact(tokens, TokenKind::Identifier) { let decorator = match decorator.lexeme { @@ -1006,6 +1131,218 @@ impl<'a> Parser<'a> { }) } + fn parse_impl(&mut self, tokens: &[Token<'a>]) -> Result> { + let impl_span = self.last(tokens).unwrap().span; + let type_parameters = self.type_parameters(tokens)?; + let name = self.identifier(tokens)?; + let struct_name_span = self.last(tokens).unwrap().span; + + let struct_type_args = if self.match_exact(tokens, TokenKind::LessThan).is_some() { + let mut args = vec![self.type_annotation(tokens)?]; + while self.match_exact(tokens, TokenKind::Comma).is_some() { + args.push(self.type_annotation(tokens)?); + } + if !self.match_closing_angle_bracket(tokens) { + return Err(ParseError::new( + ParseErrorKind::ExpectedCommaOrRightAngleBracket, + self.peek(tokens).span, + )); + } + args + } else { + vec![] + }; + + if self.match_exact(tokens, TokenKind::LeftCurly).is_none() { + return Err(ParseError::new( + ParseErrorKind::ExpectedLeftCurlyAfterImpl, + self.peek(tokens).span, + )); + } + + self.skip_empty_lines(tokens); + + let mut methods = vec![]; + while self.match_exact(tokens, TokenKind::RightCurly).is_none() { + self.skip_empty_lines(tokens); + + let mut method_decorators = vec![]; + while self.match_exact(tokens, TokenKind::At).is_some() { + let decorator = self.parse_single_decorator(tokens)?; + method_decorators.push(decorator); + self.skip_empty_lines(tokens); + } + + if self.match_exact(tokens, TokenKind::Fn).is_some() { + methods.push( + self.parse_method_declaration_with_decorators(tokens, method_decorators)?, + ); + } else if self.peek(tokens).kind == TokenKind::RightCurly { + break; + } else { + return Err(ParseError::new( + ParseErrorKind::ExpectedFnInImplBlock, + self.peek(tokens).span, + )); + } + self.skip_empty_lines(tokens); + } + + Ok(Statement::DefineImpl { + impl_span, + type_parameters, + struct_name_span, + struct_name: name, + struct_type_args, + methods, + }) + } + + fn parse_method_declaration_with_decorators( + &mut self, + tokens: &[Token<'a>], + decorators: Vec>, + ) -> Result> { + let fn_name = self + .match_exact(tokens, TokenKind::Identifier) + .ok_or_else(|| { + ParseError::new( + ParseErrorKind::ExpectedIdentifierAfterFn, + self.peek(tokens).span, + ) + })?; + let function_name_span = self.last(tokens).unwrap().span; + let type_parameters = self.type_parameters(tokens)?; + + if self.match_exact(tokens, TokenKind::LeftParen).is_none() { + return Err(ParseError::new( + ParseErrorKind::ExpectedLeftParenInFunctionDefinition, + self.peek(tokens).span, + )); + } + + self.skip_empty_lines(tokens); + + if self.match_exact(tokens, TokenKind::Self_).is_none() { + return Err(ParseError::new( + ParseErrorKind::ExpectedSelfAsFirstParameter, + self.peek(tokens).span, + )); + } + let self_span = self.last(tokens).unwrap().span; + + self.skip_empty_lines(tokens); + + let mut parameters = vec![]; + + if self.match_exact(tokens, TokenKind::RightParen).is_none() { + if self.match_exact(tokens, TokenKind::Comma).is_none() { + return Err(ParseError::new( + ParseErrorKind::ExpectedRightParenOrCommaAfterSelf, + self.peek(tokens).span, + )); + } + + self.skip_empty_lines(tokens); + + while self.match_exact(tokens, TokenKind::RightParen).is_none() { + if let Some(param_name) = self.match_exact(tokens, TokenKind::Identifier) { + let span = self.last(tokens).unwrap().span; + let param_type = if self.match_exact(tokens, TokenKind::Colon).is_some() { + Some(self.type_annotation(tokens)?) + } else { + None + }; + + parameters.push((span, param_name.lexeme, param_type)); + + self.skip_empty_lines(tokens); + let has_comma = self.match_exact(tokens, TokenKind::Comma).is_some(); + self.skip_empty_lines(tokens); + + if self.match_exact(tokens, TokenKind::RightParen).is_some() { + break; + } + + if !has_comma && self.peek(tokens).kind != TokenKind::RightParen { + return Err(ParseError::new( + ParseErrorKind::ExpectedCommaEllipsisOrRightParenInFunctionDefinition, + self.peek(tokens).span, + )); + } + } else { + return Err(ParseError::new( + ParseErrorKind::ExpectedParameterNameInFunctionDefinition, + self.peek(tokens).span, + )); + } + } + } + + // Parse optional return type annotation + let return_type_annotation = if self.match_exact(tokens, TokenKind::Arrow).is_some() { + Some(self.type_annotation(tokens)?) + } else { + None + }; + + // Parse optional body and local variables + let (body, local_variables) = if self.match_exact(tokens, TokenKind::Equal).is_none() { + (None, vec![]) + } else { + self.skip_empty_lines(tokens); + let body = self.expression(tokens)?; + + let mut local_variables = Vec::new(); + + if self + .match_exact_beyond_linebreaks(tokens, TokenKind::Where) + .is_some() + { + let keyword_span = self.last(tokens).unwrap().span; + self.skip_empty_lines(tokens); + if let Ok(local_variable) = self.parse_variable(tokens, false) { + local_variables.push(local_variable); + } else { + return Err(ParseError::new( + ParseErrorKind::ExpectedLocalVariableDefinition, + keyword_span, + )); + } + + while self + .match_exact_beyond_linebreaks(tokens, TokenKind::And) + .is_some() + { + let keyword_span = self.last(tokens).unwrap().span; + self.skip_empty_lines(tokens); + if let Ok(local_variable) = self.parse_variable(tokens, false) { + local_variables.push(local_variable); + } else { + return Err(ParseError::new( + ParseErrorKind::ExpectedLocalVariableDefinition, + keyword_span, + )); + } + } + } + + (Some(body), local_variables) + }; + + Ok(MethodDefinition { + function_name_span, + function_name: fn_name.lexeme, + type_parameters, + self_span, + parameters, + body, + local_variables, + return_type_annotation, + decorators, + }) + } + fn parse_procedure(&mut self, tokens: &[Token<'a>]) -> Result> { let span = self.last(tokens).unwrap().span; let procedure_kind = match self.last(tokens).unwrap().kind { @@ -1408,9 +1745,24 @@ impl<'a> Parser<'a> { } else if self.match_exact(tokens, TokenKind::Period).is_some() { let ident = self.identifier(tokens)?; let ident_span = self.last(tokens).unwrap().span; - let full_span = expr.full_span().extend(&ident_span); - expr = Expression::AccessField(full_span, ident_span, Box::new(expr), ident) + // Check if this is a method call (followed by '(') + if self.match_exact(tokens, TokenKind::LeftParen).is_some() { + let args = self.arguments(tokens)?; + let full_span = expr.full_span().extend(&self.last(tokens).unwrap().span); + + expr = Expression::MethodCall { + full_span, + method_span: ident_span, + receiver: Box::new(expr), + method_name: ident, + args, + }; + } else { + // Field access + let full_span = expr.full_span().extend(&ident_span); + expr = Expression::AccessField(full_span, ident_span, Box::new(expr), ident); + } } else { return Ok(expr); } @@ -1589,6 +1941,10 @@ impl<'a> Parser<'a> { } Ok(Expression::Identifier(span, identifier.lexeme)) + } else if let Some(self_token) = self.match_exact(tokens, TokenKind::Self_) { + // Allow 'self' to be used as an identifier in method bodies + let span = self.last(tokens).unwrap().span; + Ok(Expression::Identifier(span, self_token.lexeme)) } else if let Some(inner) = self.match_any(tokens, &[TokenKind::True, TokenKind::False]) { Ok(Expression::Boolean( inner.span, @@ -1716,6 +2072,7 @@ impl<'a> Parser<'a> { self.peek(tokens).kind, TokenKind::Number | TokenKind::Identifier + | TokenKind::Self_ | TokenKind::LeftParen | TokenKind::QuestionMark ) diff --git a/numbat/src/prefix_transformer.rs b/numbat/src/prefix_transformer.rs index 3f1a5fac8..87046ca93 100644 --- a/numbat/src/prefix_transformer.rs +++ b/numbat/src/prefix_transformer.rs @@ -82,6 +82,12 @@ impl Transformer { Expression::AccessField(_, _, expr, _) => { self.transform_expression(expr); } + Expression::MethodCall { receiver, args, .. } => { + self.transform_expression(receiver); + for arg in args { + self.transform_expression(arg); + } + } Expression::List(_, elements) => { for e in elements { self.transform_expression(e); @@ -212,6 +218,29 @@ impl Transformer { self.transform_expression(arg); } } + Statement::DefineImpl { methods, .. } => { + for method in methods.iter_mut() { + let mut method_body_transformer = self.clone(); + + method_body_transformer + .prefix_parser + .add_other_identifier("self", method.self_span)?; + + for (param_span, param, _) in &method.parameters { + method_body_transformer + .prefix_parser + .add_other_identifier(param, *param_span)?; + } + + if let Some(ref mut expr) = method.body { + method_body_transformer.transform_expression(expr); + } + + for def in &mut method.local_variables { + method_body_transformer.transform_define_variable(def)?; + } + } + } } Ok(()) diff --git a/numbat/src/tokenizer.rs b/numbat/src/tokenizer.rs index 4618632c1..09e29a5d0 100644 --- a/numbat/src/tokenizer.rs +++ b/numbat/src/tokenizer.rs @@ -113,6 +113,8 @@ pub enum TokenKind { Unit, Use, Struct, + Impl, + Self_, Long, Short, @@ -485,6 +487,8 @@ impl Tokenizer { m.insert("unit", TokenKind::Unit); m.insert("use", TokenKind::Use); m.insert("struct", TokenKind::Struct); + m.insert("impl", TokenKind::Impl); + m.insert("self", TokenKind::Self_); m.insert("long", TokenKind::Long); m.insert("short", TokenKind::Short); m.insert("both", TokenKind::Both); diff --git a/numbat/src/traversal.rs b/numbat/src/traversal.rs index be186a327..fe0d51f53 100644 --- a/numbat/src/traversal.rs +++ b/numbat/src/traversal.rs @@ -71,6 +71,14 @@ impl ForAllTypeSchemes for Expression<'_> { Expression::TypedHole(_, type_) => { f(type_); } + Expression::MethodCall(_, _, receiver, _, args, struct_type, return_type) => { + receiver.for_all_type_schemes(f); + for arg in args { + arg.for_all_type_schemes(f); + } + f(struct_type); + f(return_type); + } } } } @@ -107,6 +115,7 @@ impl ForAllTypeSchemes for Statement<'_> { } } Statement::DefineStruct(info) => info.for_all_type_schemes(f), + Statement::DefineImpl { struct_info, .. } => struct_info.for_all_type_schemes(f), } } } @@ -139,6 +148,7 @@ impl ForAllExpressions for Statement<'_> { } } Statement::DefineStruct(_) => {} + Statement::DefineImpl { .. } => {} } } } @@ -191,6 +201,12 @@ impl ForAllExpressions for Expression<'_> { } } Expression::TypedHole(_, _) => {} + Expression::MethodCall(_, _, receiver, _, args, _, _) => { + receiver.for_all_expressions(f); + for arg in args { + arg.for_all_expressions(f); + } + } } } } diff --git a/numbat/src/typechecker/const_evaluation.rs b/numbat/src/typechecker/const_evaluation.rs index 13720dbc9..ae533e5e7 100644 --- a/numbat/src/typechecker/const_evaluation.rs +++ b/numbat/src/typechecker/const_evaluation.rs @@ -97,6 +97,7 @@ pub fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result { typed_ast::Expression::AccessField(_, _, _, _, _, _) => "access field of struct", typed_ast::Expression::List(_, _, _) => "lists", typed_ast::Expression::TypedHole(_, _) => "typed hole", + typed_ast::Expression::MethodCall(_, _, _, _, _, _, _) => "method call", }; Err(Box::new(TypeCheckError::UnsupportedConstEvalExpression( diff --git a/numbat/src/typechecker/constraints.rs b/numbat/src/typechecker/constraints.rs index 36c1ef0f9..670f0d839 100644 --- a/numbat/src/typechecker/constraints.rs +++ b/numbat/src/typechecker/constraints.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use compact_str::{CompactString, format_compact}; use super::substitutions::{ApplySubstitution, Substitution, SubstitutionError}; +use super::type_scheme::TypeScheme; use crate::type_variable::TypeVariable; use crate::typed_ast::{DType, DTypeFactor, StructKind, Type}; @@ -192,6 +193,8 @@ pub enum Constraint { IsDType(Type), EqualScalar(DType), HasField(Type, CompactString, Type), + /// Constraint for method calls: (receiver_type, method_name, arg_types, return_type) + HasMethod(Type, CompactString, Vec, Type), } impl Constraint { @@ -224,6 +227,10 @@ impl Constraint { // Trivial resolution handling for structs is done directly in the type checker TrivialResolution::Unknown } + Constraint::HasMethod(_, _, _, _) => { + // Method resolution is deferred + TrivialResolution::Unknown + } } } @@ -351,6 +358,51 @@ impl Constraint { } } Constraint::HasField(_, _, _) => None, + Constraint::HasMethod(struct_type, method_name, arg_types, return_type) + if struct_type.is_closed() => + { + if let Type::Struct(info) = struct_type + && let Some(method_info) = info.methods.get(method_name) + { + // Get the function type and check it matches + let fn_type = match &method_info.fn_type { + TypeScheme::Concrete(t) => t.clone(), + TypeScheme::Quantified(_, qt) => qt.inner.clone(), + }; + + if let Type::Fn(param_types, ret_type) = fn_type { + // param_types[0] is self, rest are the method parameters + let expected_arg_count = param_types.len().saturating_sub(1); + if arg_types.len() == expected_arg_count { + // Create equality constraints for arguments and return type + let mut new_constraints = vec![]; + + // Add constraint for return type + new_constraints.push(Constraint::Equal( + ret_type.as_ref().clone(), + return_type.clone(), + )); + + // Add constraints for arguments (skip self parameter) + for (param_type, arg_type) in + param_types.iter().skip(1).zip(arg_types.iter()) + { + new_constraints + .push(Constraint::Equal(param_type.clone(), arg_type.clone())); + } + + Some(Satisfied::with_new_constraints(new_constraints)) + } else { + None + } + } else { + None + } + } else { + None + } + } + Constraint::HasMethod(_, _, _, _) => None, } } @@ -364,6 +416,16 @@ impl Constraint { Constraint::HasField(struct_type, field_name, field_type) => { format_compact!("HasField({struct_type}, \"{field_name}\", {field_type})") } + Constraint::HasMethod(struct_type, method_name, arg_types, return_type) => { + let args_str = arg_types + .iter() + .map(|t| t.to_string()) + .collect::>() + .join(", "); + format_compact!( + "HasMethod({struct_type}, \"{method_name}\", [{args_str}], {return_type})" + ) + } } } @@ -392,6 +454,13 @@ impl ApplySubstitution for Constraint { struct_type.apply(substitution)?; field_type.apply(substitution)?; } + Constraint::HasMethod(struct_type, _, arg_types, return_type) => { + struct_type.apply(substitution)?; + for arg_type in arg_types { + arg_type.apply(substitution)?; + } + return_type.apply(substitution)?; + } } Ok(()) } diff --git a/numbat/src/typechecker/error.rs b/numbat/src/typechecker/error.rs index b0b623f0f..a28245d36 100644 --- a/numbat/src/typechecker/error.rs +++ b/numbat/src/typechecker/error.rs @@ -173,6 +173,26 @@ pub enum TypeCheckError { #[error("Multiple typed holes in statement")] MultipleTypedHoles(Span), + + #[error("Can not call method '{2}' on non-struct type '{3}'")] + MethodCallOnNonStructType(Span, Span, String, Type), + + #[error("Method '{2}' does not exist on struct '{3}'")] + UnknownMethod(Span, Span, String, String), + + #[error("impl block for unknown struct '{1}'")] + ImplForUnknownStruct(Span, String), + + #[error("Type parameter mismatch in impl block")] + ImplTypeParameterMismatch { + impl_span: Span, + struct_span: Span, + expected: usize, + actual: usize, + }, + + #[error("Duplicate method '{1}' in impl block")] + DuplicateMethodInImpl(Span, String, Span), } pub type Result = std::result::Result>; diff --git a/numbat/src/typechecker/mod.rs b/numbat/src/typechecker/mod.rs index da3e481a2..d94249756 100644 --- a/numbat/src/typechecker/mod.rs +++ b/numbat/src/typechecker/mod.rs @@ -16,6 +16,8 @@ use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; +use indexmap::IndexMap; + use crate::arithmetic::Exponent; use crate::ast::{ self, BinaryOperator, DefineVariable, ProcedureKind, StringPart, TypeAnnotation, @@ -217,6 +219,32 @@ impl TypeChecker { self.constraints.add_dtype_constraint(type_) } + fn register_type_parameter( + &mut self, + span: Span, + name: &str, + bound: &Option, + ) { + self.type_namespace + .add_identifier( + name.to_compact_string(), + span, + CompactString::const_new("type parameter"), + ) + .ok(); + + self.registry.introduced_type_parameters.push(( + span, + name.to_compact_string(), + bound.clone(), + )); + + if let Some(TypeParameterBound::Dim) = bound { + self.add_dtype_constraint(&Type::TPar(name.to_compact_string())) + .ok(); + } + } + fn enforce_dtype(&mut self, type_: &Type, span: Span) -> Result<()> { if self .constraints @@ -263,6 +291,7 @@ impl TypeChecker { name: struct_info.name.clone(), kind: StructKind::Instance(vec![]), fields: struct_info.fields.clone(), + methods: struct_info.methods.clone(), }))); } @@ -286,6 +315,7 @@ impl TypeChecker { name: struct_info.name.clone(), kind: StructKind::Instance(concrete_type_args), fields: instantiated_fields, + methods: struct_info.methods.clone(), }))); } @@ -1007,6 +1037,7 @@ impl TypeChecker { name: struct_info.name.clone(), kind: StructKind::Instance(vec![]), fields: struct_info.fields.clone(), + methods: struct_info.methods.clone(), } } StructKind::Definition(type_parameters) => { @@ -1048,6 +1079,7 @@ impl TypeChecker { variables.iter().map(|v| Type::TVar(v.clone())).collect(), ), fields: instantiated_fields, + methods: struct_info.methods.clone(), } } StructKind::Instance(_) => { @@ -1215,6 +1247,182 @@ impl TypeChecker { let type_ = self.fresh_type_variable(); typed_ast::Expression::TypedHole(*span, TypeScheme::concrete(type_)) } + ast::Expression::MethodCall { + full_span, + method_span, + receiver, + method_name, + args, + } => { + let receiver_checked = self.elaborate_expression(receiver)?; + let receiver_type = receiver_checked.get_type(); + + let arguments_checked = args + .iter() + .map(|a| self.elaborate_expression(a)) + .collect::>>()?; + let argument_types: Vec = + arguments_checked.iter().map(|e| e.get_type()).collect(); + + if receiver_type.is_closed() { + let Type::Struct(ref struct_info) = receiver_type else { + return Err(Box::new(TypeCheckError::MethodCallOnNonStructType( + *method_span, + receiver.full_span(), + method_name.to_string(), + receiver_type.clone(), + ))); + }; + + let struct_definition = + self.structs.get(&struct_info.name).ok_or_else(|| { + TypeCheckError::UnknownStruct( + *method_span, + struct_info.name.to_string(), + ) + })?; + + let Some(method_info) = struct_definition.methods.get(*method_name) else { + return Err(Box::new(TypeCheckError::UnknownMethod( + *method_span, + receiver.full_span(), + method_name.to_string(), + struct_info.name.to_string(), + ))); + }; + + let struct_type_params = match &struct_definition.kind { + StructKind::Definition(params) => params.clone(), + StructKind::Instance(_) => vec![], + }; + let method_fn_type = method_info.fn_type.clone(); + let method_type_params = method_info.type_parameters.clone(); + let method_definition_span = method_info.definition_span; + + let instance_type_args = match &struct_info.kind { + StructKind::Instance(args) => args.clone(), + StructKind::Definition(_) => vec![], + }; + + // Fresh type variables for struct type params (allows DType substitution to work) + let fresh_vars: Vec<_> = struct_type_params + .iter() + .map(|(_, _, bound)| { + let fresh_var = self.name_generator.fresh_type_variable(); + if let Some(TypeParameterBound::Dim) = bound { + self.constraints + .add_dtype_constraint(&Type::TVar(fresh_var.clone())) + .ok(); + } + fresh_var + }) + .collect(); + + let struct_substitution = Substitution( + struct_type_params + .iter() + .zip(fresh_vars.iter()) + .map(|((_, name, _), var)| { + (TypeVariable::new(name.clone()), Type::TVar(var.clone())) + }) + .collect(), + ); + + for (var, arg) in fresh_vars.iter().zip(instance_type_args.iter()) { + self.add_equal_constraint(&Type::TVar(var.clone()), arg) + .ok(); + } + + let mut fn_type = method_fn_type.to_concrete_type(); + fn_type.apply(&struct_substitution).ok(); + + // Fresh type variables for method's own type parameters + let method_type_param_substitution = Substitution( + method_type_params + .iter() + .map(|(_, name, bound)| { + let fresh_var = self.name_generator.fresh_type_variable(); + if let Some(TypeParameterBound::Dim) = bound { + self.constraints + .add_dtype_constraint(&Type::TVar(fresh_var.clone())) + .ok(); + } + (TypeVariable::new(name.clone()), Type::TVar(fresh_var)) + }) + .collect(), + ); + fn_type.apply(&method_type_param_substitution).ok(); + + let Type::Fn(parameter_types, return_type) = fn_type else { + unreachable!("Method type should be a function type"); + }; + + let expected_args = parameter_types.len() - 1; // excludes self + if arguments_checked.len() != expected_args { + return Err(Box::new(TypeCheckError::WrongArity { + callable_span: *method_span, + callable_name: format!("{}.{}", struct_info.name, method_name), + callable_definition_span: Some(method_definition_span), + arity: expected_args..=expected_args, + num_args: arguments_checked.len(), + })); + } + + // Constrain self parameter + if !parameter_types.is_empty() { + self.add_equal_constraint(&receiver_type, ¶meter_types[0]) + .ok(); + } + + // Constrain remaining parameters + for (param_type, arg_type) in + parameter_types.iter().skip(1).zip(argument_types.iter()) + { + if self + .add_equal_constraint(param_type, arg_type) + .is_trivially_violated() + { + return Err(Box::new(TypeCheckError::IncompatibleTypesInFunctionCall( + None, + param_type.clone(), + receiver.full_span(), + arg_type.clone(), + ))); + } + } + + typed_ast::Expression::MethodCall( + *method_span, + *full_span, + Box::new(receiver_checked), + method_name, + arguments_checked, + TypeScheme::concrete(receiver_type), + TypeScheme::concrete(return_type.as_ref().clone()), + ) + } else { + // Defer method resolution via constraint + let return_type = self.fresh_type_variable(); + self.constraints + .add(Constraint::HasMethod( + receiver_type.clone(), + method_name.to_compact_string(), + argument_types.clone(), + return_type.clone(), + )) + .ok(); + + typed_ast::Expression::MethodCall( + *method_span, + *full_span, + Box::new(receiver_checked), + method_name, + arguments_checked, + TypeScheme::concrete(receiver_type), + TypeScheme::concrete(return_type), + ) + } + } }) } @@ -1489,30 +1697,7 @@ impl TypeChecker { type_parameter.to_string(), ))); } - - self.type_namespace - .add_identifier( - type_parameter.to_compact_string(), - *span, - CompactString::const_new("type parameter"), - ) - .ok(); - - self.registry.introduced_type_parameters.push(( - *span, - type_parameter.to_compact_string(), - bound.clone(), - )); - - match bound { - Some(TypeParameterBound::Dim) => { - self.add_dtype_constraint(&Type::TPar( - type_parameter.to_compact_string(), - )) - .ok(); - } - None => {} - } + self.register_type_parameter(*span, type_parameter, bound); } let mut typed_parameters = vec![]; @@ -1944,12 +2129,367 @@ impl TypeChecker { )) }) .collect::>()?, + methods: IndexMap::new(), }; self.structs .insert(struct_name.to_compact_string(), struct_info.clone()); typed_ast::Statement::DefineStruct(struct_info) } + ast::Statement::DefineImpl { + impl_span, + type_parameters, + struct_name_span, + struct_name, + struct_type_args, + methods, + } => { + // Look up the struct definition + let struct_info = self + .structs + .get(*struct_name) + .ok_or_else(|| { + TypeCheckError::ImplForUnknownStruct( + *struct_name_span, + struct_name.to_string(), + ) + })? + .clone(); + + let struct_type_params = match &struct_info.kind { + StructKind::Definition(params) => params.clone(), + StructKind::Instance(_) => { + unreachable!("Struct definition should have Definition kind") + } + }; + + if type_parameters.len() != struct_type_params.len() { + return Err(Box::new(TypeCheckError::ImplTypeParameterMismatch { + impl_span: *impl_span, + struct_span: struct_info.definition_span, + expected: struct_type_params.len(), + actual: type_parameters.len(), + })); + } + + let mut impl_typechecker = self.clone(); + + for (span, type_parameter, bound) in type_parameters { + impl_typechecker.register_type_parameter(*span, type_parameter, bound); + } + + // Build self type (use Dimension for type params since fields use dimension types) + let self_type_args: Vec = if struct_type_args.is_empty() { + type_parameters + .iter() + .map(|(_, name, _)| { + Type::Dimension(DType::from_type_parameter(name.to_compact_string())) + }) + .collect() + } else { + struct_type_args + .iter() + .map(|a| impl_typechecker.type_from_annotation(a)) + .collect::>>()? + }; + + let mut self_struct_info = struct_info.clone(); + self_struct_info.kind = StructKind::Instance(self_type_args.clone()); + let substitution: Substitution = Substitution( + struct_type_params + .iter() + .zip(self_type_args.iter()) + .map(|((_, name, _), arg)| (TypeVariable::new(name.clone()), arg.clone())) + .collect(), + ); + for (_, field_type) in self_struct_info.fields.values_mut() { + field_type.apply(&substitution).ok(); + } + let self_type = Type::Struct(Box::new(self_struct_info.clone())); + + let mut seen_methods: HashMap<&str, Span> = HashMap::new(); + + // Pass 1: Collect signatures (allows methods to call each other) + struct MethodSignatureInfo { + typed_parameters: Vec<(Span, Type)>, + return_type: Type, + mangled_name: CompactString, + } + let mut method_signatures: Vec = Vec::new(); + + for method in methods.iter() { + if let Some(first_span) = seen_methods.get(method.function_name) { + return Err(Box::new(TypeCheckError::DuplicateMethodInImpl( + method.function_name_span, + method.function_name.to_string(), + *first_span, + ))); + } + seen_methods.insert(method.function_name, method.function_name_span); + + let mut sig_typechecker = impl_typechecker.clone(); + sig_typechecker.type_namespace.save(); + + for (span, type_parameter, bound) in &method.type_parameters { + sig_typechecker.register_type_parameter(*span, type_parameter, bound); + } + + let mut typed_parameters = vec![(method.self_span, self_type.clone())]; + for (param_span, _param_name, type_annotation) in &method.parameters { + let param_type = if let Some(annotation) = type_annotation { + sig_typechecker.type_from_annotation(annotation)? + } else { + sig_typechecker.fresh_type_variable() + }; + typed_parameters.push((*param_span, param_type)); + } + + let return_type = if let Some(annotation) = &method.return_type_annotation { + sig_typechecker.type_from_annotation(annotation)? + } else { + sig_typechecker.fresh_type_variable() + }; + + let param_types: Vec = + typed_parameters.iter().map(|(_, t)| t.clone()).collect(); + let mut fn_type = Type::Fn(param_types.clone(), Box::new(return_type.clone())); + + // Normalize impl type param names to struct's names (e.g., D -> X) + let impl_to_struct_substitution = Substitution( + type_parameters + .iter() + .zip(struct_type_params.iter()) + .map(|((_, impl_name, _), (_, struct_name, _))| { + ( + TypeVariable::new(impl_name.to_compact_string()), + Type::Dimension(DType::from_type_parameter( + struct_name.clone(), + )), + ) + }) + .collect(), + ); + fn_type.apply(&impl_to_struct_substitution).ok(); + + let all_type_params: Vec<_> = type_parameters + .iter() + .chain(method.type_parameters.iter()) + .map(|(_, name, _)| (*name).to_compact_string()) + .collect(); + + let fn_type_scheme = if all_type_params.is_empty() { + TypeScheme::concrete(fn_type.clone()) + } else { + TypeScheme::make_quantified(fn_type.clone()) + }; + + let method_info = typed_ast::MethodInfo { + definition_span: method.function_name_span, + name: method.function_name.to_compact_string(), + type_parameters: method + .type_parameters + .iter() + .map(|(span, name, bound)| { + (*span, (*name).to_compact_string(), bound.clone()) + }) + .collect(), + parameters: method + .parameters + .iter() + .map(|(span, name, _)| (*span, (*name).to_compact_string())) + .collect(), + fn_type: fn_type_scheme.clone(), + }; + + self_struct_info + .methods + .insert(method.function_name.to_compact_string(), method_info); + + let mangled_name = format_compact!("{}::{}", struct_name, method.function_name); + let signature = FunctionSignature { + name: mangled_name.clone(), + definition_span: method.function_name_span, + type_parameters: type_parameters + .iter() + .chain(method.type_parameters.iter()) + .map(|(span, name, bound)| { + (*span, (*name).to_compact_string(), bound.clone()) + }) + .collect(), + parameters: std::iter::once(( + method.self_span, + CompactString::const_new("self"), + None, + )) + .chain(method.parameters.iter().map(|(span, name, annotation)| { + (*span, (*name).to_compact_string(), annotation.clone()) + })) + .collect(), + return_type_annotation: method.return_type_annotation.clone(), + fn_type: fn_type_scheme.clone(), + }; + + self.env.add_function( + mangled_name.clone(), + signature, + FunctionMetadata { + name: crate::decorator::name(&method.decorators) + .map(CompactString::from), + url: crate::decorator::url(&method.decorators).map(CompactString::from), + description: crate::decorator::description(&method.decorators), + examples: crate::decorator::examples(&method.decorators), + }, + ); + + sig_typechecker.type_namespace.restore(); + + method_signatures.push(MethodSignatureInfo { + typed_parameters, + return_type, + mangled_name, + }); + } + + // Update struct registry so method calls can resolve + let mut registry_struct_info = struct_info.clone(); + registry_struct_info.methods = self_struct_info.methods.clone(); + impl_typechecker.structs.insert( + struct_name.to_compact_string(), + registry_struct_info.clone(), + ); + + // Pass 2: Type check method bodies + let mut compiled_methods = Vec::new(); + + for (method, sig_info) in methods.iter().zip(method_signatures.iter()) { + let mut method_typechecker = impl_typechecker.clone(); + method_typechecker.env.save(); + method_typechecker.type_namespace.save(); + method_typechecker.value_namespace.save(); + + for (span, type_parameter, bound) in &method.type_parameters { + method_typechecker.register_type_parameter(*span, type_parameter, bound); + } + + method_typechecker.env.add_scheme( + CompactString::const_new("self"), + TypeScheme::make_quantified(self_type.clone()), + method.self_span, + false, + ); + + for ((param_span, param_name, _), (_, param_type)) in method + .parameters + .iter() + .zip(sig_info.typed_parameters.iter().skip(1)) + { + method_typechecker.env.add_scheme( + param_name.to_compact_string(), + TypeScheme::make_quantified(param_type.clone()), + *param_span, + false, + ); + } + + let mut typed_local_variables = vec![]; + for local_var in &method.local_variables { + typed_local_variables + .push(method_typechecker.elaborate_define_variable(local_var)?); + } + + let body_checked = if let Some(body) = &method.body { + let body_checked = method_typechecker.elaborate_expression(body)?; + let body_type = body_checked.get_type(); + + if method_typechecker + .add_equal_constraint(&body_type, &sig_info.return_type) + .is_trivially_violated() + { + if let (Type::Dimension(dtype_body), Type::Dimension(dtype_ret)) = + (&body_type, &sig_info.return_type) + { + return Err(Box::new(TypeCheckError::IncompatibleDimensions( + IncompatibleDimensionsError { + span_operation: method.function_name_span, + operation: "method return type".into(), + span_expected: method + .return_type_annotation + .as_ref() + .map(|a| a.full_span()) + .unwrap_or(method.function_name_span), + expected_name: "specified return type", + expected_dimensions: method_typechecker + .registry + .get_derived_entry_names_for( + &dtype_ret.to_base_representation(), + ), + expected_type: dtype_ret.to_base_representation(), + span_actual: body.full_span(), + actual_name: " actual return type", + actual_name_for_fix: "expression in the method body", + actual_dimensions: method_typechecker + .registry + .get_derived_entry_names_for( + &dtype_body.to_base_representation(), + ), + actual_type: dtype_body.to_base_representation(), + }, + ))); + } else { + return Err(Box::new( + TypeCheckError::IncompatibleTypesInAnnotation( + "method return type".into(), + method + .return_type_annotation + .as_ref() + .map(|a| a.full_span()) + .unwrap_or(method.function_name_span), + sig_info.return_type.clone(), + body.full_span(), + body_type, + method.function_name_span, + ), + )); + } + } + Some(body_checked) + } else { + None + }; + + let param_names: Vec = + std::iter::once(CompactString::const_new("self")) + .chain( + method + .parameters + .iter() + .map(|(_, name, _)| (*name).to_compact_string()), + ) + .collect(); + + compiled_methods.push(typed_ast::CompiledMethod { + mangled_name: sig_info.mangled_name.clone(), + parameters: param_names, + local_variables: typed_local_variables, + body: body_checked, + }); + + method_typechecker.value_namespace.restore(); + method_typechecker.type_namespace.restore(); + method_typechecker.env.restore(); + } + + let mut updated_struct_info = struct_info.clone(); + updated_struct_info.methods = self_struct_info.methods.clone(); + self.structs + .insert(struct_name.to_compact_string(), updated_struct_info.clone()); + + typed_ast::Statement::DefineImpl { + struct_name: struct_name.to_compact_string(), + struct_info: updated_struct_info, + methods: compiled_methods, + } + } }) } diff --git a/numbat/src/typechecker/substitutions.rs b/numbat/src/typechecker/substitutions.rs index 80594292f..125a95ced 100644 --- a/numbat/src/typechecker/substitutions.rs +++ b/numbat/src/typechecker/substitutions.rs @@ -226,6 +226,14 @@ impl ApplySubstitution for Expression<'_> { element_type.apply(s) } Expression::TypedHole(_, type_) => type_.apply(s), + Expression::MethodCall(_, _, receiver, _, args, struct_type, return_type) => { + receiver.apply(s)?; + for arg in args { + arg.apply(s)?; + } + struct_type.apply(s)?; + return_type.apply(s) + } } } } @@ -265,6 +273,10 @@ impl ApplySubstitution for Statement<'_> { Ok(()) } + Statement::DefineImpl { struct_info, .. } => { + struct_info.apply(s)?; + Ok(()) + } } } } diff --git a/numbat/src/typechecker/tests/type_checking.rs b/numbat/src/typechecker/tests/type_checking.rs index 9eb378e4b..b75585fff 100644 --- a/numbat/src/typechecker/tests/type_checking.rs +++ b/numbat/src/typechecker/tests/type_checking.rs @@ -861,3 +861,281 @@ fn instantiation() { TypeCheckError::ConstraintSolverError(..) )); } + +#[test] +fn methods_basic() { + // Basic method on non-generic struct + assert_successful_typecheck( + " + struct Point { x: A, y: A } + impl Point { + fn get_x(self) -> A = self.x + } + let my_point = Point { x: 1a, y: 2a } + let my_x: A = my_point.get_x() + ", + ); + + // Method with additional parameters + assert_successful_typecheck( + " + struct Container { value: A } + impl Container { + fn scale(self, factor: Scalar) -> A = self.value * factor + } + let my_container = Container { value: 5a } + let my_result: A = my_container.scale(3) + ", + ); + + // Multiple methods in one impl block + assert_successful_typecheck( + " + struct Pair { first: A, second: B } + impl Pair { + fn get_first(self) -> A = self.first + fn get_second(self) -> B = self.second + fn product(self) -> C = self.first * self.second + } + let my_pair = Pair { first: 2a, second: 3b } + let my_first: A = my_pair.get_first() + let my_second: B = my_pair.get_second() + let my_prod: C = my_pair.product() + ", + ); + + // Methods calling other methods in same impl block + assert_successful_typecheck( + " + struct Vec2 { x: A, y: A } + impl Vec2 { + fn norm_squared(self) -> A^2 = self.x^2 + self.y^2 + fn uses_norm(self) -> A^2 = self.norm_squared() * 2 + } + let my_vec = Vec2 { x: 3a, y: 4a } + let my_norm: A^2 = my_vec.uses_norm() + ", + ); +} + +#[test] +fn methods_generic_structs() { + // Method on generic struct with Dim bound + assert_successful_typecheck( + " + struct GenericVec { x: D, y: D } + impl GenericVec { + fn norm_squared(self) -> D^2 = self.x^2 + self.y^2 + } + let my_gvec = GenericVec { x: 3a, y: 4a } + let my_gnorm: A^2 = my_gvec.norm_squared() + ", + ); + + // Method with different impl type param names than struct definition + assert_successful_typecheck( + " + struct MyVec { x: X, y: X } + impl MyVec { + fn norm_squared(self) -> D^2 = self.x^2 + self.y^2 + } + let my_mvec = MyVec { x: 3a, y: 4a } + let my_mnorm: A^2 = my_mvec.norm_squared() + ", + ); + + // Multiple type parameters with different names + assert_successful_typecheck( + " + struct Tuple { first: X, second: Y } + impl Tuple { + fn get_first(self) -> A1 = self.first + fn get_second(self) -> B1 = self.second + } + let my_tuple = Tuple { first: 1a, second: 2b } + let my_tfirst: A = my_tuple.get_first() + let my_tsecond: B = my_tuple.get_second() + ", + ); + + // Generic method on generic struct + assert_successful_typecheck( + " + struct Container { value: D } + impl Container { + fn multiply(self, factor: S) -> D * S = self.value * factor + } + let my_cont = Container { value: 5a } + let my_cresult: C = my_cont.multiply(2b) + ", + ); + + // Non-dimension type parameters + assert_successful_typecheck( + " + struct Holder { item: T } + impl Holder { + fn get(self) -> X = self.item + } + let my_holder1 = Holder { item: \"hello\" } + let my_str: String = my_holder1.get() + let my_holder2 = Holder { item: true } + let my_bool: Bool = my_holder2.get() + ", + ); + + // Mixed dimension and non-dimension type params + assert_successful_typecheck( + " + struct Labeled { value: D, label: L } + impl Labeled { + fn get_value(self) -> V = self.value + fn get_label(self) -> T = self.label + } + let my_labeled = Labeled { value: 10a, label: \"length\" } + let my_lval: A = my_labeled.get_value() + let my_llab: String = my_labeled.get_label() + ", + ); +} + +#[test] +fn methods_errors() { + // Impl for unknown struct + assert!(matches!( + get_typecheck_error( + " + impl UnknownStruct { + fn foo(self) -> Scalar = 1 + } + " + ), + TypeCheckError::ImplForUnknownStruct(_, name) if name == "UnknownStruct" + )); + + // Unknown method call + assert!(matches!( + get_typecheck_error( + " + struct TestFoo { x: A } + let my_foo = TestFoo { x: 1a } + my_foo.unknown_method() + " + ), + TypeCheckError::UnknownMethod(_, _, method, _) if method == "unknown_method" + )); + + // Method call on non-struct type + assert!(matches!( + get_typecheck_error("(1a).some_method()"), + TypeCheckError::MethodCallOnNonStructType(_, _, method, _) if method == "some_method" + )); + + // Method with wrong arity - too few arguments + assert!(matches!( + get_typecheck_error( + " + struct TestBar { x: A } + impl TestBar { + fn baz(self, y: A) -> A = self.x + y + } + let my_bar = TestBar { x: 1a } + my_bar.baz() + " + ), + TypeCheckError::WrongArity { callable_name, arity, num_args: 0, .. } + if callable_name == "TestBar.baz" && arity == (1..=1) + )); + + // Method with wrong arity - too many arguments + assert!(matches!( + get_typecheck_error( + " + struct TestQux { x: A } + impl TestQux { + fn quux(self) -> A = self.x + } + let my_qux = TestQux { x: 1a } + my_qux.quux(1a, 2a) + " + ), + TypeCheckError::WrongArity { callable_name, arity, num_args: 2, .. } + if callable_name == "TestQux.quux" && arity == (0..=0) + )); + + // Duplicate method in impl block + assert!(matches!( + get_typecheck_error( + " + struct TestDup { x: A } + impl TestDup { + fn dup_method(self) -> A = self.x + fn dup_method(self) -> A = self.x * 2 + } + " + ), + TypeCheckError::DuplicateMethodInImpl(_, method, _) if method == "dup_method" + )); + + // Wrong number of type parameters in impl - too few + assert!(matches!( + get_typecheck_error( + " + struct TestWrapper { inner: D } + impl TestWrapper { + fn get(self) -> Scalar = 1 + } + " + ), + TypeCheckError::ImplTypeParameterMismatch { + expected: 1, + actual: 0, + .. + } + )); + + // Wrong number of type parameters in impl - too many + assert!(matches!( + get_typecheck_error( + " + struct TestSimple { x: A } + impl TestSimple { + fn get(self) -> D = self.x + } + " + ), + TypeCheckError::ImplTypeParameterMismatch { + expected: 0, + actual: 1, + .. + } + )); + + // Incompatible return type + assert!(matches!( + get_typecheck_error( + " + struct TestRet { x: A } + impl TestRet { + fn wrong_ret(self) -> B = self.x + } + " + ), + TypeCheckError::IncompatibleDimensions(..) + )); + + // Type mismatch in method call arguments + assert!(matches!( + get_typecheck_error( + " + struct TestArg { x: A } + impl TestArg { + fn add(self, y: A) -> A = self.x + y + } + let my_arg = TestArg { x: 1a } + my_arg.add(1b) + " + ), + TypeCheckError::IncompatibleTypesInFunctionCall(..) + )); +} diff --git a/numbat/src/typed_ast.rs b/numbat/src/typed_ast.rs index 648d172e1..444793cf8 100644 --- a/numbat/src/typed_ast.rs +++ b/numbat/src/typed_ast.rs @@ -294,14 +294,54 @@ pub enum StructKind { Instance(Vec), } +/// Information about a method defined in an impl block #[derive(Debug, Clone, PartialEq, Eq)] +pub struct MethodInfo { + pub definition_span: Span, + pub name: CompactString, + /// Type parameters for the method itself (not the struct's type parameters) + pub type_parameters: Vec<(Span, CompactString, Option)>, + /// Parameter names (excluding self) + pub parameters: Vec<(Span, CompactString)>, + /// The function type including self as first parameter: Fn[(Self, P1, P2, ...) -> R] + pub fn_type: TypeScheme, +} + +/// A compiled method ready for bytecode generation +#[derive(Debug, Clone, PartialEq)] +pub struct CompiledMethod<'a> { + /// Mangled function name (e.g., "Point::magnitude") + pub mangled_name: CompactString, + /// Parameter names including self + pub parameters: Vec, + /// Local variables defined with "where" + pub local_variables: Vec>, + /// Method body expression (None for FFI methods) + pub body: Option>, +} + +#[derive(Debug, Clone)] pub struct StructInfo { pub definition_span: Span, pub name: CompactString, pub kind: StructKind, pub fields: IndexMap, + /// Methods defined on this struct via impl blocks + pub methods: IndexMap, } +// Custom PartialEq that excludes methods from comparison +// Two struct types are equal if they have the same name, kind, and fields +// regardless of methods, since methods are attached to the type definition +// not individual type instances +impl PartialEq for StructInfo { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.kind == other.kind && self.fields == other.fields + } +} + +impl Eq for StructInfo {} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Type { TVar(TypeVariable), @@ -534,6 +574,7 @@ impl Type { name: info.name.clone(), kind: instantiated_kind, fields: instantiated_fields, + methods: info.methods.clone(), })) } Type::List(element_type) => { @@ -635,6 +676,15 @@ pub enum Expression<'a> { ), List(Span, Vec>, TypeScheme), TypedHole(Span, TypeScheme), + MethodCall( + Span, // method span + Span, // full span + Box>, // receiver (typed) + &'a str, // method name + Vec>, // args (typed, excludes receiver) + TypeScheme, // receiver struct type + TypeScheme, // return type + ), } impl Expression<'_> { @@ -669,6 +719,7 @@ impl Expression<'_> { Expression::AccessField(_span, full_span, _, _, _, _) => *full_span, Expression::List(full_span, _, _) => *full_span, Expression::TypedHole(span, _) => *span, + Expression::MethodCall(_, full_span, _, _, _, _, _) => *full_span, } } } @@ -723,6 +774,13 @@ pub enum Statement<'a> { ), ProcedureCall(crate::ast::ProcedureKind, Span, Vec>), DefineStruct(StructInfo), + DefineImpl { + struct_name: CompactString, + /// Updated struct info with methods added + struct_info: StructInfo, + /// Compiled method bodies for bytecode generation + methods: Vec>, + }, } impl Statement<'_> { @@ -815,6 +873,7 @@ impl Statement<'_> { } Statement::ProcedureCall(_, _, _) => {} Statement::DefineStruct(_) => {} + Statement::DefineImpl { .. } => {} } } @@ -913,6 +972,9 @@ impl Expression<'_> { Type::List(Box::new(element_type.unsafe_as_concrete())) } Expression::TypedHole(_, type_) => type_.unsafe_as_concrete(), + Expression::MethodCall(_, _, _, _, _, _, return_type) => { + return_type.unsafe_as_concrete() + } } } @@ -944,6 +1006,7 @@ impl Expression<'_> { ), }, Expression::TypedHole(_, type_) => type_.clone(), + Expression::MethodCall(_, _, _, _, _, _, return_type) => return_type.clone(), } } } @@ -1259,6 +1322,17 @@ impl PrettyPrint for Statement<'_> { } + m::operator("}") } + Statement::DefineImpl { struct_name, .. } => { + m::keyword("impl") + + m::space() + + m::type_identifier(struct_name.to_compact_string()) + + m::space() + + m::operator("{") + + m::space() + + m::operator("...") + + m::space() + + m::operator("}") + } } } } @@ -1279,7 +1353,8 @@ fn with_parens(expr: &Expression) -> Markup { | Expression::InstantiateStruct(..) | Expression::AccessField(..) | Expression::List(..) - | Expression::TypedHole(_, _) => expr.pretty_print(), + | Expression::TypedHole(_, _) + | Expression::MethodCall(..) => expr.pretty_print(), Expression::UnaryOperator { .. } | Expression::BinaryOperator { .. } | Expression::BinaryOperatorForDate { .. } @@ -1493,6 +1568,18 @@ impl PrettyPrint for Expression<'_> { + m::operator("]") } TypedHole(_, _) => m::operator("?"), + MethodCall(_, _, receiver, method_name, args, _, _) => { + receiver.pretty_print() + + m::operator(".") + + m::identifier(method_name.to_compact_string()) + + m::operator("(") + + itertools::Itertools::intersperse( + args.iter().map(|e| e.pretty_print()), + m::operator(",") + m::space(), + ) + .sum() + + m::operator(")") + } } } } diff --git a/vscode-extension/syntaxes/numbat.tmLanguage.json b/vscode-extension/syntaxes/numbat.tmLanguage.json index 6eacb21e1..dd98891b4 100644 --- a/vscode-extension/syntaxes/numbat.tmLanguage.json +++ b/vscode-extension/syntaxes/numbat.tmLanguage.json @@ -32,7 +32,7 @@ "patterns": [ { "name": "keyword.control.numbat", - "match": "\\b(per|to|let|fn|where|and|dimension|unit|use|struct|long|short|both|none|if|then|else|true|false|print|assert|assert_eq|type)\\b" + "match": "\\b(per|to|let|fn|where|and|dimension|unit|use|struct|impl|self|long|short|both|none|if|then|else|true|false|print|assert|assert_eq|type)\\b" } ] },