Browse Source

Merge remote-tracking branch 'origin/master' into ValentinLeTallec/master

David Peter 1 year ago
parent
commit
ac53ad595e
66 changed files with 2218 additions and 550 deletions
  1. 1 0
      Cargo.lock
  2. 8 4
      book/build.py
  3. 5 1
      book/src/SUMMARY.md
  4. 50 0
      book/src/comparison.md
  5. 1 1
      book/src/date-and-time.md
  6. 136 0
      book/src/list-functions-datetime.md
  7. 229 0
      book/src/list-functions-lists.md
  8. 409 7
      book/src/list-functions-math.md
  9. 247 9
      book/src/list-functions-other.md
  10. 166 6
      book/src/list-functions-strings.md
  11. 31 16
      examples/tests/mixed_units.nbt
  12. 1 1
      numbat-cli/src/completer.rs
  13. 10 4
      numbat-cli/src/highlighter.rs
  14. 1 1
      numbat-cli/src/main.rs
  15. 1 0
      numbat/Cargo.toml
  16. 113 20
      numbat/examples/inspect.rs
  17. 3 1
      numbat/modules/chemistry/elements.nbt
  18. 25 4
      numbat/modules/core/functions.nbt
  19. 23 0
      numbat/modules/core/lists.nbt
  20. 11 21
      numbat/modules/core/mixed_units.nbt
  21. 6 0
      numbat/modules/core/numbers.nbt
  22. 2 0
      numbat/modules/core/quantities.nbt
  23. 22 6
      numbat/modules/core/strings.nbt
  24. 14 0
      numbat/modules/datetime/functions.nbt
  25. 1 0
      numbat/modules/datetime/human.nbt
  26. 1 0
      numbat/modules/extra/algebra.nbt
  27. 9 4
      numbat/modules/extra/color.nbt
  28. 1 0
      numbat/modules/math/constants.nbt
  29. 2 0
      numbat/modules/math/geometry.nbt
  30. 2 0
      numbat/modules/math/number_theory.nbt
  31. 9 3
      numbat/modules/math/statistics.nbt
  32. 5 0
      numbat/modules/math/transcendental.nbt
  33. 2 0
      numbat/modules/numerics/diff.nbt
  34. 1 0
      numbat/modules/numerics/fixed_point.nbt
  35. 2 0
      numbat/modules/numerics/solve.nbt
  36. 4 0
      numbat/modules/physics/temperature_conversion.nbt
  37. 18 8
      numbat/modules/units/mixed.nbt
  38. 2 2
      numbat/modules/units/si.nbt
  39. 23 18
      numbat/src/ast.rs
  40. 13 17
      numbat/src/bytecode_interpreter.rs
  41. 5 6
      numbat/src/column_formatter.rs
  42. 1 1
      numbat/src/datetime.rs
  43. 74 25
      numbat/src/decorator.rs
  44. 1 1
      numbat/src/help.rs
  45. 1 1
      numbat/src/interpreter/mod.rs
  46. 71 60
      numbat/src/lib.rs
  47. 35 32
      numbat/src/markup.rs
  48. 135 12
      numbat/src/parser.rs
  49. 41 24
      numbat/src/prefix_parser.rs
  50. 12 6
      numbat/src/prefix_transformer.rs
  51. 5 33
      numbat/src/product.rs
  52. 2 1
      numbat/src/quantity.rs
  53. 0 3
      numbat/src/registry.rs
  54. 1 1
      numbat/src/resolver.rs
  55. 22 14
      numbat/src/tokenizer.rs
  56. 2 2
      numbat/src/traversal.rs
  57. 2 4
      numbat/src/typechecker/constraints.rs
  58. 1 0
      numbat/src/typechecker/environment.rs
  59. 53 45
      numbat/src/typechecker/mod.rs
  60. 2 2
      numbat/src/typechecker/substitutions.rs
  61. 1 1
      numbat/src/typechecker/tests/mod.rs
  62. 2 2
      numbat/src/typechecker/type_scheme.rs
  63. 116 89
      numbat/src/typed_ast.rs
  64. 2 2
      numbat/src/unicode_input.rs
  65. 19 27
      numbat/src/unit.rs
  66. 2 2
      numbat/src/value.rs

+ 1 - 0
Cargo.lock

@@ -1089,6 +1089,7 @@ dependencies = [
  "num-traits",
  "num-traits",
  "numbat-exchange-rates",
  "numbat-exchange-rates",
  "once_cell",
  "once_cell",
+ "percent-encoding",
  "plotly",
  "plotly",
  "pretty_dtoa",
  "pretty_dtoa",
  "rand",
  "rand",

+ 8 - 4
book/build.py

@@ -1,6 +1,7 @@
 import subprocess
 import subprocess
 from pathlib import Path
 from pathlib import Path
 import urllib.parse
 import urllib.parse
+import os
 
 
 
 
 SCRIPT_DIR = Path(__file__).parent.resolve()
 SCRIPT_DIR = Path(__file__).parent.resolve()
@@ -15,14 +16,14 @@ def generate_example(
     print(path_out)
     print(path_out)
 
 
     code = []
     code = []
-    with open(path_in, "r") as fin:
+    with open(path_in, "r", encoding="utf-8") as fin:
         for line in fin:
         for line in fin:
             if not (strip_asserts and "assert_eq" in line):
             if not (strip_asserts and "assert_eq" in line):
                 code.append(line)
                 code.append(line)
 
 
     url = f"https://numbat.dev/?q={urllib.parse.quote_plus(''.join(code))}"
     url = f"https://numbat.dev/?q={urllib.parse.quote_plus(''.join(code))}"
 
 
-    with open(path_out, "w") as fout:
+    with open(path_out, "w", encoding="utf-8") as fout:
         fout.write("<!-- This file is autogenerated! Do not modify it -->\n")
         fout.write("<!-- This file is autogenerated! Do not modify it -->\n")
         fout.write("\n")
         fout.write("\n")
         fout.write(f"# {title}\n")
         fout.write(f"# {title}\n")
@@ -80,7 +81,7 @@ generate_example(
 )
 )
 
 
 path_units = SCRIPT_DIR / "src" / "list-units.md"
 path_units = SCRIPT_DIR / "src" / "list-units.md"
-with open(path_units, "w") as f:
+with open(path_units, "w", encoding="utf-8") as f:
     print("Generating list of units...", flush=True)
     print("Generating list of units...", flush=True)
     subprocess.run(
     subprocess.run(
         ["cargo", "run", "--release", "--quiet", "--example=inspect", "units"],
         ["cargo", "run", "--release", "--quiet", "--example=inspect", "units"],
@@ -91,7 +92,7 @@ with open(path_units, "w") as f:
 
 
 def list_of_functions(file_name, document):
 def list_of_functions(file_name, document):
     path = SCRIPT_DIR / "src" / f"list-functions-{file_name}.md"
     path = SCRIPT_DIR / "src" / f"list-functions-{file_name}.md"
-    with open(path, "w") as f:
+    with open(path, "w", encoding="utf-8") as f:
         print(f"# {document['title']}\n", file=f, flush=True)
         print(f"# {document['title']}\n", file=f, flush=True)
 
 
         if introduction := document.get("introduction"):
         if introduction := document.get("introduction"):
@@ -119,6 +120,8 @@ def list_of_functions(file_name, document):
                 print(
                 print(
                     f"Generating list of functions for module '{module}'...", flush=True
                     f"Generating list of functions for module '{module}'...", flush=True
                 )
                 )
+                env = os.environ.copy()
+                env["TZ"] = "UTC"
                 subprocess.run(
                 subprocess.run(
                     [
                     [
                         "cargo",
                         "cargo",
@@ -132,6 +135,7 @@ def list_of_functions(file_name, document):
                     ],
                     ],
                     stdout=f,
                     stdout=f,
                     text=True,
                     text=True,
+                    env=env,
                 )
                 )
 
 
 
 

+ 5 - 1
book/src/SUMMARY.md

@@ -21,7 +21,6 @@
     - [XKCD 687](./example-xkcd_687.md)
     - [XKCD 687](./example-xkcd_687.md)
     - [XKCD 2585](./example-xkcd_2585.md)
     - [XKCD 2585](./example-xkcd_2585.md)
     - [XKCD 2812](./example-xkcd_2812.md)
     - [XKCD 2812](./example-xkcd_2812.md)
-- [IDE / editor integration](./editor-integration.md)
 
 
 # Numbat language reference
 # Numbat language reference
 
 
@@ -69,6 +68,11 @@
 
 
 - [Type system](./type-system.md)
 - [Type system](./type-system.md)
 
 
+# Other topics
+
+- [IDE / editor integration](./editor-integration.md)
+- [Comparison with other tools](./comparison.md)
+
 # Support
 # Support
 
 
 - [Contact us](./contact-us.md)
 - [Contact us](./contact-us.md)

+ 50 - 0
book/src/comparison.md

@@ -0,0 +1,50 @@
+# Comparison with other tools
+
+The following table provides a comparison of Numbat with other scientific calculators and programming languages. This comparison
+is certainly *not* objective, as we only list criteria that we consider important. If you think that a tool or language is missing
+or misrepresented, please [let us know](https://github.com/sharkdp/numbat/issues).
+
+|                                        | Numbat          | [Qalculate](https://qalculate.github.io/) | [Kalker](https://github.com/PaddiM8/kalker) | [GNU Units](https://www.gnu.org/software/units/) | [Frink](https://frinklang.org/) | [Wolfram Alpha](https://www.wolframalpha.com/) |
+|----------------------------------------|-----------------|-----------|--------|-----------|-------|---------------|
+| FOSS License                           | MIT, Apache-2.0 | GPL-2.0   | MIT    | GPL-3.0   | ❌     | ❌             |
+| **Interfaces**                         |                 |           |        |           |       |               |
+| Command-line                           | ✓               | ✓         | ✓    | ✓         | ✓     | ✓             |
+| Web version                            | ✓               | ❌        | ✓     | ❌         | ❌     | ✓             |
+| Graphical                              | ❌              | ✓         | ❌    | ❌         | (✓)   | ✓             |
+| **Units**                              |                 |           |        |           |       |               |
+| Comprehensive list of units            | ✓               | ✓         | ❌    | ✓         | ✓     | ✓             |
+| Custom units                           | ✓               | ✓         | ❌    | ✓         | ✓     | ❌             |
+| Physical dimensions                    | ✓               | ❌        | ❌    | ❌         | ❌     | ❌             |
+| Currency conversions                   | ✓               | ✓         | ❌    | ❌         | ✓     | ✓             |
+| Date and time calculations             | ✓               | ✓         | ❌    | ❌         | ✓     | ✓             |
+| **Language features**                  |                 |           |        |           |       |               |
+| Custom functions                       | ✓               | ✓        | ✓     | ❌         | ✓     | ❌             |
+| Real programming language              | ✓               | ❌        | ❌     | ❌         | ✓     | ?             |
+| Strongly typed                         | ✓               | ❌        | ❌     | ❌         | ❌     | ❌             |
+| **Calculator features**                |                 |           |        |           |       |               |
+| Symbolic calculations                  | ❌               | (✓)        | ❌    | ❌         | (✓)     | ✓             |
+| Hex/Oct/Bin mode                       | ✓               | ✓         | ✓     | ✓         | ✓     | ✓             |
+| Complex numbers                        | ❌ ([#180](https://github.com/sharkdp/numbat/issues/180))  | ✓        | ✓     | ❌         | ✓     | ✓             |
+| Vectors, Matrices                      | ❌               | ✓        | ✓      | ❌         | ✓     | ✓             |
+
+## Detailed comparison
+
+- [Qalculate](https://qalculate.github.io/) is a fantastic calculator with a strong support for units and conversions.
+  If you don't need the full power of a programming language, Qalculate is probably more feature-complete than Numbat.
+- [Frink](https://frinklang.org/) is a special-purpose programming language with a focus on scientific calculations
+  and units of measurement. The language is probably more powerful than Numbat, but lacks a static type system. It's also
+  a imperative/OOP language, while Numbat is a functional/declarative language. Frink is not open-source.
+- [GNU Units](https://www.gnu.org/software/units/) is probably the most comprehensive tool in terms of pre-defined units.
+  Numbat makes it very easy to define [custom units](./unit-definitions.md). If you think that a unit should be part
+  of the standard library, please [let us know](https://github.com/sharkdp/numbat/issues).
+- [Wolfram Alpha](https://www.wolframalpha.com/) is a very powerful tool, but it's focused on single-line queries instead
+  of longer computations. The query language lacks a strict syntax (which some might consider a feature). The tool is not
+  open source and sometimes has limitations with respect to the number/size of queries you can make.
+
+## Other interesting tools / languages
+
+- [F#](https://fsharp.org/) is the only programming language that we know of that comes close in terms of having an
+  expressive type system that is based on units of measure. In fact, Numbats type system is heavily inspired by F#,
+  except that it uses physical dimensions instead of physical units on the type level. Both languages have feature
+  full [type inference](./function-definitions.md#type-inference). F# is not listed above, as it's not really suitable
+  as a scientific calculator.

+ 1 - 1
book/src/date-and-time.md

@@ -30,7 +30,7 @@ now() -> unixtime
 # What is the date corresponding to a given UNIX timestamp?
 # What is the date corresponding to a given UNIX timestamp?
 from_unixtime(1707568901)
 from_unixtime(1707568901)
 
 
-# How long are one million seconds in years, months, days, hours, minutes, seconds
+# How long are one million seconds in years, months, days, hours, minutes, seconds?
 1 million seconds -> human
 1 million seconds -> human
 ```
 ```
 
 

+ 136 - 0
book/src/list-functions-datetime.md

@@ -18,6 +18,26 @@ Parses a string (date and time) into a `DateTime` object. See [here](./date-and-
 fn datetime(input: String) -> DateTime
 fn datetime(input: String) -> DateTime
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=datetime%28%222022%2D07%2D20T21%3A52%2B0200%22%29')""></button></div><code class="language-nbt hljs numbat">>>> datetime("2022-07-20T21:52+0200")
+
+    = 2022-07-20 19:52:00 UTC    [DateTime]
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=datetime%28%222022%2D07%2D20%2021%3A52%20Europe%2FBerlin%22%29')""></button></div><code class="language-nbt hljs numbat">>>> datetime("2022-07-20 21:52 Europe/Berlin")
+
+    = 2022-07-20 21:52:00 CEST (UTC +02), Europe/Berlin    [DateTime]
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=datetime%28%222022%2F07%2F20%2009%3A52%20PM%20%2B0200%22%29')""></button></div><code class="language-nbt hljs numbat">>>> datetime("2022/07/20 09:52 PM +0200")
+
+    = 2022-07-20 21:52:00 (UTC +02)    [DateTime]
+</code></pre>
+
+</details>
+
 ### `format_datetime`
 ### `format_datetime`
 Formats a `DateTime` object as a string.
 Formats a `DateTime` object as a string.
 
 
@@ -25,6 +45,16 @@ Formats a `DateTime` object as a string.
 fn format_datetime(format: String, input: DateTime) -> String
 fn format_datetime(format: String, input: DateTime) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=format%5Fdatetime%28%22This%20is%20a%20date%20in%20%25B%20in%20the%20year%20%25Y%2E%22%2C%20datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%29')""></button></div><code class="language-nbt hljs numbat">>>> format_datetime("This is a date in %B in the year %Y.", datetime("2022-07-20 21:52 +0200"))
+
+    = "This is a date in July in the year 2022."    [String]
+</code></pre>
+
+</details>
+
 ### `get_local_timezone`
 ### `get_local_timezone`
 Returns the users local timezone.
 Returns the users local timezone.
 
 
@@ -32,6 +62,16 @@ Returns the users local timezone.
 fn get_local_timezone() -> String
 fn get_local_timezone() -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=get%5Flocal%5Ftimezone%28%29')""></button></div><code class="language-nbt hljs numbat">>>> get_local_timezone()
+
+    = "UTC"    [String]
+</code></pre>
+
+</details>
+
 ### `tz`
 ### `tz`
 Returns a timezone conversion function, typically used with the conversion operator.
 Returns a timezone conversion function, typically used with the conversion operator.
 
 
@@ -39,6 +79,21 @@ Returns a timezone conversion function, typically used with the conversion opera
 fn tz(tz: String) -> Fn[(DateTime) -> DateTime]
 fn tz(tz: String) -> Fn[(DateTime) -> DateTime]
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%20%2D%3E%20tz%28%22Europe%2FAmsterdam%22%29')""></button></div><code class="language-nbt hljs numbat">>>> datetime("2022-07-20 21:52 +0200") -> tz("Europe/Amsterdam")
+
+    = 2022-07-20 21:52:00 CEST (UTC +02), Europe/Amsterdam    [DateTime]
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%20%2D%3E%20tz%28%22Asia%2FTaipei%22%29')""></button></div><code class="language-nbt hljs numbat">>>> datetime("2022-07-20 21:52 +0200") -> tz("Asia/Taipei")
+
+    = 2022-07-21 03:52:00 CST (UTC +08), Asia/Taipei    [DateTime]
+</code></pre>
+
+</details>
+
 ### `unixtime`
 ### `unixtime`
 Converts a `DateTime` to a UNIX timestamp. Can be used on the right hand side of a conversion operator: `now() -> unixtime`.
 Converts a `DateTime` to a UNIX timestamp. Can be used on the right hand side of a conversion operator: `now() -> unixtime`.
 
 
@@ -46,6 +101,16 @@ Converts a `DateTime` to a UNIX timestamp. Can be used on the right hand side of
 fn unixtime(input: DateTime) -> Scalar
 fn unixtime(input: DateTime) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%20%2D%3E%20unixtime')""></button></div><code class="language-nbt hljs numbat">>>> datetime("2022-07-20 21:52 +0200") -> unixtime
+
+    = 1_658_346_720
+</code></pre>
+
+</details>
+
 ### `from_unixtime`
 ### `from_unixtime`
 Converts a UNIX timestamp to a `DateTime` object.
 Converts a UNIX timestamp to a `DateTime` object.
 
 
@@ -53,6 +118,16 @@ Converts a UNIX timestamp to a `DateTime` object.
 fn from_unixtime(input: Scalar) -> DateTime
 fn from_unixtime(input: Scalar) -> DateTime
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=from%5Funixtime%282%5E31%29')""></button></div><code class="language-nbt hljs numbat">>>> from_unixtime(2^31)
+
+    = 2038-01-19 03:14:08 UTC    [DateTime]
+</code></pre>
+
+</details>
+
 ### `today`
 ### `today`
 Returns the current date at midnight (in the local time).
 Returns the current date at midnight (in the local time).
 
 
@@ -67,6 +142,16 @@ Parses a string (only date) into a `DateTime` object.
 fn date(input: String) -> DateTime
 fn date(input: String) -> DateTime
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=date%28%222022%2D07%2D20%22%29')""></button></div><code class="language-nbt hljs numbat">>>> date("2022-07-20")
+
+    = 2022-07-20 00:00:00 UTC    [DateTime]
+</code></pre>
+
+</details>
+
 ### `time`
 ### `time`
 Parses a string (time only) into a `DateTime` object.
 Parses a string (time only) into a `DateTime` object.
 
 
@@ -81,6 +166,16 @@ Adds the given time span to a `DateTime`. This uses leap-year and DST-aware cale
 fn calendar_add(dt: DateTime, span: Time) -> DateTime
 fn calendar_add(dt: DateTime, span: Time) -> DateTime
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=calendar%5Fadd%28datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%2C%202%20years%29')""></button></div><code class="language-nbt hljs numbat">>>> calendar_add(datetime("2022-07-20 21:52 +0200"), 2 years)
+
+    = 2024-07-20 21:52:00 (UTC +02)    [DateTime]
+</code></pre>
+
+</details>
+
 ### `calendar_sub`
 ### `calendar_sub`
 Subtract the given time span from a `DateTime`. This uses leap-year and DST-aware calendar arithmetic with variable-length days, months, and years.
 Subtract the given time span from a `DateTime`. This uses leap-year and DST-aware calendar arithmetic with variable-length days, months, and years.
 
 
@@ -88,6 +183,16 @@ Subtract the given time span from a `DateTime`. This uses leap-year and DST-awar
 fn calendar_sub(dt: DateTime, span: Time) -> DateTime
 fn calendar_sub(dt: DateTime, span: Time) -> DateTime
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=calendar%5Fsub%28datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%2C%203%20years%29')""></button></div><code class="language-nbt hljs numbat">>>> calendar_sub(datetime("2022-07-20 21:52 +0200"), 3 years)
+
+    = 2019-07-20 21:52:00 (UTC +02)    [DateTime]
+</code></pre>
+
+</details>
+
 ### `weekday`
 ### `weekday`
 Get the day of the week from a given `DateTime`.
 Get the day of the week from a given `DateTime`.
 
 
@@ -95,6 +200,16 @@ Get the day of the week from a given `DateTime`.
 fn weekday(dt: DateTime) -> String
 fn weekday(dt: DateTime) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=weekday%28datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%29')""></button></div><code class="language-nbt hljs numbat">>>> weekday(datetime("2022-07-20 21:52 +0200"))
+
+    = "Wednesday"    [String]
+</code></pre>
+
+</details>
+
 ### `julian_date` (Julian date)
 ### `julian_date` (Julian date)
 Convert a `DateTime` to a Julian date, the number of days since the origin of the Julian date system (noon on November 24, 4714 BC in the proleptic Gregorian calendar).
 Convert a `DateTime` to a Julian date, the number of days since the origin of the Julian date system (noon on November 24, 4714 BC in the proleptic Gregorian calendar).
 More information [here](https://en.wikipedia.org/wiki/Julian_day).
 More information [here](https://en.wikipedia.org/wiki/Julian_day).
@@ -103,6 +218,16 @@ More information [here](https://en.wikipedia.org/wiki/Julian_day).
 fn julian_date(dt: DateTime) -> Time
 fn julian_date(dt: DateTime) -> Time
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=julian%5Fdate%28datetime%28%222022%2D07%2D20%2021%3A52%20%2B0200%22%29%29')""></button></div><code class="language-nbt hljs numbat">>>> julian_date(datetime("2022-07-20 21:52 +0200"))
+
+    = 2.45978e+6 day    [Time]
+</code></pre>
+
+</details>
+
 ### `human` (Human-readable time duration)
 ### `human` (Human-readable time duration)
 Converts a time duration to a human-readable string in days, hours, minutes and seconds.
 Converts a time duration to a human-readable string in days, hours, minutes and seconds.
 More information [here](https://numbat.dev/doc/date-and-time.html).
 More information [here](https://numbat.dev/doc/date-and-time.html).
@@ -111,3 +236,14 @@ More information [here](https://numbat.dev/doc/date-and-time.html).
 fn human(time: Time) -> String
 fn human(time: Time) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+How long is a microcentury?
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=century%2F1e6%20%2D%3E%20human')""></button></div><code class="language-nbt hljs numbat">>>> century/1e6 -> human
+
+    = "52 minutes + 35.693 seconds"    [String]
+</code></pre>
+
+</details>
+

+ 229 - 0
book/src/list-functions-lists.md

@@ -9,6 +9,16 @@ Get the length of a list.
 fn len<A>(xs: List<A>) -> Scalar
 fn len<A>(xs: List<A>) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=len%28%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> len([3, 2, 1])
+
+    = 3
+</code></pre>
+
+</details>
+
 ### `head`
 ### `head`
 Get the first element of a list. Yields a runtime error if the list is empty.
 Get the first element of a list. Yields a runtime error if the list is empty.
 
 
@@ -16,6 +26,16 @@ Get the first element of a list. Yields a runtime error if the list is empty.
 fn head<A>(xs: List<A>) -> A
 fn head<A>(xs: List<A>) -> A
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=head%28%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> head([3, 2, 1])
+
+    = 3
+</code></pre>
+
+</details>
+
 ### `tail`
 ### `tail`
 Get everything but the first element of a list. Yields a runtime error if the list is empty.
 Get everything but the first element of a list. Yields a runtime error if the list is empty.
 
 
@@ -23,6 +43,16 @@ Get everything but the first element of a list. Yields a runtime error if the li
 fn tail<A>(xs: List<A>) -> List<A>
 fn tail<A>(xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=tail%28%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> tail([3, 2, 1])
+
+    = [2, 1]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `cons`
 ### `cons`
 Prepend an element to a list.
 Prepend an element to a list.
 
 
@@ -30,6 +60,16 @@ Prepend an element to a list.
 fn cons<A>(x: A, xs: List<A>) -> List<A>
 fn cons<A>(x: A, xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=cons%2877%2C%20%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> cons(77, [3, 2, 1])
+
+    = [77, 3, 2, 1]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `cons_end`
 ### `cons_end`
 Append an element to the end of a list.
 Append an element to the end of a list.
 
 
@@ -37,6 +77,16 @@ Append an element to the end of a list.
 fn cons_end<A>(x: A, xs: List<A>) -> List<A>
 fn cons_end<A>(x: A, xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=cons%5Fend%2877%2C%20%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> cons_end(77, [3, 2, 1])
+
+    = [3, 2, 1, 77]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `is_empty`
 ### `is_empty`
 Check if a list is empty.
 Check if a list is empty.
 
 
@@ -44,6 +94,21 @@ Check if a list is empty.
 fn is_empty<A>(xs: List<A>) -> Bool
 fn is_empty<A>(xs: List<A>) -> Bool
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Fempty%28%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> is_empty([3, 2, 1])
+
+    = false    [Bool]
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Fempty%28%5B%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> is_empty([])
+
+    = true    [Bool]
+</code></pre>
+
+</details>
+
 ### `concat`
 ### `concat`
 Concatenate two lists.
 Concatenate two lists.
 
 
@@ -51,6 +116,16 @@ Concatenate two lists.
 fn concat<A>(xs1: List<A>, xs2: List<A>) -> List<A>
 fn concat<A>(xs1: List<A>, xs2: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=concat%28%5B3%2C%202%2C%201%5D%2C%20%5B10%2C%2011%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> concat([3, 2, 1], [10, 11])
+
+    = [3, 2, 1, 10, 11]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `take`
 ### `take`
 Get the first `n` elements of a list.
 Get the first `n` elements of a list.
 
 
@@ -58,6 +133,16 @@ Get the first `n` elements of a list.
 fn take<A>(n: Scalar, xs: List<A>) -> List<A>
 fn take<A>(n: Scalar, xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=take%282%2C%20%5B3%2C%202%2C%201%2C%200%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> take(2, [3, 2, 1, 0])
+
+    = [3, 2]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `drop`
 ### `drop`
 Get everything but the first `n` elements of a list.
 Get everything but the first `n` elements of a list.
 
 
@@ -65,6 +150,16 @@ Get everything but the first `n` elements of a list.
 fn drop<A>(n: Scalar, xs: List<A>) -> List<A>
 fn drop<A>(n: Scalar, xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=drop%282%2C%20%5B3%2C%202%2C%201%2C%200%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> drop(2, [3, 2, 1, 0])
+
+    = [1, 0]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `element_at`
 ### `element_at`
 Get the element at index `i` in a list.
 Get the element at index `i` in a list.
 
 
@@ -72,6 +167,16 @@ Get the element at index `i` in a list.
 fn element_at<A>(i: Scalar, xs: List<A>) -> A
 fn element_at<A>(i: Scalar, xs: List<A>) -> A
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=element%5Fat%282%2C%20%5B3%2C%202%2C%201%2C%200%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> element_at(2, [3, 2, 1, 0])
+
+    = 1
+</code></pre>
+
+</details>
+
 ### `range`
 ### `range`
 Generate a range of integer numbers from `start` to `end` (inclusive).
 Generate a range of integer numbers from `start` to `end` (inclusive).
 
 
@@ -79,6 +184,16 @@ Generate a range of integer numbers from `start` to `end` (inclusive).
 fn range(start: Scalar, end: Scalar) -> List<Scalar>
 fn range(start: Scalar, end: Scalar) -> List<Scalar>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=range%282%2C%2012%29')""></button></div><code class="language-nbt hljs numbat">>>> range(2, 12)
+
+    = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `reverse`
 ### `reverse`
 Reverse the order of a list.
 Reverse the order of a list.
 
 
@@ -86,6 +201,16 @@ Reverse the order of a list.
 fn reverse<A>(xs: List<A>) -> List<A>
 fn reverse<A>(xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=reverse%28%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> reverse([3, 2, 1])
+
+    = [1, 2, 3]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `map`
 ### `map`
 Generate a new list by applying a function to each element of the input list.
 Generate a new list by applying a function to each element of the input list.
 
 
@@ -93,6 +218,17 @@ Generate a new list by applying a function to each element of the input list.
 fn map<A, B>(f: Fn[(A) -> B], xs: List<A>) -> List<B>
 fn map<A, B>(f: Fn[(A) -> B], xs: List<A>) -> List<B>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Square all elements of a list.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=map%28sqr%2C%20%5B3%2C%202%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> map(sqr, [3, 2, 1])
+
+    = [9, 4, 1]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `filter`
 ### `filter`
 Filter a list by a predicate.
 Filter a list by a predicate.
 
 
@@ -100,6 +236,16 @@ Filter a list by a predicate.
 fn filter<A>(p: Fn[(A) -> Bool], xs: List<A>) -> List<A>
 fn filter<A>(p: Fn[(A) -> Bool], xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=filter%28is%5Ffinite%2C%20%5B0%2C%201e10%2C%20NaN%2C%20%2Dinf%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> filter(is_finite, [0, 1e10, NaN, -inf])
+
+    = [0, 10_000_000_000]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `foldl`
 ### `foldl`
 Fold a function over a list.
 Fold a function over a list.
 
 
@@ -107,6 +253,17 @@ Fold a function over a list.
 fn foldl<A, B>(f: Fn[(A, B) -> A], acc: A, xs: List<B>) -> A
 fn foldl<A, B>(f: Fn[(A, B) -> A], acc: A, xs: List<B>) -> A
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Join a list of strings by folding.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=foldl%28str%5Fappend%2C%20%22%22%2C%20%5B%22Num%22%2C%20%22bat%22%2C%20%22%21%22%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> foldl(str_append, "", ["Num", "bat", "!"])
+
+    = "Numbat!"    [String]
+</code></pre>
+
+</details>
+
 ### `sort_by_key`
 ### `sort_by_key`
 Sort a list of elements, using the given key function that maps the element to a quantity.
 Sort a list of elements, using the given key function that maps the element to a quantity.
 
 
@@ -114,6 +271,18 @@ Sort a list of elements, using the given key function that maps the element to a
 fn sort_by_key<A, D: Dim>(key: Fn[(A) -> D], xs: List<A>) -> List<A>
 fn sort_by_key<A, D: Dim>(key: Fn[(A) -> D], xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Sort by last digit.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=fn%20last%5Fdigit%28x%29%20%3D%20mod%28x%2C%2010%29%0Asort%5Fby%5Fkey%28last%5Fdigit%2C%20%5B701%2C%20313%2C%209999%2C%204%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> fn last_digit(x) = mod(x, 10)
+sort_by_key(last_digit, [701, 313, 9999, 4])
+
+    = [701, 313, 4, 9999]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `sort`
 ### `sort`
 Sort a list of quantities.
 Sort a list of quantities.
 
 
@@ -121,6 +290,16 @@ Sort a list of quantities.
 fn sort<D: Dim>(xs: List<D>) -> List<D>
 fn sort<D: Dim>(xs: List<D>) -> List<D>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=sort%28%5B3%2C%202%2C%207%2C%208%2C%20%2D4%2C%200%2C%20%2D5%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> sort([3, 2, 7, 8, -4, 0, -5])
+
+    = [-5, -4, 0, 2, 3, 7, 8]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `intersperse`
 ### `intersperse`
 Add an element between each pair of elements in a list.
 Add an element between each pair of elements in a list.
 
 
@@ -128,6 +307,16 @@ Add an element between each pair of elements in a list.
 fn intersperse<A>(sep: A, xs: List<A>) -> List<A>
 fn intersperse<A>(sep: A, xs: List<A>) -> List<A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=intersperse%280%2C%20%5B1%2C%201%2C%201%2C%201%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> intersperse(0, [1, 1, 1, 1])
+
+    = [1, 0, 1, 0, 1, 0, 1]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `sum`
 ### `sum`
 Sum all elements of a list.
 Sum all elements of a list.
 
 
@@ -135,6 +324,16 @@ Sum all elements of a list.
 fn sum<D: Dim>(xs: List<D>) -> D
 fn sum<D: Dim>(xs: List<D>) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=sum%28%5B3%20m%2C%20200%20cm%2C%201000%20mm%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> sum([3 m, 200 cm, 1000 mm])
+
+    = 6 m    [Length]
+</code></pre>
+
+</details>
+
 ### `linspace`
 ### `linspace`
 Generate a list of `n_steps` evenly spaced numbers from `start` to `end` (inclusive).
 Generate a list of `n_steps` evenly spaced numbers from `start` to `end` (inclusive).
 
 
@@ -142,6 +341,16 @@ Generate a list of `n_steps` evenly spaced numbers from `start` to `end` (inclus
 fn linspace<D: Dim>(start: D, end: D, n_steps: Scalar) -> List<D>
 fn linspace<D: Dim>(start: D, end: D, n_steps: Scalar) -> List<D>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=linspace%28%2D5%20m%2C%205%20m%2C%2011%29')""></button></div><code class="language-nbt hljs numbat">>>> linspace(-5 m, 5 m, 11)
+
+    = [-5 m, -4 m, -3 m, -2 m, -1 m, 0 m, 1 m, 2 m, 3 m, 4 m, 5 m]    [List<Length>]
+</code></pre>
+
+</details>
+
 ### `join`
 ### `join`
 Convert a list of strings into a single string by concatenating them with a separator.
 Convert a list of strings into a single string by concatenating them with a separator.
 
 
@@ -149,6 +358,16 @@ Convert a list of strings into a single string by concatenating them with a sepa
 fn join(xs: List<String>, sep: String) -> String
 fn join(xs: List<String>, sep: String) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=join%28%5B%22snake%22%2C%20%22case%22%5D%2C%20%22%5F%22%29')""></button></div><code class="language-nbt hljs numbat">>>> join(["snake", "case"], "_")
+
+    = "snake_case"    [String]
+</code></pre>
+
+</details>
+
 ### `split`
 ### `split`
 Split a string into a list of strings using a separator.
 Split a string into a list of strings using a separator.
 
 
@@ -156,3 +375,13 @@ Split a string into a list of strings using a separator.
 fn split(input: String, separator: String) -> List<String>
 fn split(input: String, separator: String) -> List<String>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=split%28%22Numbat%20is%20a%20statically%20typed%20programming%20language%2E%22%2C%20%22%20%22%29')""></button></div><code class="language-nbt hljs numbat">>>> split("Numbat is a statically typed programming language.", " ")
+
+    = ["Numbat", "is", "a", "statically", "typed", "programming", "language."]    [List<String>]
+</code></pre>
+
+</details>
+

+ 409 - 7
book/src/list-functions-math.md

@@ -13,6 +13,16 @@ Return the input value.
 fn id<A>(x: A) -> A
 fn id<A>(x: A) -> A
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=id%288%20kg%29')""></button></div><code class="language-nbt hljs numbat">>>> id(8 kg)
+
+    = 8 kg    [Mass]
+</code></pre>
+
+</details>
+
 ### `abs` (Absolute value)
 ### `abs` (Absolute value)
 Return the absolute value \\( |x| \\) of the input. This works for quantities, too: `abs(-5 m) = 5 m`.
 Return the absolute value \\( |x| \\) of the input. This works for quantities, too: `abs(-5 m) = 5 m`.
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.abs).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.abs).
@@ -21,6 +31,16 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn abs<T: Dim>(x: T) -> T
 fn abs<T: Dim>(x: T) -> T
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=abs%28%2D22%2E2%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> abs(-22.2 m)
+
+    = 22.2 m    [Length]
+</code></pre>
+
+</details>
+
 ### `sqrt` (Square root)
 ### `sqrt` (Square root)
 Return the square root \\( \sqrt{x} \\) of the input: `sqrt(121 m^2) = 11 m`.
 Return the square root \\( \sqrt{x} \\) of the input: `sqrt(121 m^2) = 11 m`.
 More information [here](https://en.wikipedia.org/wiki/Square_root).
 More information [here](https://en.wikipedia.org/wiki/Square_root).
@@ -29,6 +49,16 @@ More information [here](https://en.wikipedia.org/wiki/Square_root).
 fn sqrt<D: Dim>(x: D^2) -> D
 fn sqrt<D: Dim>(x: D^2) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=sqrt%284%20are%29%20%2D%3E%20m')""></button></div><code class="language-nbt hljs numbat">>>> sqrt(4 are) -> m
+
+    = 20 m    [Length]
+</code></pre>
+
+</details>
+
 ### `cbrt` (Cube root)
 ### `cbrt` (Cube root)
 Return the cube root \\( \sqrt[3]{x} \\) of the input: `cbrt(8 m^3) = 2 m`.
 Return the cube root \\( \sqrt[3]{x} \\) of the input: `cbrt(8 m^3) = 2 m`.
 More information [here](https://en.wikipedia.org/wiki/Cube_root).
 More information [here](https://en.wikipedia.org/wiki/Cube_root).
@@ -37,6 +67,16 @@ More information [here](https://en.wikipedia.org/wiki/Cube_root).
 fn cbrt<D: Dim>(x: D^3) -> D
 fn cbrt<D: Dim>(x: D^3) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=cbrt%288%20L%29%20%2D%3E%20cm')""></button></div><code class="language-nbt hljs numbat">>>> cbrt(8 L) -> cm
+
+    = 20.0 cm    [Length]
+</code></pre>
+
+</details>
+
 ### `sqr` (Square function)
 ### `sqr` (Square function)
 Return the square of the input, \\( x^2 \\): `sqr(5 m) = 25 m^2`.
 Return the square of the input, \\( x^2 \\): `sqr(5 m) = 25 m^2`.
 
 
@@ -44,6 +84,16 @@ Return the square of the input, \\( x^2 \\): `sqr(5 m) = 25 m^2`.
 fn sqr<D: Dim>(x: D) -> D^2
 fn sqr<D: Dim>(x: D) -> D^2
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=sqr%287%29')""></button></div><code class="language-nbt hljs numbat">>>> sqr(7)
+
+    = 49
+</code></pre>
+
+</details>
+
 ### `round` (Rounding)
 ### `round` (Rounding)
 Round to the nearest integer. If the value is half-way between two integers, round away from \\( 0 \\). See also: `round_in`.
 Round to the nearest integer. If the value is half-way between two integers, round away from \\( 0 \\). See also: `round_in`.
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.round).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.round).
@@ -52,13 +102,45 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn round(x: Scalar) -> Scalar
 fn round(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=round%285%2E5%29')""></button></div><code class="language-nbt hljs numbat">>>> round(5.5)
+
+    = 6
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=round%28%2D5%2E5%29')""></button></div><code class="language-nbt hljs numbat">>>> round(-5.5)
+
+    = -6
+</code></pre>
+
+</details>
+
 ### `round_in` (Rounding)
 ### `round_in` (Rounding)
-Round to the nearest multiple of `base`. For example: `round_in(m, 5.3 m) == 5 m`.
+Round to the nearest multiple of `base`.
 
 
 ```nbt
 ```nbt
 fn round_in<D: Dim>(base: D, value: D) -> D
 fn round_in<D: Dim>(base: D, value: D) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Round in meters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=round%5Fin%28m%2C%205%2E3%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> round_in(m, 5.3 m)
+
+    = 5 m    [Length]
+</code></pre>
+
+Round in centimeters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=round%5Fin%28cm%2C%205%2E3%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> round_in(cm, 5.3 m)
+
+    = 530 cm    [Length]
+</code></pre>
+
+</details>
+
 ### `floor` (Floor function)
 ### `floor` (Floor function)
 Returns the largest integer less than or equal to \\( x \\). See also: `floor_in`.
 Returns the largest integer less than or equal to \\( x \\). See also: `floor_in`.
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.floor).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.floor).
@@ -67,13 +149,40 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn floor(x: Scalar) -> Scalar
 fn floor(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=floor%285%2E5%29')""></button></div><code class="language-nbt hljs numbat">>>> floor(5.5)
+
+    = 5
+</code></pre>
+
+</details>
+
 ### `floor_in` (Floor function)
 ### `floor_in` (Floor function)
-Returns the largest integer multiple of `base` less than or equal to `value`. For example: `floor_in(m, 5.7 m) == 5 m`.
+Returns the largest integer multiple of `base` less than or equal to `value`.
 
 
 ```nbt
 ```nbt
 fn floor_in<D: Dim>(base: D, value: D) -> D
 fn floor_in<D: Dim>(base: D, value: D) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Floor in meters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=floor%5Fin%28m%2C%205%2E7%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> floor_in(m, 5.7 m)
+
+    = 5 m    [Length]
+</code></pre>
+
+Floor in centimeters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=floor%5Fin%28cm%2C%205%2E7%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> floor_in(cm, 5.7 m)
+
+    = 570 cm    [Length]
+</code></pre>
+
+</details>
+
 ### `ceil` (Ceil function)
 ### `ceil` (Ceil function)
 Returns the smallest integer greater than or equal to \\( x \\). See also: `ceil_in`.
 Returns the smallest integer greater than or equal to \\( x \\). See also: `ceil_in`.
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.ceil).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.ceil).
@@ -82,13 +191,40 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn ceil(x: Scalar) -> Scalar
 fn ceil(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=ceil%285%2E5%29')""></button></div><code class="language-nbt hljs numbat">>>> ceil(5.5)
+
+    = 6
+</code></pre>
+
+</details>
+
 ### `ceil_in` (Ceil function)
 ### `ceil_in` (Ceil function)
-Returns the smallest integer multuple of `base` greater than or equal to `value`. For example: `ceil_in(m, 5.3 m) == 6 m`.
+Returns the smallest integer multiple of `base` greater than or equal to `value`.
 
 
 ```nbt
 ```nbt
 fn ceil_in<D: Dim>(base: D, value: D) -> D
 fn ceil_in<D: Dim>(base: D, value: D) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Ceil in meters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=ceil%5Fin%28m%2C%205%2E3%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> ceil_in(m, 5.3 m)
+
+    = 6 m    [Length]
+</code></pre>
+
+Ceil in centimeters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=ceil%5Fin%28cm%2C%205%2E3%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> ceil_in(cm, 5.3 m)
+
+    = 530 cm    [Length]
+</code></pre>
+
+</details>
+
 ### `trunc` (Truncation)
 ### `trunc` (Truncation)
 Returns the integer part of \\( x \\). Non-integer numbers are always truncated towards zero. See also: `trunc_in`.
 Returns the integer part of \\( x \\). Non-integer numbers are always truncated towards zero. See also: `trunc_in`.
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.trunc).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.trunc).
@@ -97,13 +233,45 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn trunc(x: Scalar) -> Scalar
 fn trunc(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=trunc%285%2E5%29')""></button></div><code class="language-nbt hljs numbat">>>> trunc(5.5)
+
+    = 5
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=trunc%28%2D5%2E5%29')""></button></div><code class="language-nbt hljs numbat">>>> trunc(-5.5)
+
+    = -5
+</code></pre>
+
+</details>
+
 ### `trunc_in` (Truncation)
 ### `trunc_in` (Truncation)
-Truncates to an integer multiple of `base` (towards zero). For example: `trunc_in(m, -5.7 m) == -5 m`.
+Truncates to an integer multiple of `base` (towards zero).
 
 
 ```nbt
 ```nbt
 fn trunc_in<D: Dim>(base: D, value: D) -> D
 fn trunc_in<D: Dim>(base: D, value: D) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Truncate in meters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=trunc%5Fin%28m%2C%205%2E7%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> trunc_in(m, 5.7 m)
+
+    = 5 m    [Length]
+</code></pre>
+
+Truncate in centimeters.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=trunc%5Fin%28cm%2C%205%2E7%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> trunc_in(cm, 5.7 m)
+
+    = 570 cm    [Length]
+</code></pre>
+
+</details>
+
 ### `mod` (Modulo)
 ### `mod` (Modulo)
 Calculates the least nonnegative remainder of \\( a (\mod b) \\).
 Calculates the least nonnegative remainder of \\( a (\mod b) \\).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.rem_euclid).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.rem_euclid).
@@ -112,6 +280,16 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn mod<T: Dim>(a: T, b: T) -> T
 fn mod<T: Dim>(a: T, b: T) -> T
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=mod%2827%2C%205%29')""></button></div><code class="language-nbt hljs numbat">>>> mod(27, 5)
+
+    = 2
+</code></pre>
+
+</details>
+
 ## Transcendental functions
 ## Transcendental functions
 
 
 Defined in: `math::transcendental`
 Defined in: `math::transcendental`
@@ -124,6 +302,16 @@ More information [here](https://en.wikipedia.org/wiki/Exponential_function).
 fn exp(x: Scalar) -> Scalar
 fn exp(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=exp%284%29')""></button></div><code class="language-nbt hljs numbat">>>> exp(4)
+
+    = 54.5982
+</code></pre>
+
+</details>
+
 ### `ln` (Natural logarithm)
 ### `ln` (Natural logarithm)
 The natural logarithm with base \\( e \\).
 The natural logarithm with base \\( e \\).
 More information [here](https://en.wikipedia.org/wiki/Natural_logarithm).
 More information [here](https://en.wikipedia.org/wiki/Natural_logarithm).
@@ -132,6 +320,16 @@ More information [here](https://en.wikipedia.org/wiki/Natural_logarithm).
 fn ln(x: Scalar) -> Scalar
 fn ln(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=ln%2820%29')""></button></div><code class="language-nbt hljs numbat">>>> ln(20)
+
+    = 2.99573
+</code></pre>
+
+</details>
+
 ### `log` (Natural logarithm)
 ### `log` (Natural logarithm)
 The natural logarithm with base \\( e \\).
 The natural logarithm with base \\( e \\).
 More information [here](https://en.wikipedia.org/wiki/Natural_logarithm).
 More information [here](https://en.wikipedia.org/wiki/Natural_logarithm).
@@ -140,6 +338,16 @@ More information [here](https://en.wikipedia.org/wiki/Natural_logarithm).
 fn log(x: Scalar) -> Scalar
 fn log(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=log%2820%29')""></button></div><code class="language-nbt hljs numbat">>>> log(20)
+
+    = 2.99573
+</code></pre>
+
+</details>
+
 ### `log10` (Common logarithm)
 ### `log10` (Common logarithm)
 The common logarithm with base \\( 10 \\).
 The common logarithm with base \\( 10 \\).
 More information [here](https://en.wikipedia.org/wiki/Common_logarithm).
 More information [here](https://en.wikipedia.org/wiki/Common_logarithm).
@@ -148,6 +356,16 @@ More information [here](https://en.wikipedia.org/wiki/Common_logarithm).
 fn log10(x: Scalar) -> Scalar
 fn log10(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=log10%28100%29')""></button></div><code class="language-nbt hljs numbat">>>> log10(100)
+
+    = 2
+</code></pre>
+
+</details>
+
 ### `log2` (Binary logarithm)
 ### `log2` (Binary logarithm)
 The binary logarithm with base \\( 2 \\).
 The binary logarithm with base \\( 2 \\).
 More information [here](https://en.wikipedia.org/wiki/Binary_logarithm).
 More information [here](https://en.wikipedia.org/wiki/Binary_logarithm).
@@ -156,6 +374,16 @@ More information [here](https://en.wikipedia.org/wiki/Binary_logarithm).
 fn log2(x: Scalar) -> Scalar
 fn log2(x: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=log2%28256%29')""></button></div><code class="language-nbt hljs numbat">>>> log2(256)
+
+    = 8
+</code></pre>
+
+</details>
+
 ### `gamma` (Gamma function)
 ### `gamma` (Gamma function)
 The gamma function, \\( \Gamma(x) \\).
 The gamma function, \\( \Gamma(x) \\).
 More information [here](https://en.wikipedia.org/wiki/Gamma_function).
 More information [here](https://en.wikipedia.org/wiki/Gamma_function).
@@ -264,27 +492,57 @@ fn atanh(x: Scalar) -> Scalar
 Defined in: `math::statistics`
 Defined in: `math::statistics`
 
 
 ### `maximum` (Maxmimum)
 ### `maximum` (Maxmimum)
-Get the largest element of a list: `maximum([30 cm, 2 m]) = 2 m`.
+Get the largest element of a list.
 
 
 ```nbt
 ```nbt
 fn maximum<D: Dim>(xs: List<D>) -> D
 fn maximum<D: Dim>(xs: List<D>) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=maximum%28%5B30%20cm%2C%202%20m%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> maximum([30 cm, 2 m])
+
+    = 2 m    [Length]
+</code></pre>
+
+</details>
+
 ### `minimum` (Minimum)
 ### `minimum` (Minimum)
-Get the smallest element of a list: `minimum([30 cm, 2 m]) = 30 cm`.
+Get the smallest element of a list.
 
 
 ```nbt
 ```nbt
 fn minimum<D: Dim>(xs: List<D>) -> D
 fn minimum<D: Dim>(xs: List<D>) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=minimum%28%5B30%20cm%2C%202%20m%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> minimum([30 cm, 2 m])
+
+    = 30 cm    [Length]
+</code></pre>
+
+</details>
+
 ### `mean` (Arithmetic mean)
 ### `mean` (Arithmetic mean)
-Calculate the arithmetic mean of a list of quantities: `mean([1 m, 2 m, 300 cm]) = 2 m`.
+Calculate the arithmetic mean of a list of quantities.
 More information [here](https://en.wikipedia.org/wiki/Arithmetic_mean).
 More information [here](https://en.wikipedia.org/wiki/Arithmetic_mean).
 
 
 ```nbt
 ```nbt
 fn mean<D: Dim>(xs: List<D>) -> D
 fn mean<D: Dim>(xs: List<D>) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=mean%28%5B1%20m%2C%202%20m%2C%20300%20cm%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> mean([1 m, 2 m, 300 cm])
+
+    = 2 m    [Length]
+</code></pre>
+
+</details>
+
 ### `variance` (Variance)
 ### `variance` (Variance)
 Calculate the population variance of a list of quantities.
 Calculate the population variance of a list of quantities.
 More information [here](https://en.wikipedia.org/wiki/Variance).
 More information [here](https://en.wikipedia.org/wiki/Variance).
@@ -293,6 +551,16 @@ More information [here](https://en.wikipedia.org/wiki/Variance).
 fn variance<D: Dim>(xs: List<D>) -> D^2
 fn variance<D: Dim>(xs: List<D>) -> D^2
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=variance%28%5B1%20m%2C%202%20m%2C%20300%20cm%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> variance([1 m, 2 m, 300 cm])
+
+    = 0.666667 m²    [Area]
+</code></pre>
+
+</details>
+
 ### `stdev` (Standard deviation)
 ### `stdev` (Standard deviation)
 Calculate the population standard deviation of a list of quantities.
 Calculate the population standard deviation of a list of quantities.
 More information [here](https://en.wikipedia.org/wiki/Standard_deviation).
 More information [here](https://en.wikipedia.org/wiki/Standard_deviation).
@@ -301,6 +569,16 @@ More information [here](https://en.wikipedia.org/wiki/Standard_deviation).
 fn stdev<D: Dim>(xs: List<D>) -> D
 fn stdev<D: Dim>(xs: List<D>) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=stdev%28%5B1%20m%2C%202%20m%2C%20300%20cm%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> stdev([1 m, 2 m, 300 cm])
+
+    = 0.816497 m    [Length]
+</code></pre>
+
+</details>
+
 ### `median` (Median)
 ### `median` (Median)
 Calculate the median of a list of quantities.
 Calculate the median of a list of quantities.
 More information [here](https://en.wikipedia.org/wiki/Median).
 More information [here](https://en.wikipedia.org/wiki/Median).
@@ -309,6 +587,16 @@ More information [here](https://en.wikipedia.org/wiki/Median).
 fn median<D: Dim>(xs: List<D>) -> D
 fn median<D: Dim>(xs: List<D>) -> D
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=median%28%5B1%20m%2C%202%20m%2C%20400%20cm%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> median([1 m, 2 m, 400 cm])
+
+    = 2 m    [Length]
+</code></pre>
+
+</details>
+
 ## Random sampling, distributions
 ## Random sampling, distributions
 
 
 Defined in: `core::random`, `math::distributions`
 Defined in: `core::random`, `math::distributions`
@@ -413,6 +701,16 @@ More information [here](https://en.wikipedia.org/wiki/Greatest_common_divisor).
 fn gcd(a: Scalar, b: Scalar) -> Scalar
 fn gcd(a: Scalar, b: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=gcd%2860%2C%2042%29')""></button></div><code class="language-nbt hljs numbat">>>> gcd(60, 42)
+
+    = 6
+</code></pre>
+
+</details>
+
 ### `lcm` (Least common multiple)
 ### `lcm` (Least common multiple)
 The smallest positive integer that is divisible by both \\( a \\) and \\( b \\).
 The smallest positive integer that is divisible by both \\( a \\) and \\( b \\).
 More information [here](https://en.wikipedia.org/wiki/Least_common_multiple).
 More information [here](https://en.wikipedia.org/wiki/Least_common_multiple).
@@ -421,6 +719,16 @@ More information [here](https://en.wikipedia.org/wiki/Least_common_multiple).
 fn lcm(a: Scalar, b: Scalar) -> Scalar
 fn lcm(a: Scalar, b: Scalar) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=lcm%2814%2C%204%29')""></button></div><code class="language-nbt hljs numbat">>>> lcm(14, 4)
+
+    = 28
+</code></pre>
+
+</details>
+
 ## Numerical methods
 ## Numerical methods
 
 
 Defined in: `numerics::diff`, `numerics::solve`, `numerics::fixed_point`
 Defined in: `numerics::diff`, `numerics::solve`, `numerics::fixed_point`
@@ -433,6 +741,28 @@ More information [here](https://en.wikipedia.org/wiki/Numerical_differentiation)
 fn diff<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x: X) -> Y / X
 fn diff<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x: X) -> Y / X
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Compute the derivative of \\( f(x) = x² -x -1 \\) at \\( x=1 \\).
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20numerics%3A%3Adiff%0Afn%20polynomial%28x%29%20%3D%20x%C2%B2%20%2D%20x%20%2D%201%0Adiff%28polynomial%2C%201%29')""></button></div><code class="language-nbt hljs numbat">>>> use numerics::diff
+fn polynomial(x) = x² - x - 1
+diff(polynomial, 1)
+
+    = 1.0
+</code></pre>
+
+Compute the free fall velocity after \\( t=2 s \\).
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20numerics%3A%3Adiff%0Afn%20distance%28t%29%20%3D%200%2E5%20g0%20t%C2%B2%0Afn%20velocity%28t%29%20%3D%20diff%28distance%2C%20t%29%0Avelocity%282%20s%29')""></button></div><code class="language-nbt hljs numbat">>>> use numerics::diff
+fn distance(t) = 0.5 g0 t²
+fn velocity(t) = diff(distance, t)
+velocity(2 s)
+
+    = 19.6133 m/s    [Velocity]
+</code></pre>
+
+</details>
+
 ### `root_bisect` (Bisection method)
 ### `root_bisect` (Bisection method)
 Find the root of the function \\( f \\) in the interval \\( [x_1, x_2] \\) using the bisection method. The function \\( f \\) must be continuous and \\( f(x_1) \cdot f(x_2) < 0 \\).
 Find the root of the function \\( f \\) in the interval \\( [x_1, x_2] \\) using the bisection method. The function \\( f \\) must be continuous and \\( f(x_1) \cdot f(x_2) < 0 \\).
 More information [here](https://en.wikipedia.org/wiki/Bisection_method).
 More information [here](https://en.wikipedia.org/wiki/Bisection_method).
@@ -441,6 +771,19 @@ More information [here](https://en.wikipedia.org/wiki/Bisection_method).
 fn root_bisect<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x1: X, x2: X, x_tol: X, y_tol: Y) -> X
 fn root_bisect<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x1: X, x2: X, x_tol: X, y_tol: Y) -> X
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Find the root of \\( f(x) = x² +x -2 \\) in the interval \\( [0, 100] \\).
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20numerics%3A%3Asolve%0Afn%20f%28x%29%20%3D%20x%C2%B2%20%2Bx%20%2D2%0Aroot%5Fbisect%28f%2C%200%2C%20100%2C%200%2E01%2C%200%2E01%29')""></button></div><code class="language-nbt hljs numbat">>>> use numerics::solve
+fn f(x) = x² +x -2
+root_bisect(f, 0, 100, 0.01, 0.01)
+
+    = 1.00098
+</code></pre>
+
+</details>
+
 ### `root_newton` (Newton's method)
 ### `root_newton` (Newton's method)
 Find the root of the function \\( f(x) \\) and its derivative \\( f'(x) \\) using Newton's method.
 Find the root of the function \\( f(x) \\) and its derivative \\( f'(x) \\) using Newton's method.
 More information [here](https://en.wikipedia.org/wiki/Newton%27s_method).
 More information [here](https://en.wikipedia.org/wiki/Newton%27s_method).
@@ -449,6 +792,20 @@ More information [here](https://en.wikipedia.org/wiki/Newton%27s_method).
 fn root_newton<X: Dim, Y: Dim>(f: Fn[(X) -> Y], f_prime: Fn[(X) -> Y / X], x0: X, y_tol: Y) -> X
 fn root_newton<X: Dim, Y: Dim>(f: Fn[(X) -> Y], f_prime: Fn[(X) -> Y / X], x0: X, y_tol: Y) -> X
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Find a root of \\( f(x) = x² -3x +2 \\) using Newton's method.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20numerics%3A%3Asolve%0Afn%20f%28x%29%20%3D%20x%C2%B2%20%2D3x%20%2B2%0Afn%20f%5Fprime%28x%29%20%3D%202x%20%2D3%0Aroot%5Fnewton%28f%2C%20f%5Fprime%2C%200%20%2C%200%2E01%29')""></button></div><code class="language-nbt hljs numbat">>>> use numerics::solve
+fn f(x) = x² -3x +2
+fn f_prime(x) = 2x -3
+root_newton(f, f_prime, 0 , 0.01)
+
+    = 0.996078
+</code></pre>
+
+</details>
+
 ### `fixed_point` (Fixed-point iteration)
 ### `fixed_point` (Fixed-point iteration)
 Compute the approximate fixed point of a function \\( f: X \rightarrow X \\) starting from \\( x_0 \\), until \\( |f(x) - x| < ε \\).
 Compute the approximate fixed point of a function \\( f: X \rightarrow X \\) starting from \\( x_0 \\), until \\( |f(x) - x| < ε \\).
 More information [here](https://en.wikipedia.org/wiki/Fixed-point_iteration).
 More information [here](https://en.wikipedia.org/wiki/Fixed-point_iteration).
@@ -457,6 +814,19 @@ More information [here](https://en.wikipedia.org/wiki/Fixed-point_iteration).
 fn fixed_point<X: Dim>(f: Fn[(X) -> X], x0: X, ε: X) -> X
 fn fixed_point<X: Dim>(f: Fn[(X) -> X], x0: X, ε: X) -> X
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Compute the fixed poin of \\( f(x) = x/2 -1 \\).
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20numerics%3A%3Afixed%5Fpoint%0Afn%20function%28x%29%20%3D%20x%2F2%20%2D%201%0Afixed%5Fpoint%28function%2C%200%2C%200%2E01%29')""></button></div><code class="language-nbt hljs numbat">>>> use numerics::fixed_point
+fn function(x) = x/2 - 1
+fixed_point(function, 0, 0.01)
+
+    = -1.99219
+</code></pre>
+
+</details>
+
 ## Geometry
 ## Geometry
 
 
 Defined in: `math::geometry`
 Defined in: `math::geometry`
@@ -468,6 +838,16 @@ The length of the hypotenuse of a right-angled triangle \\( \sqrt{x^2+y^2} \\).
 fn hypot2<T: Dim>(x: T, y: T) -> T
 fn hypot2<T: Dim>(x: T, y: T) -> T
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=hypot2%283%20m%2C%204%20m%29')""></button></div><code class="language-nbt hljs numbat">>>> hypot2(3 m, 4 m)
+
+    = 5 m    [Length]
+</code></pre>
+
+</details>
+
 ### `hypot3`
 ### `hypot3`
 The Euclidean norm of a 3D vector \\( \sqrt{x^2+y^2+z^2} \\).
 The Euclidean norm of a 3D vector \\( \sqrt{x^2+y^2+z^2} \\).
 
 
@@ -475,6 +855,16 @@ The Euclidean norm of a 3D vector \\( \sqrt{x^2+y^2+z^2} \\).
 fn hypot3<T: Dim>(x: T, y: T, z: T) -> T
 fn hypot3<T: Dim>(x: T, y: T, z: T) -> T
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=hypot3%288%2C%209%2C%2012%29')""></button></div><code class="language-nbt hljs numbat">>>> hypot3(8, 9, 12)
+
+    = 17
+</code></pre>
+
+</details>
+
 ### `circle_area`
 ### `circle_area`
 The area of a circle, \\( \pi r^2 \\).
 The area of a circle, \\( \pi r^2 \\).
 
 
@@ -515,6 +905,18 @@ More information [here](https://en.wikipedia.org/wiki/Quadratic_equation).
 fn quadratic_equation<A: Dim, B: Dim>(a: A, b: B, c: B^2 / A) -> List<B / A>
 fn quadratic_equation<A: Dim, B: Dim>(a: A, b: B, c: B^2 / A) -> List<B / A>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Solve the equation \\( 2x² -x -1 = 0 \\)
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20extra%3A%3Aalgebra%0Aquadratic%5Fequation%282%2C%20%2D1%2C%20%2D1%29')""></button></div><code class="language-nbt hljs numbat">>>> use extra::algebra
+quadratic_equation(2, -1, -1)
+
+    = [1, -0.5]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ## Trigonometry (extra)
 ## Trigonometry (extra)
 
 
 Defined in: `math::trigonometry_extra`
 Defined in: `math::trigonometry_extra`

+ 247 - 9
book/src/list-functions-other.md

@@ -25,6 +25,21 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn is_nan<T: Dim>(n: T) -> Bool
 fn is_nan<T: Dim>(n: T) -> Bool
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Fnan%2837%29')""></button></div><code class="language-nbt hljs numbat">>>> is_nan(37)
+
+    = false    [Bool]
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Fnan%28NaN%29')""></button></div><code class="language-nbt hljs numbat">>>> is_nan(NaN)
+
+    = true    [Bool]
+</code></pre>
+
+</details>
+
 ### `is_infinite`
 ### `is_infinite`
 Returns true if the input is positive infinity or negative infinity.
 Returns true if the input is positive infinity or negative infinity.
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.is_infinite).
 More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.is_infinite).
@@ -33,6 +48,21 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 fn is_infinite<T: Dim>(n: T) -> Bool
 fn is_infinite<T: Dim>(n: T) -> Bool
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Finfinite%2837%29')""></button></div><code class="language-nbt hljs numbat">>>> is_infinite(37)
+
+    = false    [Bool]
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Finfinite%28%2Dinf%29')""></button></div><code class="language-nbt hljs numbat">>>> is_infinite(-inf)
+
+    = true    [Bool]
+</code></pre>
+
+</details>
+
 ### `is_finite`
 ### `is_finite`
 Returns true if the input is neither infinite nor `NaN`.
 Returns true if the input is neither infinite nor `NaN`.
 
 
@@ -40,6 +70,21 @@ Returns true if the input is neither infinite nor `NaN`.
 fn is_finite<T: Dim>(n: T) -> Bool
 fn is_finite<T: Dim>(n: T) -> Bool
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Ffinite%2837%29')""></button></div><code class="language-nbt hljs numbat">>>> is_finite(37)
+
+    = true    [Bool]
+</code></pre>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=is%5Ffinite%28%2Dinf%29')""></button></div><code class="language-nbt hljs numbat">>>> is_finite(-inf)
+
+    = false    [Bool]
+</code></pre>
+
+</details>
+
 ## Quantities
 ## Quantities
 
 
 Defined in: `core::quantities`
 Defined in: `core::quantities`
@@ -51,6 +96,16 @@ Extract the unit of a quantity (the `km/h` in `20 km/h`). This can be useful in
 fn unit_of<T: Dim>(x: T) -> T
 fn unit_of<T: Dim>(x: T) -> T
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=unit%5Fof%2820%20km%2Fh%29')""></button></div><code class="language-nbt hljs numbat">>>> unit_of(20 km/h)
+
+    = 1 km/h    [Velocity]
+</code></pre>
+
+</details>
+
 ### `value_of`
 ### `value_of`
 Extract the plain value of a quantity (the `20` in `20 km/h`). This can be useful in generic code, but should generally be avoided otherwise.
 Extract the plain value of a quantity (the `20` in `20 km/h`). This can be useful in generic code, but should generally be avoided otherwise.
 
 
@@ -58,53 +113,137 @@ Extract the plain value of a quantity (the `20` in `20 km/h`). This can be usefu
 fn value_of<T: Dim>(x: T) -> Scalar
 fn value_of<T: Dim>(x: T) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=value%5Fof%2820%20km%2Fh%29')""></button></div><code class="language-nbt hljs numbat">>>> value_of(20 km/h)
+
+    = 20
+</code></pre>
+
+</details>
+
 ## Chemical elements
 ## Chemical elements
 
 
 Defined in: `chemistry::elements`
 Defined in: `chemistry::elements`
 
 
 ### `element` (Chemical element)
 ### `element` (Chemical element)
-Get properties of a chemical element by its symbol or name (case-insensitive). For example: `element("H")` or `element("hydrogen")`.
+Get properties of a chemical element by its symbol or name (case-insensitive).
 
 
 ```nbt
 ```nbt
 fn element(pattern: String) -> ChemicalElement
 fn element(pattern: String) -> ChemicalElement
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+Get the entire element struct for hydrogen.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=element%28%22H%22%29')""></button></div><code class="language-nbt hljs numbat">>>> element("H")
+
+    = ChemicalElement { symbol: "H", name: "Hydrogen", atomic_number: 1, group: 1, group_name: "Alkali metals", period: 1, melting_point: 13.99 K, boiling_point: 20.271 K, density: 0.00008988 g/cm³, electron_affinity: 0.754 eV, ionization_energy: 13.598 eV, vaporization_heat: 0.904 kJ/mol }    [ChemicalElement]
+</code></pre>
+
+Get the ionization energy of hydrogen.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=element%28%22hydrogen%22%29%2Eionization%5Fenergy')""></button></div><code class="language-nbt hljs numbat">>>> element("hydrogen").ionization_energy
+
+    = 13.598 eV    [Energy or Torque]
+</code></pre>
+
+</details>
+
 ## Mixed unit conversion
 ## Mixed unit conversion
 
 
 Defined in: `units::mixed`
 Defined in: `units::mixed`
 
 
+### `unit_list` (Unit list)
+Convert a value to a mixed representation using the provided units.
+
+```nbt
+fn unit_list<D: Dim>(units: List<D>, value: D) -> List<D>
+```
+
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=5500%20m%20%7C%3E%20unit%5Flist%28%5Bmiles%2C%20yards%2C%20feet%2C%20inches%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> 5500 m |> unit_list([miles, yards, feet, inches])
+
+    = [3 mi, 734 yd, 2 ft, 7.43307 in]    [List<Length>]
+</code></pre>
+
+</details>
+
 ### `DMS` (Degrees, minutes, seconds)
 ### `DMS` (Degrees, minutes, seconds)
 Convert an angle to a mixed degrees, (arc)minutes, and (arc)seconds representation. Also called sexagesimal degree notation.
 Convert an angle to a mixed degrees, (arc)minutes, and (arc)seconds representation. Also called sexagesimal degree notation.
 More information [here](https://en.wikipedia.org/wiki/Sexagesimal_degree).
 More information [here](https://en.wikipedia.org/wiki/Sexagesimal_degree).
 
 
 ```nbt
 ```nbt
-fn DMS(alpha: Angle) -> String
+fn DMS(alpha: Angle) -> List<Angle>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=46%2E5858%C2%B0%20%2D%3E%20DMS')""></button></div><code class="language-nbt hljs numbat">>>> 46.5858° -> DMS
+
+    = [46°, 35′, 8.88″]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `DM` (Degrees, decimal minutes)
 ### `DM` (Degrees, decimal minutes)
 Convert an angle to a mixed degrees and decimal minutes representation.
 Convert an angle to a mixed degrees and decimal minutes representation.
 More information [here](https://en.wikipedia.org/wiki/Decimal_degrees).
 More information [here](https://en.wikipedia.org/wiki/Decimal_degrees).
 
 
 ```nbt
 ```nbt
-fn DM(alpha: Angle) -> String
+fn DM(alpha: Angle) -> List<Angle>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=46%2E5858%C2%B0%20%2D%3E%20DM')""></button></div><code class="language-nbt hljs numbat">>>> 46.5858° -> DM
+
+    = [46°, 35.148′]    [List<Scalar>]
+</code></pre>
+
+</details>
+
 ### `feet_and_inches` (Feet and inches)
 ### `feet_and_inches` (Feet and inches)
 Convert a length to a mixed feet and inches representation.
 Convert a length to a mixed feet and inches representation.
 More information [here](https://en.wikipedia.org/wiki/Foot_(unit)).
 More information [here](https://en.wikipedia.org/wiki/Foot_(unit)).
 
 
 ```nbt
 ```nbt
-fn feet_and_inches(length: Length) -> String
+fn feet_and_inches(length: Length) -> List<Length>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=180%20cm%20%2D%3E%20feet%5Fand%5Finches')""></button></div><code class="language-nbt hljs numbat">>>> 180 cm -> feet_and_inches
+
+    = [5 ft, 10.8661 in]    [List<Length>]
+</code></pre>
+
+</details>
+
 ### `pounds_and_ounces` (Pounds and ounces)
 ### `pounds_and_ounces` (Pounds and ounces)
 Convert a mass to a mixed pounds and ounces representation.
 Convert a mass to a mixed pounds and ounces representation.
 More information [here](https://en.wikipedia.org/wiki/Pound_(mass)).
 More information [here](https://en.wikipedia.org/wiki/Pound_(mass)).
 
 
 ```nbt
 ```nbt
-fn pounds_and_ounces(mass: Mass) -> String
+fn pounds_and_ounces(mass: Mass) -> List<Mass>
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=1%20kg%20%2D%3E%20pounds%5Fand%5Founces')""></button></div><code class="language-nbt hljs numbat">>>> 1 kg -> pounds_and_ounces
+
+    = [2 lb, 3.27396 oz]    [List<Mass>]
+</code></pre>
+
+</details>
+
 ## Temperature conversion
 ## Temperature conversion
 
 
 Defined in: `physics::temperature_conversion`
 Defined in: `physics::temperature_conversion`
@@ -117,6 +256,17 @@ More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_te
 fn from_celsius(t_celsius: Scalar) -> Temperature
 fn from_celsius(t_celsius: Scalar) -> Temperature
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+300 °C in Kelvin.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=from%5Fcelsius%28300%29')""></button></div><code class="language-nbt hljs numbat">>>> from_celsius(300)
+
+    = 573.15 K    [Temperature]
+</code></pre>
+
+</details>
+
 ### `celsius`
 ### `celsius`
 Converts from Kelvin to degree Celcius (°C). This can be used on the right hand side of a conversion operator: `200 K -> celsius`.
 Converts from Kelvin to degree Celcius (°C). This can be used on the right hand side of a conversion operator: `200 K -> celsius`.
 More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature).
 More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature).
@@ -125,6 +275,17 @@ More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_te
 fn celsius(t_kelvin: Temperature) -> Scalar
 fn celsius(t_kelvin: Temperature) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+300 K in degree Celsius.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=300K%20%2D%3E%20celsius')""></button></div><code class="language-nbt hljs numbat">>>> 300K -> celsius
+
+    = 26.85
+</code></pre>
+
+</details>
+
 ### `from_fahrenheit`
 ### `from_fahrenheit`
 Converts from degree Fahrenheit (°F) to Kelvin.
 Converts from degree Fahrenheit (°F) to Kelvin.
 More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature).
 More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature).
@@ -133,6 +294,17 @@ More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_te
 fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature
 fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+300 °F in Kelvin.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=from%5Ffahrenheit%28300%29')""></button></div><code class="language-nbt hljs numbat">>>> from_fahrenheit(300)
+
+    = 422.039 K    [Temperature]
+</code></pre>
+
+</details>
+
 ### `fahrenheit`
 ### `fahrenheit`
 Converts from Kelvin to degree Fahrenheit (°F). This can be used on the right hand side of a conversion operator: `200 K -> fahrenheit`.
 Converts from Kelvin to degree Fahrenheit (°F). This can be used on the right hand side of a conversion operator: `200 K -> fahrenheit`.
 More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature).
 More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature).
@@ -141,6 +313,17 @@ More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_te
 fn fahrenheit(t_kelvin: Temperature) -> Scalar
 fn fahrenheit(t_kelvin: Temperature) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+300 K in degree Fahrenheit.
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=300K%20%2D%3E%20fahrenheit')""></button></div><code class="language-nbt hljs numbat">>>> 300K -> fahrenheit
+
+    = 80.33
+</code></pre>
+
+</details>
+
 ## Color format conversion
 ## Color format conversion
 
 
 Defined in: `extra::color`
 Defined in: `extra::color`
@@ -152,31 +335,86 @@ Create a `Color` from RGB (red, green, blue) values in the range \\( [0, 256) \\
 fn rgb(red: Scalar, green: Scalar, blue: Scalar) -> Color
 fn rgb(red: Scalar, green: Scalar, blue: Scalar) -> Color
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20extra%3A%3Acolor%0Argb%28125%2C%20128%2C%20218%29')""></button></div><code class="language-nbt hljs numbat">>>> use extra::color
+rgb(125, 128, 218)
+
+    = Color { red: 125, green: 128, blue: 218 }    [Color]
+</code></pre>
+
+</details>
+
 ### `color`
 ### `color`
-Create a `Color` from a (hexadecimal) value, e.g. `color(0xff7700)`.
+Create a `Color` from a (hexadecimal) value.
 
 
 ```nbt
 ```nbt
 fn color(rgb_hex: Scalar) -> Color
 fn color(rgb_hex: Scalar) -> Color
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20extra%3A%3Acolor%0Acolor%280xff7700%29')""></button></div><code class="language-nbt hljs numbat">>>> use extra::color
+color(0xff7700)
+
+    = Color { red: 255, green: 119, blue: 0 }    [Color]
+</code></pre>
+
+</details>
+
 ### `color_rgb`
 ### `color_rgb`
-Convert a color to its RGB representation, e.g. `cyan -> color_rgb`.
+Convert a color to its RGB representation.
 
 
 ```nbt
 ```nbt
 fn color_rgb(color: Color) -> String
 fn color_rgb(color: Color) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20extra%3A%3Acolor%0Acyan%20%2D%3E%20color%5Frgb')""></button></div><code class="language-nbt hljs numbat">>>> use extra::color
+cyan -> color_rgb
+
+    = "rgb(0, 255, 255)"    [String]
+</code></pre>
+
+</details>
+
 ### `color_rgb_float`
 ### `color_rgb_float`
-Convert a color to its RGB floating point representation, e.g. `cyan -> color_rgb_float`.
+Convert a color to its RGB floating point representation.
 
 
 ```nbt
 ```nbt
 fn color_rgb_float(color: Color) -> String
 fn color_rgb_float(color: Color) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20extra%3A%3Acolor%0Acyan%20%2D%3E%20color%5Frgb%5Ffloat')""></button></div><code class="language-nbt hljs numbat">>>> use extra::color
+cyan -> color_rgb_float
+
+    = "rgb(0.000, 1.000, 1.000)"    [String]
+</code></pre>
+
+</details>
+
 ### `color_hex`
 ### `color_hex`
-Convert a color to its hexadecimal representation, e.g. `rgb(225, 36, 143) -> color_hex`.
+Convert a color to its hexadecimal representation.
 
 
 ```nbt
 ```nbt
 fn color_hex(color: Color) -> String
 fn color_hex(color: Color) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=use%20extra%3A%3Acolor%0Argb%28225%2C%2036%2C%20143%29%20%2D%3E%20color%5Fhex')""></button></div><code class="language-nbt hljs numbat">>>> use extra::color
+rgb(225, 36, 143) -> color_hex
+
+    = "#e1248f"    [String]
+</code></pre>
+
+</details>
+

+ 166 - 6
book/src/list-functions-strings.md

@@ -9,6 +9,16 @@ The length of a string.
 fn str_length(s: String) -> Scalar
 fn str_length(s: String) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=str%5Flength%28%22Numbat%22%29')""></button></div><code class="language-nbt hljs numbat">>>> str_length("Numbat")
+
+    = 6
+</code></pre>
+
+</details>
+
 ### `str_slice`
 ### `str_slice`
 Subslice of a string.
 Subslice of a string.
 
 
@@ -16,20 +26,50 @@ Subslice of a string.
 fn str_slice(s: String, start: Scalar, end: Scalar) -> String
 fn str_slice(s: String, start: Scalar, end: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=str%5Fslice%28%22Numbat%22%2C%203%2C%206%29')""></button></div><code class="language-nbt hljs numbat">>>> str_slice("Numbat", 3, 6)
+
+    = "bat"    [String]
+</code></pre>
+
+</details>
+
 ### `chr`
 ### `chr`
-Get a single-character string from a Unicode code point. Example: `0x2764 -> chr`.
+Get a single-character string from a Unicode code point.
 
 
 ```nbt
 ```nbt
 fn chr(n: Scalar) -> String
 fn chr(n: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=0x2764%20%2D%3E%20chr')""></button></div><code class="language-nbt hljs numbat">>>> 0x2764 -> chr
+
+    = "❤"    [String]
+</code></pre>
+
+</details>
+
 ### `ord`
 ### `ord`
-Get the Unicode code point of the first character in a string. Example: `"❤" -> ord`.
+Get the Unicode code point of the first character in a string.
 
 
 ```nbt
 ```nbt
 fn ord(s: String) -> Scalar
 fn ord(s: String) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=%22%E2%9D%A4%22%20%2D%3E%20ord')""></button></div><code class="language-nbt hljs numbat">>>> "❤" -> ord
+
+    = 10084
+</code></pre>
+
+</details>
+
 ### `lowercase`
 ### `lowercase`
 Convert a string to lowercase.
 Convert a string to lowercase.
 
 
@@ -37,6 +77,16 @@ Convert a string to lowercase.
 fn lowercase(s: String) -> String
 fn lowercase(s: String) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=lowercase%28%22Numbat%22%29')""></button></div><code class="language-nbt hljs numbat">>>> lowercase("Numbat")
+
+    = "numbat"    [String]
+</code></pre>
+
+</details>
+
 ### `uppercase`
 ### `uppercase`
 Convert a string to uppercase.
 Convert a string to uppercase.
 
 
@@ -44,6 +94,16 @@ Convert a string to uppercase.
 fn uppercase(s: String) -> String
 fn uppercase(s: String) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=uppercase%28%22Numbat%22%29')""></button></div><code class="language-nbt hljs numbat">>>> uppercase("Numbat")
+
+    = "NUMBAT"    [String]
+</code></pre>
+
+</details>
+
 ### `str_append`
 ### `str_append`
 Concatenate two strings.
 Concatenate two strings.
 
 
@@ -51,6 +111,16 @@ Concatenate two strings.
 fn str_append(a: String, b: String) -> String
 fn str_append(a: String, b: String) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=str%5Fappend%28%22Numbat%22%2C%20%22%21%22%29')""></button></div><code class="language-nbt hljs numbat">>>> str_append("Numbat", "!")
+
+    = "Numbat!"    [String]
+</code></pre>
+
+</details>
+
 ### `str_find`
 ### `str_find`
 Find the first occurrence of a substring in a string.
 Find the first occurrence of a substring in a string.
 
 
@@ -58,6 +128,16 @@ Find the first occurrence of a substring in a string.
 fn str_find(haystack: String, needle: String) -> Scalar
 fn str_find(haystack: String, needle: String) -> Scalar
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=str%5Ffind%28%22Numbat%20is%20a%20statically%20typed%20programming%20language%2E%22%2C%20%22typed%22%29')""></button></div><code class="language-nbt hljs numbat">>>> str_find("Numbat is a statically typed programming language.", "typed")
+
+    = 23
+</code></pre>
+
+</details>
+
 ### `str_contains`
 ### `str_contains`
 Check if a string contains a substring.
 Check if a string contains a substring.
 
 
@@ -65,6 +145,16 @@ Check if a string contains a substring.
 fn str_contains(haystack: String, needle: String) -> Bool
 fn str_contains(haystack: String, needle: String) -> Bool
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=str%5Fcontains%28%22Numbat%20is%20a%20statically%20typed%20programming%20language%2E%22%2C%20%22typed%22%29')""></button></div><code class="language-nbt hljs numbat">>>> str_contains("Numbat is a statically typed programming language.", "typed")
+
+    = true    [Bool]
+</code></pre>
+
+</details>
+
 ### `str_replace`
 ### `str_replace`
 Replace all occurrences of a substring in a string.
 Replace all occurrences of a substring in a string.
 
 
@@ -72,6 +162,16 @@ Replace all occurrences of a substring in a string.
 fn str_replace(s: String, pattern: String, replacement: String) -> String
 fn str_replace(s: String, pattern: String, replacement: String) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=str%5Freplace%28%22Numbat%20is%20a%20statically%20typed%20programming%20language%2E%22%2C%20%22statically%20typed%20programming%20language%22%2C%20%22scientific%20calculator%22%29')""></button></div><code class="language-nbt hljs numbat">>>> str_replace("Numbat is a statically typed programming language.", "statically typed programming language", "scientific calculator")
+
+    = "Numbat is a scientific calculator."    [String]
+</code></pre>
+
+</details>
+
 ### `str_repeat`
 ### `str_repeat`
 Repeat the input string `n` times.
 Repeat the input string `n` times.
 
 
@@ -79,27 +179,67 @@ Repeat the input string `n` times.
 fn str_repeat(a: String, n: Scalar) -> String
 fn str_repeat(a: String, n: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=str%5Frepeat%28%22abc%22%2C%204%29')""></button></div><code class="language-nbt hljs numbat">>>> str_repeat("abc", 4)
+
+    = "abcabcabcabc"    [String]
+</code></pre>
+
+</details>
+
 ### `base`
 ### `base`
-Convert a number to the given base. Example: `42 |> base(16)`.
+Convert a number to the given base.
 
 
 ```nbt
 ```nbt
 fn base(b: Scalar, x: Scalar) -> String
 fn base(b: Scalar, x: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=42%20%7C%3E%20base%2816%29')""></button></div><code class="language-nbt hljs numbat">>>> 42 |> base(16)
+
+    = "2a"    [String]
+</code></pre>
+
+</details>
+
 ### `bin`
 ### `bin`
-Get a binary representation of a number. Example: `42 -> bin`.
+Get a binary representation of a number.
 
 
 ```nbt
 ```nbt
 fn bin(x: Scalar) -> String
 fn bin(x: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=42%20%2D%3E%20bin')""></button></div><code class="language-nbt hljs numbat">>>> 42 -> bin
+
+    = "0b101010"    [String]
+</code></pre>
+
+</details>
+
 ### `oct`
 ### `oct`
-Get an octal representation of a number. Example: `42 -> oct`.
+Get an octal representation of a number.
 
 
 ```nbt
 ```nbt
 fn oct(x: Scalar) -> String
 fn oct(x: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=42%20%2D%3E%20oct')""></button></div><code class="language-nbt hljs numbat">>>> 42 -> oct
+
+    = "0o52"    [String]
+</code></pre>
+
+</details>
+
 ### `dec`
 ### `dec`
 Get a decimal representation of a number.
 Get a decimal representation of a number.
 
 
@@ -107,10 +247,30 @@ Get a decimal representation of a number.
 fn dec(x: Scalar) -> String
 fn dec(x: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=0b111%20%2D%3E%20dec')""></button></div><code class="language-nbt hljs numbat">>>> 0b111 -> dec
+
+    = "7"    [String]
+</code></pre>
+
+</details>
+
 ### `hex`
 ### `hex`
-Get a hexadecimal representation of a number. Example: `2^31-1 -> hex`.
+Get a hexadecimal representation of a number.
 
 
 ```nbt
 ```nbt
 fn hex(x: Scalar) -> String
 fn hex(x: Scalar) -> String
 ```
 ```
 
 
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=2%5E31%2D1%20%2D%3E%20hex')""></button></div><code class="language-nbt hljs numbat">>>> 2^31-1 -> hex
+
+    = "0x7fffffff"    [String]
+</code></pre>
+
+</details>
+

+ 31 - 16
examples/tests/mixed_units.nbt

@@ -4,33 +4,48 @@
 assert_eq(38° + 53′ + 23″, 38.8897°, 1e-4°)
 assert_eq(38° + 53′ + 23″, 38.8897°, 1e-4°)
 assert_eq(-(77° + 0′ + 32″), -77.0089°, 1e-4°)
 assert_eq(-(77° + 0′ + 32″), -77.0089°, 1e-4°)
 
 
-assert_eq(38.8897° -> DMS, "38° 53′ 23″")
-assert_eq(-77.0089° -> DMS, "-77° 0′ 32″")
+assert_eq("{38.8897° -> DMS}", "{[38°, 53′, 22.92″]}")
+assert_eq("{-77.0089° -> DMS}", "{[-77°, -0′, -32.04]}")
 
 
 ## Stuttgart
 ## Stuttgart
 assert_eq(48° + 46′ + 32″, 48.7756°, 1e-4°)
 assert_eq(48° + 46′ + 32″, 48.7756°, 1e-4°)
 assert_eq(9° + 10′ + 58″, 9.1828°, 1e-4°)
 assert_eq(9° + 10′ + 58″, 9.1828°, 1e-4°)
 
 
-assert_eq(48.7756° -> DMS, "48° 46′ 32″")
-assert_eq(9.1828° -> DMS, "9° 10′ 58″")
+assert_eq("{48.7756° -> DMS}", "{[48°, 46′, 32.16]}")
+assert_eq("{9.1828° -> DMS}", "{[, 10′, 58.08]}")
 
 
 # Degrees, decimal minutes (DM)
 # Degrees, decimal minutes (DM)
 
 
-assert_eq(38.8897° -> DM, "38° 53.382′")
-assert_eq(-77.0089° -> DM, "-77° 0.534′")
+assert_eq("{38.8897° -> DM}", "{[38°, 53.382′]}")
+assert_eq("{-77.0089° -> DM}", "{[-77°, -0.534′]}")
 
 
 # Feet and inches
 # Feet and inches
 
 
-assert_eq(5.5 ft -> feet_and_inches, "5 ft 6 in")
-assert_eq(6.75 ft -> feet_and_inches, "6 ft 9 in")
-assert_eq(-5.5 ft -> feet_and_inches, "-5 ft 6 in")
-assert_eq(0 -> feet_and_inches, "0 ft 0 in")
-assert_eq(1 ft -> feet_and_inches, "1 ft 0 in")
-assert_eq(2.345 inch -> feet_and_inches, "0 ft 2.345 in")
+assert_eq("{5.5 ft -> feet_and_inches}", "{[5 ft, 6 in]}")
+assert_eq("{6.75 ft -> feet_and_inches}", "{[6 ft, 9 in]}")
+assert_eq("{-5.5 ft -> feet_and_inches}", "{[-5 ft, -6 in]}")
+assert_eq("{0 -> feet_and_inches}", "{[0 ft, 0 in]}")
+assert_eq("{1 ft -> feet_and_inches}", "{[1 ft, 0 in]}")
+assert_eq("{2.345 inch -> feet_and_inches}", "{[0 ft, 2.345 in]}")
 
 
 # Pounds and ounces
 # Pounds and ounces
 
 
-assert_eq(5 lb -> pounds_and_ounces, "5 lb 0 oz")
-assert_eq(5.5 lb -> pounds_and_ounces, "5 lb 8 oz")
-assert_eq(6.75 lb -> pounds_and_ounces, "6 lb 12 oz")
-assert_eq(-5.5 lb -> pounds_and_ounces, "-5 lb 8 oz")
+assert_eq("{5 lb -> pounds_and_ounces}", "{[5 lb, 0 oz]}")
+assert_eq("{5.5 lb -> pounds_and_ounces}", "{[5 lb, 8 oz]}")
+assert_eq("{6.75 lb -> pounds_and_ounces}", "{[6 lb, 12 oz]}")
+assert_eq("{-5.5 lb -> pounds_and_ounces}", "{[-5 lb, -8 oz]}")
+
+# Unit list
+
+let test1 = 12 m + 34 cm + 5 mm + 678 µm
+assert_eq(test1 |> unit_list([m]) |> head, test1)
+assert_eq(test1 |> unit_list([m, cm]) |> sum, test1)
+assert_eq(test1 |> unit_list([m, cm, mm]) |> sum, test1)
+assert_eq(test1 |> unit_list([m, cm, mm, µm]) |> sum, test1)
+
+let test2 = 12 degree + 34 arcminute + 5 arcsec
+assert_eq(test2 |> unit_list([degree]) |> head, test2)
+assert_eq(test2 |> unit_list([degree, arcmin]) |> sum, test2)
+assert_eq(test2 |> unit_list([degree, arcmin, arcsec]) |> sum, test2)
+
+

+ 1 - 1
numbat-cli/src/completer.rs

@@ -131,7 +131,7 @@ impl Completer for NumbatCompleter {
             candidates
             candidates
                 .map(|w| Pair {
                 .map(|w| Pair {
                     display: w.to_string(),
                     display: w.to_string(),
-                    replacement: w.to_string(),
+                    replacement: w,
                 })
                 })
                 .collect(),
                 .collect(),
         ))
         ))

+ 10 - 4
numbat-cli/src/highlighter.rs

@@ -40,17 +40,23 @@ impl Highlighter for NumbatHighlighter {
         if ctx.variable_names().any(|n| n == candidate)
         if ctx.variable_names().any(|n| n == candidate)
             || ctx.function_names().any(|n| format!("{n}(") == candidate)
             || ctx.function_names().any(|n| format!("{n}(") == candidate)
         {
         {
-            Cow::Owned(ansi_format(&markup::identifier(candidate), false))
+            Cow::Owned(ansi_format(
+                &markup::identifier(candidate.to_string()),
+                false,
+            ))
         } else if ctx
         } else if ctx
             .unit_names()
             .unit_names()
             .iter()
             .iter()
             .any(|un| un.iter().any(|n| n == candidate))
             .any(|un| un.iter().any(|n| n == candidate))
         {
         {
-            Cow::Owned(ansi_format(&markup::unit(candidate), false))
+            Cow::Owned(ansi_format(&markup::unit(candidate.to_string()), false))
         } else if ctx.dimension_names().iter().any(|n| n == candidate) {
         } else if ctx.dimension_names().iter().any(|n| n == candidate) {
-            Cow::Owned(ansi_format(&markup::type_identifier(candidate), false))
+            Cow::Owned(ansi_format(
+                &markup::type_identifier(candidate.to_string()),
+                false,
+            ))
         } else if KEYWORDS.iter().any(|k| k == &candidate) {
         } else if KEYWORDS.iter().any(|k| k == &candidate) {
-            Cow::Owned(ansi_format(&markup::keyword(candidate), false))
+            Cow::Owned(ansi_format(&markup::keyword(candidate.to_string()), false))
         } else {
         } else {
             Cow::Borrowed(candidate)
             Cow::Borrowed(candidate)
         }
         }

+ 1 - 1
numbat-cli/src/main.rs

@@ -418,7 +418,7 @@ impl Cli {
                                                 let m = m::text(
                                                 let m = m::text(
                                                     "successfully saved session history to",
                                                     "successfully saved session history to",
                                                 ) + m::space()
                                                 ) + m::space()
-                                                    + m::string(dst);
+                                                    + m::string(dst.to_string());
                                                 println!("{}", ansi_format(&m, interactive));
                                                 println!("{}", ansi_format(&m, interactive));
                                             }
                                             }
                                             Err(err) => {
                                             Err(err) => {

+ 1 - 0
numbat/Cargo.toml

@@ -49,6 +49,7 @@ glob = "0.3"
 insta = "1.34.0"
 insta = "1.34.0"
 once_cell = "1.19.0"
 once_cell = "1.19.0"
 criterion = { version = "0.5", features = ["html_reports"] }
 criterion = { version = "0.5", features = ["html_reports"] }
+percent-encoding = "2.3.1"
 
 
 [[bench]]
 [[bench]]
 name = "prelude"
 name = "prelude"

+ 113 - 20
numbat/examples/inspect.rs

@@ -1,5 +1,9 @@
 use itertools::Itertools;
 use itertools::Itertools;
-use numbat::{module_importer::FileSystemImporter, resolver::CodeSource, Context};
+use numbat::markup::plain_text_format;
+use numbat::module_importer::FileSystemImporter;
+use numbat::resolver::CodeSource;
+use numbat::Context;
+use percent_encoding;
 use std::path::Path;
 use std::path::Path;
 
 
 const AUTO_GENERATED_HINT: &str = "<!-- NOTE! This file is auto-generated -->";
 const AUTO_GENERATED_HINT: &str = "<!-- NOTE! This file is auto-generated -->";
@@ -40,8 +44,8 @@ and — where sensible — units allow for [binary prefixes](https://en.wikipedi
     }
     }
 }
 }
 
 
-fn inspect_functions_in_module(ctx: &Context, module: String) {
-    for (fn_name, name, signature, description, url, code_source) in ctx.functions() {
+fn inspect_functions_in_module(ctx: &Context, prelude_ctx: &Context, module: String) {
+    for (fn_name, name, signature, description, url, examples, code_source) in ctx.functions() {
         let CodeSource::Module(module_path, _) = code_source else {
         let CodeSource::Module(module_path, _) = code_source else {
             unreachable!();
             unreachable!();
         };
         };
@@ -57,19 +61,7 @@ fn inspect_functions_in_module(ctx: &Context, module: String) {
         }
         }
 
 
         if let Some(ref description_raw) = description {
         if let Some(ref description_raw) = description {
-            let description_raw = description_raw.trim().to_string();
-
-            // Replace $..$ with \\( .. \\) for mdbook.
-            let mut description = String::new();
-            for (i, part) in description_raw.split('$').enumerate() {
-                if i % 2 == 0 {
-                    description.push_str(part);
-                } else {
-                    description.push_str("\\\\( ");
-                    description.push_str(part);
-                    description.push_str(" \\\\)");
-                }
-            }
+            let description = replace_equation_delimiters(description_raw.trim().to_string());
 
 
             if description.ends_with('.') {
             if description.ends_with('.') {
                 println!("{description}");
                 println!("{description}");
@@ -86,17 +78,118 @@ fn inspect_functions_in_module(ctx: &Context, module: String) {
         println!("{signature}");
         println!("{signature}");
         println!("```");
         println!("```");
         println!();
         println!();
+
+        if !examples.is_empty() {
+            println!("<details>");
+            println!("<summary>Examples</summary>");
+            println!();
+
+            for (example_code, example_description) in examples {
+                let mut example_ctx = prelude_ctx.clone();
+                let extra_import = if !example_ctx
+                    .resolver()
+                    .imported_modules
+                    .contains(&module_path)
+                {
+                    format!("use {}\n", module)
+                } else {
+                    "".into()
+                };
+                let _result = example_ctx
+                    .interpret(&extra_import, CodeSource::Internal)
+                    .unwrap();
+
+                if let Ok((statements, results)) =
+                    example_ctx.interpret(&example_code, CodeSource::Internal)
+                {
+                    let code = extra_import + &example_code;
+
+                    //Format the example input
+                    let example_input = format!(">>> {}", code);
+
+                    //Encode the example url
+                    let example_url = format!(
+                        "https://numbat.dev/?q={}",
+                        percent_encoding::utf8_percent_encode(
+                            &code,
+                            percent_encoding::NON_ALPHANUMERIC
+                        )
+                    );
+
+                    //Assemble the example output
+                    let result_markup = results.to_markup(
+                        statements.last(),
+                        &example_ctx.dimension_registry(),
+                        true,
+                        true,
+                    );
+                    let example_output = &plain_text_format(&result_markup, false);
+
+                    //Print the example
+                    if let Some(example_description) = example_description {
+                        println!("{}", replace_equation_delimiters(example_description));
+                    }
+
+                    print!("<pre>");
+                    print!("<div class=\"buttons\">");
+                    print!("<button class=\"fa fa-play play-button\" title=\"{}\" aria-label=\"{}\"  onclick=\" window.open('{}')\"\"></button>",
+                        "Run this code",
+                        "Run this code",
+                        example_url);
+                    print!("</div>");
+                    print!("<code class=\"language-nbt hljs numbat\">");
+                    for l in example_input.lines() {
+                        println!("{}", l);
+                    }
+                    println!();
+                    for l in example_output.lines() {
+                        println!("{}", l);
+                    }
+                    println!("</code></pre>");
+                    println!();
+                } else {
+                    eprintln!(
+                        "Warning: Example \"{example_code}\" of function {fn_name} did not run successfully."
+                    );
+                }
+            }
+            println!("</details>");
+            println!();
+        }
     }
     }
 }
 }
 
 
-fn main() {
-    let module_path = Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("modules");
+// Replace $..$ with \\( .. \\) for mdbook.
+fn replace_equation_delimiters(text_in: String) -> String {
+    let mut text_out = String::new();
+    for (i, part) in text_in.split('$').enumerate() {
+        if i % 2 == 0 {
+            text_out.push_str(part);
+        } else {
+            text_out.push_str("\\\\( ");
+            text_out.push_str(part);
+            text_out.push_str(" \\\\)");
+        }
+    }
+    return text_out;
+}
 
 
+fn prepare_context() -> Context {
+    let module_path = Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("modules");
     let mut importer = FileSystemImporter::default();
     let mut importer = FileSystemImporter::default();
     importer.add_path(module_path);
     importer.add_path(module_path);
-    let mut ctx = Context::new(importer);
+    return Context::new(importer);
+}
+
+fn main() {
+    let mut ctx = prepare_context();
     let _result = ctx.interpret("use all", CodeSource::Internal).unwrap();
     let _result = ctx.interpret("use all", CodeSource::Internal).unwrap();
 
 
+    let mut example_ctx = prepare_context();
+    let _result = example_ctx
+        .interpret("use prelude", CodeSource::Internal)
+        .unwrap();
+
     let mut args = std::env::args();
     let mut args = std::env::args();
     args.next();
     args.next();
     if let Some(arg) = args.next() {
     if let Some(arg) = args.next() {
@@ -104,7 +197,7 @@ fn main() {
             "units" => inspect_units(&ctx),
             "units" => inspect_units(&ctx),
             "functions" => {
             "functions" => {
                 let module = args.next().unwrap();
                 let module = args.next().unwrap();
-                inspect_functions_in_module(&ctx, module)
+                inspect_functions_in_module(&ctx, &example_ctx, module)
             }
             }
             _ => eprintln!("USAGE: inspect [units|functions <module>]"),
             _ => eprintln!("USAGE: inspect [units|functions <module>]"),
         }
         }

+ 3 - 1
numbat/modules/chemistry/elements.nbt

@@ -49,6 +49,8 @@ fn _convert_from_raw(raw: _ChemicalElementRaw) -> ChemicalElement =
     }
     }
 
 
 @name("Chemical element")
 @name("Chemical element")
-@description("Get properties of a chemical element by its symbol or name (case-insensitive). For example: `element(\"H\")` or `element(\"hydrogen\")`.")
+@description("Get properties of a chemical element by its symbol or name (case-insensitive).")
+@example("element(\"H\")", "Get the entire element struct for hydrogen.")
+@example("element(\"hydrogen\").ionization_energy", "Get the ionization energy of hydrogen.")
 fn element(pattern: String) -> ChemicalElement =
 fn element(pattern: String) -> ChemicalElement =
     _convert_from_raw(_get_chemical_element_data_raw(pattern))
     _convert_from_raw(_get_chemical_element_data_raw(pattern))

+ 25 - 4
numbat/modules/core/functions.nbt

@@ -2,64 +2,85 @@ use core::scalar
 
 
 @name("Identity function")
 @name("Identity function")
 @description("Return the input value.")
 @description("Return the input value.")
+@example("id(8 kg)")
 fn id<A>(x: A) -> A = x
 fn id<A>(x: A) -> A = x
 
 
 @name("Absolute value")
 @name("Absolute value")
 @description("Return the absolute value $|x|$ of the input. This works for quantities, too: `abs(-5 m) = 5 m`.")
 @description("Return the absolute value $|x|$ of the input. This works for quantities, too: `abs(-5 m) = 5 m`.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.abs")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.abs")
+@example("abs(-22.2 m)")
 fn abs<T: Dim>(x: T) -> T
 fn abs<T: Dim>(x: T) -> T
 
 
 @name("Square root")
 @name("Square root")
 @description("Return the square root $\\sqrt\{x\}$ of the input: `sqrt(121 m^2) = 11 m`.")
 @description("Return the square root $\\sqrt\{x\}$ of the input: `sqrt(121 m^2) = 11 m`.")
 @url("https://en.wikipedia.org/wiki/Square_root")
 @url("https://en.wikipedia.org/wiki/Square_root")
+@example("sqrt(4 are) -> m")
 fn sqrt<D: Dim>(x: D^2) -> D = x^(1/2)
 fn sqrt<D: Dim>(x: D^2) -> D = x^(1/2)
 
 
 @name("Cube root")
 @name("Cube root")
 @description("Return the cube root $\\sqrt[3]\{x\}$ of the input: `cbrt(8 m^3) = 2 m`.")
 @description("Return the cube root $\\sqrt[3]\{x\}$ of the input: `cbrt(8 m^3) = 2 m`.")
 @url("https://en.wikipedia.org/wiki/Cube_root")
 @url("https://en.wikipedia.org/wiki/Cube_root")
+@example("cbrt(8 L) -> cm")
 fn cbrt<D: Dim>(x: D^3) -> D = x^(1/3)
 fn cbrt<D: Dim>(x: D^3) -> D = x^(1/3)
 
 
 @name("Square function")
 @name("Square function")
 @description("Return the square of the input, $x^2$: `sqr(5 m) = 25 m^2`.")
 @description("Return the square of the input, $x^2$: `sqr(5 m) = 25 m^2`.")
+@example("sqr(7)")
 fn sqr<D: Dim>(x: D) -> D^2 = x^2
 fn sqr<D: Dim>(x: D) -> D^2 = x^2
 
 
 @name("Rounding")
 @name("Rounding")
 @description("Round to the nearest integer. If the value is half-way between two integers, round away from $0$. See also: `round_in`.")
 @description("Round to the nearest integer. If the value is half-way between two integers, round away from $0$. See also: `round_in`.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.round")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.round")
+@example("round(5.5)")
+@example("round(-5.5)")
 fn round(x: Scalar) -> Scalar
 fn round(x: Scalar) -> Scalar
 
 
 @name("Rounding")
 @name("Rounding")
-@description("Round to the nearest multiple of `base`. For example: `round_in(m, 5.3 m) == 5 m`.")
+@description("Round to the nearest multiple of `base`.")
+@example("round_in(m, 5.3 m)", "Round in meters.")
+@example("round_in(cm, 5.3 m)", "Round in centimeters.")
 fn round_in<D: Dim>(base: D, value: D) -> D = round(value / base) × base
 fn round_in<D: Dim>(base: D, value: D) -> D = round(value / base) × base
 
 
 @name("Floor function")
 @name("Floor function")
 @description("Returns the largest integer less than or equal to $x$. See also: `floor_in`.")
 @description("Returns the largest integer less than or equal to $x$. See also: `floor_in`.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.floor")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.floor")
+@example("floor(5.5)")
 fn floor(x: Scalar) -> Scalar
 fn floor(x: Scalar) -> Scalar
 
 
 @name("Floor function")
 @name("Floor function")
-@description("Returns the largest integer multiple of `base` less than or equal to `value`. For example: `floor_in(m, 5.7 m) == 5 m`.")
+@description("Returns the largest integer multiple of `base` less than or equal to `value`.")
+@example("floor_in(m, 5.7 m)", "Floor in meters.")
+@example("floor_in(cm, 5.7 m)", "Floor in centimeters.")
 fn floor_in<D: Dim>(base: D, value: D) -> D = floor(value / base) × base
 fn floor_in<D: Dim>(base: D, value: D) -> D = floor(value / base) × base
 
 
 @name("Ceil function")
 @name("Ceil function")
 @description("Returns the smallest integer greater than or equal to $x$. See also: `ceil_in`.")
 @description("Returns the smallest integer greater than or equal to $x$. See also: `ceil_in`.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.ceil")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.ceil")
+@example("ceil(5.5)")
 fn ceil(x: Scalar) -> Scalar
 fn ceil(x: Scalar) -> Scalar
 
 
 @name("Ceil function")
 @name("Ceil function")
-@description("Returns the smallest integer multuple of `base` greater than or equal to `value`. For example: `ceil_in(m, 5.3 m) == 6 m`.")
+@description("Returns the smallest integer multiple of `base` greater than or equal to `value`.")
+@example("ceil_in(m, 5.3 m)", "Ceil in meters.")
+@example("ceil_in(cm, 5.3 m)", "Ceil in centimeters.")
+
 fn ceil_in<D: Dim>(base: D, value: D) -> D = ceil(value / base) × base
 fn ceil_in<D: Dim>(base: D, value: D) -> D = ceil(value / base) × base
 
 
 @name("Truncation")
 @name("Truncation")
 @description("Returns the integer part of $x$. Non-integer numbers are always truncated towards zero. See also: `trunc_in`.")
 @description("Returns the integer part of $x$. Non-integer numbers are always truncated towards zero. See also: `trunc_in`.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.trunc")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.trunc")
+@example("trunc(5.5)")
+@example("trunc(-5.5)")
 fn trunc(x: Scalar) -> Scalar
 fn trunc(x: Scalar) -> Scalar
 
 
 @name("Truncation")
 @name("Truncation")
-@description("Truncates to an integer multiple of `base` (towards zero). For example: `trunc_in(m, -5.7 m) == -5 m`.")
+@description("Truncates to an integer multiple of `base` (towards zero).")
+@example("trunc_in(m, 5.7 m)", "Truncate in meters.")
+@example("trunc_in(cm, 5.7 m)", "Truncate in centimeters.")
 fn trunc_in<D: Dim>(base: D, value: D) -> D = trunc(value / base) × base
 fn trunc_in<D: Dim>(base: D, value: D) -> D = trunc(value / base) × base
 
 
 @name("Modulo")
 @name("Modulo")
 @description("Calculates the least nonnegative remainder of $a (\\mod b)$.")
 @description("Calculates the least nonnegative remainder of $a (\\mod b)$.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.rem_euclid")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.rem_euclid")
+@example("mod(27, 5)")
 fn mod<T: Dim>(a: T, b: T) -> T
 fn mod<T: Dim>(a: T, b: T) -> T

+ 23 - 0
numbat/modules/core/lists.nbt

@@ -3,48 +3,60 @@ use core::error
 use core::strings
 use core::strings
 
 
 @description("Get the length of a list")
 @description("Get the length of a list")
+@example("len([3, 2, 1])")
 fn len<A>(xs: List<A>) -> Scalar
 fn len<A>(xs: List<A>) -> Scalar
 
 
 @description("Get the first element of a list. Yields a runtime error if the list is empty.")
 @description("Get the first element of a list. Yields a runtime error if the list is empty.")
+@example("head([3, 2, 1])")
 fn head<A>(xs: List<A>) -> A
 fn head<A>(xs: List<A>) -> A
 
 
 @description("Get everything but the first element of a list. Yields a runtime error if the list is empty.")
 @description("Get everything but the first element of a list. Yields a runtime error if the list is empty.")
+@example("tail([3, 2, 1])")
 fn tail<A>(xs: List<A>) -> List<A>
 fn tail<A>(xs: List<A>) -> List<A>
 
 
 @description("Prepend an element to a list")
 @description("Prepend an element to a list")
+@example("cons(77, [3, 2, 1])")
 fn cons<A>(x: A, xs: List<A>) -> List<A>
 fn cons<A>(x: A, xs: List<A>) -> List<A>
 
 
 @description("Append an element to the end of a list")
 @description("Append an element to the end of a list")
+@example("cons_end(77, [3, 2, 1])")
 fn cons_end<A>(x: A, xs: List<A>) -> List<A>
 fn cons_end<A>(x: A, xs: List<A>) -> List<A>
 
 
 @description("Check if a list is empty")
 @description("Check if a list is empty")
+@example("is_empty([3, 2, 1])")
+@example("is_empty([])")
 fn is_empty<A>(xs: List<A>) -> Bool = xs == []
 fn is_empty<A>(xs: List<A>) -> Bool = xs == []
 
 
 @description("Concatenate two lists")
 @description("Concatenate two lists")
+@example("concat([3, 2, 1], [10, 11])")
 fn concat<A>(xs1: List<A>, xs2: List<A>) -> List<A> =
 fn concat<A>(xs1: List<A>, xs2: List<A>) -> List<A> =
   if is_empty(xs1)
   if is_empty(xs1)
     then xs2
     then xs2
     else cons(head(xs1), concat(tail(xs1), xs2))
     else cons(head(xs1), concat(tail(xs1), xs2))
 
 
 @description("Get the first `n` elements of a list")
 @description("Get the first `n` elements of a list")
+@example("take(2, [3, 2, 1, 0])")
 fn take<A>(n: Scalar, xs: List<A>) -> List<A> =
 fn take<A>(n: Scalar, xs: List<A>) -> List<A> =
   if n == 0 || is_empty(xs)
   if n == 0 || is_empty(xs)
     then []
     then []
     else cons(head(xs), take(n - 1, tail(xs)))
     else cons(head(xs), take(n - 1, tail(xs)))
 
 
 @description("Get everything but the first `n` elements of a list")
 @description("Get everything but the first `n` elements of a list")
+@example("drop(2, [3, 2, 1, 0])")
 fn drop<A>(n: Scalar, xs: List<A>) -> List<A> =
 fn drop<A>(n: Scalar, xs: List<A>) -> List<A> =
   if n == 0 || is_empty(xs)
   if n == 0 || is_empty(xs)
     then xs
     then xs
     else drop(n - 1, tail(xs))
     else drop(n - 1, tail(xs))
 
 
 @description("Get the element at index `i` in a list")
 @description("Get the element at index `i` in a list")
+@example("element_at(2, [3, 2, 1, 0])")
 fn element_at<A>(i: Scalar, xs: List<A>) -> A =
 fn element_at<A>(i: Scalar, xs: List<A>) -> A =
   if i == 0
   if i == 0
     then head(xs)
     then head(xs)
     else element_at(i - 1, tail(xs))
     else element_at(i - 1, tail(xs))
 
 
 @description("Generate a range of integer numbers from `start` to `end` (inclusive)")
 @description("Generate a range of integer numbers from `start` to `end` (inclusive)")
+@example("range(2, 12)")
 fn range(start: Scalar, end: Scalar) -> List<Scalar> =
 fn range(start: Scalar, end: Scalar) -> List<Scalar> =
   if start > end
   if start > end
     then []
     then []
@@ -52,18 +64,21 @@ fn range(start: Scalar, end: Scalar) -> List<Scalar> =
 
 
 
 
 @description("Reverse the order of a list")
 @description("Reverse the order of a list")
+@example("reverse([3, 2, 1])")
 fn reverse<A>(xs: List<A>) -> List<A> =
 fn reverse<A>(xs: List<A>) -> List<A> =
   if is_empty(xs)
   if is_empty(xs)
     then []
     then []
     else cons_end(head(xs), reverse(tail(xs)))
     else cons_end(head(xs), reverse(tail(xs)))
 
 
 @description("Generate a new list by applying a function to each element of the input list")
 @description("Generate a new list by applying a function to each element of the input list")
+@example("map(sqr, [3, 2, 1])", "Square all elements of a list.")
 fn map<A, B>(f: Fn[(A) -> B], xs: List<A>) -> List<B> =
 fn map<A, B>(f: Fn[(A) -> B], xs: List<A>) -> List<B> =
   if is_empty(xs)
   if is_empty(xs)
     then []
     then []
     else cons(f(head(xs)), map(f, tail(xs)))
     else cons(f(head(xs)), map(f, tail(xs)))
 
 
 @description("Filter a list by a predicate")
 @description("Filter a list by a predicate")
+@example("filter(is_finite, [0, 1e10, NaN, -inf])")
 fn filter<A>(p: Fn[(A) -> Bool], xs: List<A>) -> List<A> =
 fn filter<A>(p: Fn[(A) -> Bool], xs: List<A>) -> List<A> =
   if is_empty(xs)
   if is_empty(xs)
     then []
     then []
@@ -72,6 +87,7 @@ fn filter<A>(p: Fn[(A) -> Bool], xs: List<A>) -> List<A> =
       else filter(p, tail(xs))
       else filter(p, tail(xs))
 
 
 @description("Fold a function over a list")
 @description("Fold a function over a list")
+@example("foldl(str_append, \"\", [\"Num\", \"bat\", \"!\"])", "Join a list of strings by folding.")
 fn foldl<A, B>(f: Fn[(A, B) -> A], acc: A, xs: List<B>) -> A =
 fn foldl<A, B>(f: Fn[(A, B) -> A], acc: A, xs: List<B>) -> A =
   if is_empty(xs)
   if is_empty(xs)
     then acc
     then acc
@@ -88,6 +104,7 @@ fn _merge(xs, ys, cmp) =
 
 
 
 
 @description("Sort a list of elements, using the given key function that maps the element to a quantity")
 @description("Sort a list of elements, using the given key function that maps the element to a quantity")
+@example("fn last_digit(x) = mod(x, 10)\nsort_by_key(last_digit, [701, 313, 9999, 4])","Sort by last digit.")
 fn sort_by_key<A, D: Dim>(key: Fn[(A) -> D], xs: List<A>) -> List<A> =
 fn sort_by_key<A, D: Dim>(key: Fn[(A) -> D], xs: List<A>) -> List<A> =
   if is_empty(xs)
   if is_empty(xs)
     then []
     then []
@@ -98,9 +115,11 @@ fn sort_by_key<A, D: Dim>(key: Fn[(A) -> D], xs: List<A>) -> List<A> =
                   key)
                   key)
 
 
 @description("Sort a list of quantities")
 @description("Sort a list of quantities")
+@example("sort([3, 2, 7, 8, -4, 0, -5])")
 fn sort<D: Dim>(xs: List<D>) -> List<D> = sort_by_key(id, xs)
 fn sort<D: Dim>(xs: List<D>) -> List<D> = sort_by_key(id, xs)
 
 
 @description("Add an element between each pair of elements in a list")
 @description("Add an element between each pair of elements in a list")
+@example("intersperse(0, [1, 1, 1, 1])")
 fn intersperse<A>(sep: A, xs: List<A>) -> List<A> =
 fn intersperse<A>(sep: A, xs: List<A>) -> List<A> =
   if is_empty(xs)
   if is_empty(xs)
     then []
     then []
@@ -110,6 +129,7 @@ fn intersperse<A>(sep: A, xs: List<A>) -> List<A> =
 
 
 fn _add(x, y) = x + y # TODO: replace this with a local function once we support them
 fn _add(x, y) = x + y # TODO: replace this with a local function once we support them
 @description("Sum all elements of a list")
 @description("Sum all elements of a list")
+@example("sum([3 m, 200 cm, 1000 mm])")
 fn sum<D: Dim>(xs: List<D>) -> D = foldl(_add, 0, xs)
 fn sum<D: Dim>(xs: List<D>) -> D = foldl(_add, 0, xs)
 
 
 # TODO: implement linspace using `map` or similar once we have closures. This is ugly.
 # TODO: implement linspace using `map` or similar once we have closures. This is ugly.
@@ -119,12 +139,14 @@ fn _linspace_helper(start, end, n_steps, i) =
     else cons(start + (end - start) * i / (n_steps - 1), _linspace_helper(start, end, n_steps, i + 1))
     else cons(start + (end - start) * i / (n_steps - 1), _linspace_helper(start, end, n_steps, i + 1))
 
 
 @description("Generate a list of `n_steps` evenly spaced numbers from `start` to `end` (inclusive)")
 @description("Generate a list of `n_steps` evenly spaced numbers from `start` to `end` (inclusive)")
+@example("linspace(-5 m, 5 m, 11)")
 fn linspace<D: Dim>(start: D, end: D, n_steps: Scalar) -> List<D> =
 fn linspace<D: Dim>(start: D, end: D, n_steps: Scalar) -> List<D> =
   if n_steps <= 1
   if n_steps <= 1
     then error("Number of steps must be larger than 1")
     then error("Number of steps must be larger than 1")
     else _linspace_helper(start, end, n_steps, 0)
     else _linspace_helper(start, end, n_steps, 0)
 
 
 @description("Convert a list of strings into a single string by concatenating them with a separator")
 @description("Convert a list of strings into a single string by concatenating them with a separator")
+@example("join([\"snake\", \"case\"], \"_\")")
 fn join(xs: List<String>, sep: String) =
 fn join(xs: List<String>, sep: String) =
   if is_empty(xs)
   if is_empty(xs)
     then ""
     then ""
@@ -133,6 +155,7 @@ fn join(xs: List<String>, sep: String) =
       else "{head(xs)}{sep}{join(tail(xs), sep)}"
       else "{head(xs)}{sep}{join(tail(xs), sep)}"
 
 
 @description("Split a string into a list of strings using a separator")
 @description("Split a string into a list of strings using a separator")
+@example("split(\"Numbat is a statically typed programming language.\", \" \")")
 fn split(input: String, separator: String) -> List<String> =
 fn split(input: String, separator: String) -> List<String> =
   if input == ""
   if input == ""
     then []
     then []

+ 11 - 21
numbat/modules/core/mixed_units.nbt

@@ -3,25 +3,15 @@ use core::lists
 
 
 # Helper functions for mixed-unit conversions. See units::mixed for more.
 # Helper functions for mixed-unit conversions. See units::mixed for more.
 
 
-fn _mixed_units_helper<D: Dim>(q: D, units: List<D>, names: List<String>, round_last: Bool) -> List<String> =
-  if is_empty(units)
-    then
-      []
-    else
-      cons(
-        if len(units) == 1
-          then
-            if round_last
-              then "{round(q / head(units))}{head(names)}"
-              else "{q / head(units)}{head(names)}"
-          else "{trunc(q / head(units))}{head(names)}",
-        _mixed_units_helper(
-          q - trunc(q / head(units)) * head(units),
-          tail(units),
-          tail(names),
-          round_last))
+fn _zero_length<A: Dim>(val: A) -> A = val * 0 -> val
 
 
-fn _mixed_units<D: Dim>(q: D, units: List<D>, names: List<String>, round_last: Bool) -> String =
-  if q < 0
-    then str_append("-", _mixed_units(-q, units, names, round_last))
-    else join(_mixed_units_helper(q, units, names, round_last), "")
+fn _mixed_unit_list<D: Dim>(val: D, units: List<D>, acc: List<D>) -> List<D> =
+  if val == 0
+    then concat(acc, map(_zero_length, units))
+    else if len(units) == 1
+      then cons_end(val -> head(units), acc)
+      else _mixed_unit_list(val - unit_val, tail(units), cons_end(unit_val, acc))
+  where unit_val: D =
+    if (len(units) > 0)
+      then (val |> trunc_in(head(units)))
+      else error("Units list cannot be empty")

+ 6 - 0
numbat/modules/core/numbers.nbt

@@ -1,10 +1,16 @@
 @description("Returns true if the input is `NaN`.")
 @description("Returns true if the input is `NaN`.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.is_nan")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.is_nan")
+@example("is_nan(37)")
+@example("is_nan(NaN)")
 fn is_nan<T: Dim>(n: T) -> Bool
 fn is_nan<T: Dim>(n: T) -> Bool
 
 
 @description("Returns true if the input is positive infinity or negative infinity.")
 @description("Returns true if the input is positive infinity or negative infinity.")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.is_infinite")
 @url("https://doc.rust-lang.org/std/primitive.f64.html#method.is_infinite")
+@example("is_infinite(37)")
+@example("is_infinite(-inf)")
 fn is_infinite<T: Dim>(n: T) -> Bool
 fn is_infinite<T: Dim>(n: T) -> Bool
 
 
 @description("Returns true if the input is neither infinite nor `NaN`.")
 @description("Returns true if the input is neither infinite nor `NaN`.")
+@example("is_finite(37)")
+@example("is_finite(-inf)")
 fn is_finite<T: Dim>(n: T) -> Bool = !is_nan(n) && !is_infinite(n)
 fn is_finite<T: Dim>(n: T) -> Bool = !is_nan(n) && !is_infinite(n)

+ 2 - 0
numbat/modules/core/quantities.nbt

@@ -1,7 +1,9 @@
 use core::scalar
 use core::scalar
 
 
 @description("Extract the unit of a quantity (the `km/h` in `20 km/h`). This can be useful in generic code, but should generally be avoided otherwise.")
 @description("Extract the unit of a quantity (the `km/h` in `20 km/h`). This can be useful in generic code, but should generally be avoided otherwise.")
+@example("unit_of(20 km/h)")
 fn unit_of<T: Dim>(x: T) -> T
 fn unit_of<T: Dim>(x: T) -> T
 
 
 @description("Extract the plain value of a quantity (the `20` in `20 km/h`). This can be useful in generic code, but should generally be avoided otherwise.")
 @description("Extract the plain value of a quantity (the `20` in `20 km/h`). This can be useful in generic code, but should generally be avoided otherwise.")
+@example("value_of(20 km/h)")
 fn value_of<T: Dim>(x: T) -> Scalar = x / unit_of(x)
 fn value_of<T: Dim>(x: T) -> Scalar = x / unit_of(x)

+ 22 - 6
numbat/modules/core/strings.nbt

@@ -3,27 +3,35 @@ use core::functions
 use core::error
 use core::error
 
 
 @description("The length of a string")
 @description("The length of a string")
+@example("str_length(\"Numbat\")")
 fn str_length(s: String) -> Scalar
 fn str_length(s: String) -> Scalar
 
 
 @description("Subslice of a string")
 @description("Subslice of a string")
+@example("str_slice(\"Numbat\", 3, 6)")
 fn str_slice(s: String, start: Scalar, end: Scalar) -> String
 fn str_slice(s: String, start: Scalar, end: Scalar) -> String
 
 
-@description("Get a single-character string from a Unicode code point. Example: `0x2764 -> chr`")
+@description("Get a single-character string from a Unicode code point.")
+@example("0x2764 -> chr")
 fn chr(n: Scalar) -> String
 fn chr(n: Scalar) -> String
 
 
-@description("Get the Unicode code point of the first character in a string. Example: `\"❤\" -> ord`")
+@description("Get the Unicode code point of the first character in a string.")
+@example("\"❤\" -> ord")
 fn ord(s: String) -> Scalar
 fn ord(s: String) -> Scalar
 
 
 @description("Convert a string to lowercase")
 @description("Convert a string to lowercase")
+@example("lowercase(\"Numbat\")")
 fn lowercase(s: String) -> String
 fn lowercase(s: String) -> String
 
 
 @description("Convert a string to uppercase")
 @description("Convert a string to uppercase")
+@example("uppercase(\"Numbat\")")
 fn uppercase(s: String) -> String
 fn uppercase(s: String) -> String
 
 
 @description("Concatenate two strings")
 @description("Concatenate two strings")
+@example("str_append(\"Numbat\", \"!\")")
 fn str_append(a: String, b: String) -> String = "{a}{b}"
 fn str_append(a: String, b: String) -> String = "{a}{b}"
 
 
 @description("Find the first occurrence of a substring in a string")
 @description("Find the first occurrence of a substring in a string")
+@example("str_find(\"Numbat is a statically typed programming language.\", \"typed\")")
 fn str_find(haystack: String, needle: String) -> Scalar =
 fn str_find(haystack: String, needle: String) -> Scalar =
   if len_haystack == 0
   if len_haystack == 0
     then -1
     then -1
@@ -36,10 +44,12 @@ fn str_find(haystack: String, needle: String) -> Scalar =
     and tail_haystack = str_slice(haystack, 1, len_haystack)
     and tail_haystack = str_slice(haystack, 1, len_haystack)
 
 
 @description("Check if a string contains a substring")
 @description("Check if a string contains a substring")
+@example("str_contains(\"Numbat is a statically typed programming language.\", \"typed\")")
 fn str_contains(haystack: String, needle: String) -> Bool =
 fn str_contains(haystack: String, needle: String) -> Bool =
   str_find(haystack, needle) != -1
   str_find(haystack, needle) != -1
 
 
 @description("Replace all occurrences of a substring in a string")
 @description("Replace all occurrences of a substring in a string")
+@example("str_replace(\"Numbat is a statically typed programming language.\", \"statically typed programming language\", \"scientific calculator\")")
 fn str_replace(s: String, pattern: String, replacement: String) -> String =
 fn str_replace(s: String, pattern: String, replacement: String) -> String =
   if pattern == ""
   if pattern == ""
     then s
     then s
@@ -50,6 +60,7 @@ fn str_replace(s: String, pattern: String, replacement: String) -> String =
            else s
            else s
 
 
 @description("Repeat the input string `n` times")
 @description("Repeat the input string `n` times")
+@example("str_repeat(\"abc\", 4)")
 fn str_repeat(a: String, n: Scalar) -> String =
 fn str_repeat(a: String, n: Scalar) -> String =
   if n > 0
   if n > 0
     then str_append(a, str_repeat(a, n - 1))
     then str_append(a, str_repeat(a, n - 1))
@@ -76,7 +87,8 @@ fn _digit_in_base(base: Scalar, x: Scalar) -> String =
 # TODO: once we have anonymous functions / closures, we can implement base in a way
 # TODO: once we have anonymous functions / closures, we can implement base in a way
 # that it returns a partially-applied version of '_number_in_base'. This would allow
 # that it returns a partially-applied version of '_number_in_base'. This would allow
 # arbitrary 'x -> base(b)' conversions.
 # arbitrary 'x -> base(b)' conversions.
-@description("Convert a number to the given base. Example: `42 |> base(16)`")
+@description("Convert a number to the given base.")
+@example("42 |> base(16)")
 fn base(b: Scalar, x: Scalar) -> String =
 fn base(b: Scalar, x: Scalar) -> String =
   if x < 0
   if x < 0
     then "-{base(b, -x)}"
     then "-{base(b, -x)}"
@@ -84,14 +96,18 @@ fn base(b: Scalar, x: Scalar) -> String =
       then _digit_in_base(b, x)
       then _digit_in_base(b, x)
       else str_append(base(b, floor(x / b)), _digit_in_base(b, mod(x, b)))
       else str_append(base(b, floor(x / b)), _digit_in_base(b, mod(x, b)))
 
 
-@description("Get a binary representation of a number. Example: `42 -> bin`")
+@description("Get a binary representation of a number.")
+@example("42 -> bin")
 fn bin(x: Scalar) -> String = if x < 0 then "-{bin(-x)}" else "0b{base(2, x)}"
 fn bin(x: Scalar) -> String = if x < 0 then "-{bin(-x)}" else "0b{base(2, x)}"
 
 
-@description("Get an octal representation of a number. Example: `42 -> oct`")
+@description("Get an octal representation of a number.")
+@example("42 -> oct")
 fn oct(x: Scalar) -> String = if x < 0 then "-{oct(-x)}" else "0o{base(8, x)}"
 fn oct(x: Scalar) -> String = if x < 0 then "-{oct(-x)}" else "0o{base(8, x)}"
 
 
 @description("Get a decimal representation of a number.")
 @description("Get a decimal representation of a number.")
+@example("0b111 -> dec")
 fn dec(x: Scalar) -> String = base(10, x)
 fn dec(x: Scalar) -> String = base(10, x)
 
 
-@description("Get a hexadecimal representation of a number. Example: `2^31-1 -> hex`")
+@description("Get a hexadecimal representation of a number.")
+@example("2^31-1 -> hex")
 fn hex(x: Scalar) -> String = if x < 0 then "-{hex(-x)}" else "0x{base(16, x)}"
 fn hex(x: Scalar) -> String = if x < 0 then "-{hex(-x)}" else "0x{base(16, x)}"

+ 14 - 0
numbat/modules/datetime/functions.nbt

@@ -7,15 +7,22 @@ use units::time
 fn now() -> DateTime
 fn now() -> DateTime
 
 
 @description("Parses a string (date and time) into a `DateTime` object. See [here](./date-and-time.md#date-time-formats) for an overview of the supported formats.")
 @description("Parses a string (date and time) into a `DateTime` object. See [here](./date-and-time.md#date-time-formats) for an overview of the supported formats.")
+@example("datetime(\"2022-07-20T21:52+0200\")")
+@example("datetime(\"2022-07-20 21:52 Europe/Berlin\")")
+@example("datetime(\"2022/07/20 09:52 PM +0200\")")
 fn datetime(input: String) -> DateTime
 fn datetime(input: String) -> DateTime
 
 
 @description("Formats a `DateTime` object as a string.")
 @description("Formats a `DateTime` object as a string.")
+@example("format_datetime(\"This is a date in %B in the year %Y.\", datetime(\"2022-07-20 21:52 +0200\"))")
 fn format_datetime(format: String, input: DateTime) -> String
 fn format_datetime(format: String, input: DateTime) -> String
 
 
 @description("Returns the users local timezone.")
 @description("Returns the users local timezone.")
+@example("get_local_timezone()")
 fn get_local_timezone() -> String
 fn get_local_timezone() -> String
 
 
 @description("Returns a timezone conversion function, typically used with the conversion operator.")
 @description("Returns a timezone conversion function, typically used with the conversion operator.")
+@example("datetime(\"2022-07-20 21:52 +0200\") -> tz(\"Europe/Amsterdam\")")
+@example("datetime(\"2022-07-20 21:52 +0200\") -> tz(\"Asia/Taipei\")")
 fn tz(tz: String) -> Fn[(DateTime) -> DateTime]
 fn tz(tz: String) -> Fn[(DateTime) -> DateTime]
 
 
 @description("Timezone conversion function targeting the users local timezone (`datetime -> local`).")
 @description("Timezone conversion function targeting the users local timezone (`datetime -> local`).")
@@ -25,9 +32,11 @@ let local: Fn[(DateTime) -> DateTime] = tz(get_local_timezone())
 let UTC: Fn[(DateTime) -> DateTime] = tz("UTC")
 let UTC: Fn[(DateTime) -> DateTime] = tz("UTC")
 
 
 @description("Converts a `DateTime` to a UNIX timestamp. Can be used on the right hand side of a conversion operator: `now() -> unixtime`.")
 @description("Converts a `DateTime` to a UNIX timestamp. Can be used on the right hand side of a conversion operator: `now() -> unixtime`.")
+@example("datetime(\"2022-07-20 21:52 +0200\") -> unixtime")
 fn unixtime(input: DateTime) -> Scalar
 fn unixtime(input: DateTime) -> Scalar
 
 
 @description("Converts a UNIX timestamp to a `DateTime` object.")
 @description("Converts a UNIX timestamp to a `DateTime` object.")
+@example("from_unixtime(2^31)")
 fn from_unixtime(input: Scalar) -> DateTime
 fn from_unixtime(input: Scalar) -> DateTime
 
 
 fn _today_str() = format_datetime("%Y-%m-%d", now())
 fn _today_str() = format_datetime("%Y-%m-%d", now())
@@ -36,6 +45,7 @@ fn _today_str() = format_datetime("%Y-%m-%d", now())
 fn today() -> DateTime = datetime("{_today_str()} 00:00:00")
 fn today() -> DateTime = datetime("{_today_str()} 00:00:00")
 
 
 @description("Parses a string (only date) into a `DateTime` object.")
 @description("Parses a string (only date) into a `DateTime` object.")
+@example("date(\"2022-07-20\")")
 fn date(input: String) -> DateTime =
 fn date(input: String) -> DateTime =
   if str_contains(input, " ")
   if str_contains(input, " ")
     then datetime(str_replace(input, " ", " 00:00:00 "))
     then datetime(str_replace(input, " ", " 00:00:00 "))
@@ -50,6 +60,7 @@ fn _add_months(dt: DateTime, n_months: Scalar) -> DateTime
 fn _add_years(dt: DateTime, n_years: Scalar) -> DateTime
 fn _add_years(dt: DateTime, n_years: Scalar) -> DateTime
 
 
 @description("Adds the given time span to a `DateTime`. This uses leap-year and DST-aware calendar arithmetic with variable-length days, months, and years.")
 @description("Adds the given time span to a `DateTime`. This uses leap-year and DST-aware calendar arithmetic with variable-length days, months, and years.")
+@example("calendar_add(datetime(\"2022-07-20 21:52 +0200\"), 2 years)")
 fn calendar_add(dt: DateTime, span: Time) -> DateTime =
 fn calendar_add(dt: DateTime, span: Time) -> DateTime =
    if span_unit == days
    if span_unit == days
      then _add_days(dt, span / days)
      then _add_days(dt, span / days)
@@ -65,14 +76,17 @@ fn calendar_add(dt: DateTime, span: Time) -> DateTime =
     span_unit = unit_of(span)
     span_unit = unit_of(span)
 
 
 @description("Subtract the given time span from a `DateTime`. This uses leap-year and DST-aware calendar arithmetic with variable-length days, months, and years.")
 @description("Subtract the given time span from a `DateTime`. This uses leap-year and DST-aware calendar arithmetic with variable-length days, months, and years.")
+@example("calendar_sub(datetime(\"2022-07-20 21:52 +0200\"), 3 years)")
 fn calendar_sub(dt: DateTime, span: Time) -> DateTime =
 fn calendar_sub(dt: DateTime, span: Time) -> DateTime =
   calendar_add(dt, -span)
   calendar_add(dt, -span)
 
 
 @description("Get the day of the week from a given `DateTime`.")
 @description("Get the day of the week from a given `DateTime`.")
+@example("weekday(datetime(\"2022-07-20 21:52 +0200\"))")
 fn weekday(dt: DateTime) -> String = format_datetime("%A", dt)
 fn weekday(dt: DateTime) -> String = format_datetime("%A", dt)
 
 
 @name("Julian date")
 @name("Julian date")
 @description("Convert a `DateTime` to a Julian date, the number of days since the origin of the Julian date system (noon on November 24, 4714 BC in the proleptic Gregorian calendar).")
 @description("Convert a `DateTime` to a Julian date, the number of days since the origin of the Julian date system (noon on November 24, 4714 BC in the proleptic Gregorian calendar).")
 @url("https://en.wikipedia.org/wiki/Julian_day")
 @url("https://en.wikipedia.org/wiki/Julian_day")
+@example("julian_date(datetime(\"2022-07-20 21:52 +0200\"))")
 fn julian_date(dt: DateTime) -> Time =
 fn julian_date(dt: DateTime) -> Time =
   (dt - datetime("-4713-11-24 12:00:00 +0000")) -> days
   (dt - datetime("-4713-11-24 12:00:00 +0000")) -> days

+ 1 - 0
numbat/modules/datetime/human.nbt

@@ -55,4 +55,5 @@ fn _abs_human(time: Time) -> String =
 @name("Human-readable time duration")
 @name("Human-readable time duration")
 @url("https://numbat.dev/doc/date-and-time.html")
 @url("https://numbat.dev/doc/date-and-time.html")
 @description("Converts a time duration to a human-readable string in days, hours, minutes and seconds.")
 @description("Converts a time duration to a human-readable string in days, hours, minutes and seconds.")
+@example("century/1e6 -> human", "How long is a microcentury?")
 fn human(time: Time) -> String = _human_manage_past(abs(time) -> _abs_human, time)
 fn human(time: Time) -> String = _human_manage_past(abs(time) -> _abs_human, time)

+ 1 - 0
numbat/modules/extra/algebra.nbt

@@ -7,6 +7,7 @@ fn _qe_solution<A: Dim, B: Dim>(a: A, b: B, c: B² / A, sign: Scalar) -> B / A =
 @name("Solve quadratic equations")
 @name("Solve quadratic equations")
 @url("https://en.wikipedia.org/wiki/Quadratic_equation")
 @url("https://en.wikipedia.org/wiki/Quadratic_equation")
 @description("Returns the solutions of the equation a x² + b x + c = 0")
 @description("Returns the solutions of the equation a x² + b x + c = 0")
+@example("quadratic_equation(2, -1, -1)", "Solve the equation $2x² -x -1 = 0$")
 fn quadratic_equation<A: Dim, B: Dim>(a: A, b: B, c: B² / A) -> List<B / A> =
 fn quadratic_equation<A: Dim, B: Dim>(a: A, b: B, c: B² / A) -> List<B / A> =
   if a == 0
   if a == 0
     then if b == 0
     then if b == 0

+ 9 - 4
numbat/modules/extra/color.nbt

@@ -9,10 +9,12 @@ struct Color {
 }
 }
 
 
 @description("Create a `Color` from RGB (red, green, blue) values in the range $[0, 256)$.")
 @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 =
 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, e.g. `color(0xff7700)`")
+@description("Create a `Color` from a (hexadecimal) value.")
+@example("color(0xff7700)")
 fn color(rgb_hex: Scalar) -> Color =
 fn color(rgb_hex: Scalar) -> Color =
   rgb(
   rgb(
     floor(rgb_hex / 256^2),
     floor(rgb_hex / 256^2),
@@ -22,15 +24,18 @@ fn color(rgb_hex: Scalar) -> Color =
 fn _color_to_scalar(color: Color) -> Scalar =
 fn _color_to_scalar(color: Color) -> Scalar =
   color.red * 0x010000 + color.green * 0x000100 + color.blue
   color.red * 0x010000 + color.green * 0x000100 + color.blue
 
 
-@description("Convert a color to its RGB representation, e.g. `cyan -> color_rgb`")
+@description("Convert a color to its RGB representation.")
+@example("cyan -> color_rgb")
 fn color_rgb(color: Color) -> String =
 fn color_rgb(color: Color) -> String =
   "rgb({color.red}, {color.green}, {color.blue})"
   "rgb({color.red}, {color.green}, {color.blue})"
 
 
-@description("Convert a color to its RGB floating point representation, e.g. `cyan -> color_rgb_float`")
+@description("Convert a color to its RGB floating point representation.")
+@example("cyan -> color_rgb_float")
 fn color_rgb_float(color: Color) -> String =
 fn color_rgb_float(color: Color) -> String =
   "rgb({color.red / 255:.3}, {color.green / 255:.3}, {color.blue / 255:.3})"
   "rgb({color.red / 255:.3}, {color.green / 255:.3}, {color.blue / 255:.3})"
 
 
-@description("Convert a color to its hexadecimal representation, e.g. `rgb(225, 36, 143) -> color_hex`")
+@description("Convert a color to its hexadecimal representation.")
+@example("rgb(225, 36, 143) -> color_hex")
 fn color_hex(color: Color) -> String =
 fn color_hex(color: Color) -> String =
   str_append("#", str_replace(str_replace("{color -> _color_to_scalar -> hex:>8}", "0x", ""), " ", "0"))
   str_append("#", str_replace(str_replace("{color -> _color_to_scalar -> hex:>8}", "0x", ""), " ", "0"))
 
 

+ 1 - 0
numbat/modules/math/constants.nbt

@@ -9,6 +9,7 @@ let π = 3.14159265358979323846264338327950288
 
 
 @name("Tau")
 @name("Tau")
 @url("https://en.wikipedia.org/wiki/Turn_(angle)#Tau_proposals")
 @url("https://en.wikipedia.org/wiki/Turn_(angle)#Tau_proposals")
+@aliases(tau)
 let τ = 2 π
 let τ = 2 π
 
 
 @name("Euler's number")
 @name("Euler's number")

+ 2 - 0
numbat/modules/math/geometry.nbt

@@ -2,9 +2,11 @@ use core::functions
 use math::constants
 use math::constants
 
 
 @description("The length of the hypotenuse of a right-angled triangle $\\sqrt\{x^2+y^2\}$.")
 @description("The length of the hypotenuse of a right-angled triangle $\\sqrt\{x^2+y^2\}$.")
+@example("hypot2(3 m, 4 m)")
 fn hypot2<T: Dim>(x: T, y: T) -> T = sqrt(x^2 + y^2)
 fn hypot2<T: Dim>(x: T, y: T) -> T = sqrt(x^2 + y^2)
 
 
 @description("The Euclidean norm of a 3D vector $\\sqrt\{x^2+y^2+z^2\}$.")
 @description("The Euclidean norm of a 3D vector $\\sqrt\{x^2+y^2+z^2\}$.")
+@example("hypot3(8, 9, 12)")
 fn hypot3<T: Dim>(x: T, y: T, z: T) -> T = sqrt(x^2 + y^2 + z^2)
 fn hypot3<T: Dim>(x: T, y: T, z: T) -> T = sqrt(x^2 + y^2 + z^2)
 
 
 # The following functions use a generic dimension instead of
 # The following functions use a generic dimension instead of

+ 2 - 0
numbat/modules/math/number_theory.nbt

@@ -4,6 +4,7 @@ use core::functions
 @name("Greatest common divisor")
 @name("Greatest common divisor")
 @description("The largest positive integer that divides each of the integers $a$ and $b$.")
 @description("The largest positive integer that divides each of the integers $a$ and $b$.")
 @url("https://en.wikipedia.org/wiki/Greatest_common_divisor")
 @url("https://en.wikipedia.org/wiki/Greatest_common_divisor")
+@example("gcd(60, 42)")
 fn gcd(a: Scalar, b: Scalar) -> Scalar =
 fn gcd(a: Scalar, b: Scalar) -> Scalar =
   if b == 0
   if b == 0
     then abs(a)
     then abs(a)
@@ -12,4 +13,5 @@ fn gcd(a: Scalar, b: Scalar) -> Scalar =
 @name("Least common multiple")
 @name("Least common multiple")
 @description("The smallest positive integer that is divisible by both $a$ and $b$.")
 @description("The smallest positive integer that is divisible by both $a$ and $b$.")
 @url("https://en.wikipedia.org/wiki/Least_common_multiple")
 @url("https://en.wikipedia.org/wiki/Least_common_multiple")
+@example("lcm(14, 4)")
 fn lcm(a: Scalar, b: Scalar) -> Scalar = abs(a * b) / gcd(a, b)
 fn lcm(a: Scalar, b: Scalar) -> Scalar = abs(a * b) / gcd(a, b)

+ 9 - 3
numbat/modules/math/statistics.nbt

@@ -5,38 +5,44 @@ fn _max<D: Dim>(x: D, y: D) -> D = if x > y then x else y
 fn _min<D: Dim>(x: D, y: D) -> D = if x < y then x else y
 fn _min<D: Dim>(x: D, y: D) -> D = if x < y then x else y
 
 
 @name("Maxmimum")
 @name("Maxmimum")
-@description("Get the largest element of a list: `maximum([30 cm, 2 m]) = 2 m`.")
+@description("Get the largest element of a list.")
+@example("maximum([30 cm, 2 m])")
 fn maximum<D: Dim>(xs: List<D>) -> D =
 fn maximum<D: Dim>(xs: List<D>) -> D =
   if len(xs) == 1
   if len(xs) == 1
     then head(xs)
     then head(xs)
     else _max(head(xs), maximum(tail(xs)))
     else _max(head(xs), maximum(tail(xs)))
 
 
 @name("Minimum")
 @name("Minimum")
-@description("Get the smallest element of a list: `minimum([30 cm, 2 m]) = 30 cm`.")
+@description("Get the smallest element of a list.")
+@example("minimum([30 cm, 2 m])")
 fn minimum<D: Dim>(xs: List<D>) -> D =
 fn minimum<D: Dim>(xs: List<D>) -> D =
   if len(xs) == 1
   if len(xs) == 1
     then head(xs)
     then head(xs)
     else _min(head(xs), minimum(tail(xs)))
     else _min(head(xs), minimum(tail(xs)))
 
 
 @name("Arithmetic mean")
 @name("Arithmetic mean")
-@description("Calculate the arithmetic mean of a list of quantities: `mean([1 m, 2 m, 300 cm]) = 2 m`.")
+@description("Calculate the arithmetic mean of a list of quantities.")
+@example("mean([1 m, 2 m, 300 cm])")
 @url("https://en.wikipedia.org/wiki/Arithmetic_mean")
 @url("https://en.wikipedia.org/wiki/Arithmetic_mean")
 fn mean<D: Dim>(xs: List<D>) -> D = if is_empty(xs) then 0 else sum(xs) / len(xs)
 fn mean<D: Dim>(xs: List<D>) -> D = if is_empty(xs) then 0 else sum(xs) / len(xs)
 
 
 @name("Variance")
 @name("Variance")
 @url("https://en.wikipedia.org/wiki/Variance")
 @url("https://en.wikipedia.org/wiki/Variance")
 @description("Calculate the population variance of a list of quantities")
 @description("Calculate the population variance of a list of quantities")
+@example("variance([1 m, 2 m, 300 cm])")
 fn variance<D: Dim>(xs: List<D>) -> D^2 =
 fn variance<D: Dim>(xs: List<D>) -> D^2 =
   mean(map(sqr, xs)) - sqr(mean(xs))
   mean(map(sqr, xs)) - sqr(mean(xs))
 
 
 @name("Standard deviation")
 @name("Standard deviation")
 @url("https://en.wikipedia.org/wiki/Standard_deviation")
 @url("https://en.wikipedia.org/wiki/Standard_deviation")
 @description("Calculate the population standard deviation of a list of quantities")
 @description("Calculate the population standard deviation of a list of quantities")
+@example("stdev([1 m, 2 m, 300 cm])")
 fn stdev<D: Dim>(xs: List<D>) -> D = sqrt(variance(xs))
 fn stdev<D: Dim>(xs: List<D>) -> D = sqrt(variance(xs))
 
 
 @name("Median")
 @name("Median")
 @url("https://en.wikipedia.org/wiki/Median")
 @url("https://en.wikipedia.org/wiki/Median")
 @description("Calculate the median of a list of quantities")
 @description("Calculate the median of a list of quantities")
+@example("median([1 m, 2 m, 400 cm])")
 fn median<D: Dim>(xs: List<D>) -> D =  # TODO: this is extremely inefficient
 fn median<D: Dim>(xs: List<D>) -> D =  # TODO: this is extremely inefficient
   if mod(n, 2) == 1
   if mod(n, 2) == 1
     then element_at((n - 1) / 2, sort(xs))
     then element_at((n - 1) / 2, sort(xs))

+ 5 - 0
numbat/modules/math/transcendental.nbt

@@ -3,26 +3,31 @@ use core::scalar
 @name("Exponential function")
 @name("Exponential function")
 @description("The exponential function, $e^x$.")
 @description("The exponential function, $e^x$.")
 @url("https://en.wikipedia.org/wiki/Exponential_function")
 @url("https://en.wikipedia.org/wiki/Exponential_function")
+@example("exp(4)")
 fn exp(x: Scalar) -> Scalar
 fn exp(x: Scalar) -> Scalar
 
 
 @name("Natural logarithm")
 @name("Natural logarithm")
 @description("The natural logarithm with base $e$.")
 @description("The natural logarithm with base $e$.")
 @url("https://en.wikipedia.org/wiki/Natural_logarithm")
 @url("https://en.wikipedia.org/wiki/Natural_logarithm")
+@example("ln(20)")
 fn ln(x: Scalar) -> Scalar
 fn ln(x: Scalar) -> Scalar
 
 
 @name("Natural logarithm")
 @name("Natural logarithm")
 @description("The natural logarithm with base $e$.")
 @description("The natural logarithm with base $e$.")
 @url("https://en.wikipedia.org/wiki/Natural_logarithm")
 @url("https://en.wikipedia.org/wiki/Natural_logarithm")
+@example("log(20)")
 fn log(x: Scalar) -> Scalar = ln(x)
 fn log(x: Scalar) -> Scalar = ln(x)
 
 
 @name("Common logarithm")
 @name("Common logarithm")
 @description("The common logarithm with base $10$.")
 @description("The common logarithm with base $10$.")
 @url("https://en.wikipedia.org/wiki/Common_logarithm")
 @url("https://en.wikipedia.org/wiki/Common_logarithm")
+@example("log10(100)")
 fn log10(x: Scalar) -> Scalar
 fn log10(x: Scalar) -> Scalar
 
 
 @name("Binary logarithm")
 @name("Binary logarithm")
 @description("The binary logarithm with base $2$.")
 @description("The binary logarithm with base $2$.")
 @url("https://en.wikipedia.org/wiki/Binary_logarithm")
 @url("https://en.wikipedia.org/wiki/Binary_logarithm")
+@example("log2(256)")
 fn log2(x: Scalar) -> Scalar
 fn log2(x: Scalar) -> Scalar
 
 
 @name("Gamma function")
 @name("Gamma function")

+ 2 - 0
numbat/modules/numerics/diff.nbt

@@ -3,6 +3,8 @@ use core::quantities
 @name("Numerical differentiation")
 @name("Numerical differentiation")
 @url("https://en.wikipedia.org/wiki/Numerical_differentiation")
 @url("https://en.wikipedia.org/wiki/Numerical_differentiation")
 @description("Compute the numerical derivative of the function $f$ at point $x$ using the central difference method.")
 @description("Compute the numerical derivative of the function $f$ at point $x$ using the central difference method.")
+@example("fn polynomial(x) = x² - x - 1\ndiff(polynomial, 1)", "Compute the derivative of $f(x) = x² -x -1$ at $x=1$.")
+@example("fn distance(t) = 0.5 g0 t²\nfn velocity(t) = diff(distance, t)\nvelocity(2 s)", "Compute the free fall velocity after $t=2 s$.")
 fn diff<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x: X) -> Y / X =
 fn diff<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x: X) -> Y / X =
   (f(x + Δx) - f(x - Δx)) / 2 Δx
   (f(x + Δx) - f(x - Δx)) / 2 Δx
   where
   where

+ 1 - 0
numbat/modules/numerics/fixed_point.nbt

@@ -15,5 +15,6 @@ fn _fixed_point<X: Dim>(f: Fn[(X) -> X], x0: X, ε: X, max_iter: Scalar) =
 @name("Fixed-point iteration")
 @name("Fixed-point iteration")
 @url("https://en.wikipedia.org/wiki/Fixed-point_iteration")
 @url("https://en.wikipedia.org/wiki/Fixed-point_iteration")
 @description("Compute the approximate fixed point of a function $f: X \\rightarrow X$ starting from $x_0$, until $|f(x) - x| < ε$.")
 @description("Compute the approximate fixed point of a function $f: X \\rightarrow X$ starting from $x_0$, until $|f(x) - x| < ε$.")
+@example("fn function(x) = x/2 - 1\nfixed_point(function, 0, 0.01)", "Compute the fixed poin of $f(x) = x/2 -1$.")
 fn fixed_point<X: Dim>(f: Fn[(X) -> X], x0: X, ε: X) =
 fn fixed_point<X: Dim>(f: Fn[(X) -> X], x0: X, ε: X) =
   _fixed_point(f, x0, ε, 100)
   _fixed_point(f, x0, ε, 100)

+ 2 - 0
numbat/modules/numerics/solve.nbt

@@ -4,6 +4,7 @@ use core::error
 @name("Bisection method")
 @name("Bisection method")
 @url("https://en.wikipedia.org/wiki/Bisection_method")
 @url("https://en.wikipedia.org/wiki/Bisection_method")
 @description("Find the root of the function $f$ in the interval $[x_1, x_2]$ using the bisection method. The function $f$ must be continuous and $f(x_1) \cdot f(x_2) < 0$.")
 @description("Find the root of the function $f$ in the interval $[x_1, x_2]$ using the bisection method. The function $f$ must be continuous and $f(x_1) \cdot f(x_2) < 0$.")
+@example("fn f(x) = x² +x -2\nroot_bisect(f, 0, 100, 0.01, 0.01)", "Find the root of $f(x) = x² +x -2$ in the interval $[0, 100]$.")
 fn root_bisect<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x1: X, x2: X, x_tol: X, y_tol: Y) -> X =
 fn root_bisect<X: Dim, Y: Dim>(f: Fn[(X) -> Y], x1: X, x2: X, x_tol: X, y_tol: Y) -> X =
   if abs(x1 - x2) < x_tol
   if abs(x1 - x2) < x_tol
     then x_mean
     then x_mean
@@ -27,5 +28,6 @@ fn _root_newton_helper<X: Dim, Y: Dim>(f: Fn[(X) -> Y], f_prime: Fn[(X) -> Y / X
 @name("Newton's method")
 @name("Newton's method")
 @url("https://en.wikipedia.org/wiki/Newton%27s_method") 
 @url("https://en.wikipedia.org/wiki/Newton%27s_method") 
 @description("Find the root of the function $f(x)$ and its derivative $f'(x)$ using Newton's method.")
 @description("Find the root of the function $f(x)$ and its derivative $f'(x)$ using Newton's method.")
+@example("fn f(x) = x² -3x +2\nfn f_prime(x) = 2x -3\nroot_newton(f, f_prime, 0 , 0.01)", "Find a root of $f(x) = x² -3x +2$ using Newton's method.")
 fn root_newton<X: Dim, Y: Dim>(f: Fn[(X) -> Y], f_prime: Fn[(X) -> Y / X], x0: X, y_tol: Y) -> X =
 fn root_newton<X: Dim, Y: Dim>(f: Fn[(X) -> Y], f_prime: Fn[(X) -> Y / X], x0: X, y_tol: Y) -> X =
   _root_newton_helper(f, f_prime, x0, y_tol, 10_000)
   _root_newton_helper(f, f_prime, x0, y_tol, 10_000)

+ 4 - 0
numbat/modules/physics/temperature_conversion.nbt

@@ -5,10 +5,12 @@ use units::si
 let _offset_celsius = 273.15
 let _offset_celsius = 273.15
 
 
 @description("Converts from degree Celsius (°C) to Kelvin.")
 @description("Converts from degree Celsius (°C) to Kelvin.")
+@example("from_celsius(300)", "300 °C in Kelvin.")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 fn from_celsius(t_celsius: Scalar) -> Temperature = (t_celsius + _offset_celsius) kelvin
 fn from_celsius(t_celsius: Scalar) -> Temperature = (t_celsius + _offset_celsius) kelvin
 
 
 @description("Converts from Kelvin to degree Celcius (°C). This can be used on the right hand side of a conversion operator: `200 K -> celsius`.")
 @description("Converts from Kelvin to degree Celcius (°C). This can be used on the right hand side of a conversion operator: `200 K -> celsius`.")
+@example("300K -> celsius", "300 K in degree Celsius.")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 fn celsius(t_kelvin: Temperature) -> Scalar = t_kelvin / kelvin - _offset_celsius
 fn celsius(t_kelvin: Temperature) -> Scalar = t_kelvin / kelvin - _offset_celsius
 
 
@@ -16,9 +18,11 @@ let _offset_fahrenheit = 459.67
 let _scale_fahrenheit = 5 / 9
 let _scale_fahrenheit = 5 / 9
 
 
 @description("Converts from degree Fahrenheit (°F) to Kelvin.")
 @description("Converts from degree Fahrenheit (°F) to Kelvin.")
+@example("from_fahrenheit(300)", "300 °F in Kelvin.")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature = ((t_fahrenheit + _offset_fahrenheit) × _scale_fahrenheit) kelvin
 fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature = ((t_fahrenheit + _offset_fahrenheit) × _scale_fahrenheit) kelvin
 
 
 @description("Converts from Kelvin to degree Fahrenheit (°F). This can be used on the right hand side of a conversion operator: `200 K -> fahrenheit`.")
 @description("Converts from Kelvin to degree Fahrenheit (°F). This can be used on the right hand side of a conversion operator: `200 K -> fahrenheit`.")
+@example("300K -> fahrenheit", "300 K in degree Fahrenheit.")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 fn fahrenheit(t_kelvin: Temperature) -> Scalar = (t_kelvin / kelvin) / _scale_fahrenheit - _offset_fahrenheit
 fn fahrenheit(t_kelvin: Temperature) -> Scalar = (t_kelvin / kelvin) / _scale_fahrenheit - _offset_fahrenheit

+ 18 - 8
numbat/modules/units/mixed.nbt

@@ -2,26 +2,36 @@ use core::mixed_units
 use units::si
 use units::si
 use units::imperial
 use units::imperial
 
 
+@name("Unit list")
+@description("Convert a value to a mixed representation using the provided units.")
+@example("5500 m |> unit_list([miles, yards, feet, inches])")
+fn unit_list<D: Dim>(units: List<D>, value: D) -> List<D> = _mixed_unit_list(value, units, [])
+
 @name("Degrees, minutes, seconds")
 @name("Degrees, minutes, seconds")
 @description("Convert an angle to a mixed degrees, (arc)minutes, and (arc)seconds representation. Also called sexagesimal degree notation.")
 @description("Convert an angle to a mixed degrees, (arc)minutes, and (arc)seconds representation. Also called sexagesimal degree notation.")
 @url("https://en.wikipedia.org/wiki/Sexagesimal_degree")
 @url("https://en.wikipedia.org/wiki/Sexagesimal_degree")
-fn DMS(alpha: Angle) -> String =
-  _mixed_units(alpha, [deg, arcmin, arcsec], ["° ", "′ ", "″"], true)
+@example("46.5858° -> DMS")
+fn DMS(alpha: Angle) -> List<Angle> =
+  unit_list([degree, arcminute, arcsecond], alpha)
 
 
 @name("Degrees, decimal minutes")
 @name("Degrees, decimal minutes")
 @description("Convert an angle to a mixed degrees and decimal minutes representation.")
 @description("Convert an angle to a mixed degrees and decimal minutes representation.")
 @url("https://en.wikipedia.org/wiki/Decimal_degrees")
 @url("https://en.wikipedia.org/wiki/Decimal_degrees")
-fn DM(alpha: Angle) -> String =
-  _mixed_units(alpha, [deg, arcmin], ["° ", "′"], false)
+@example("46.5858° -> DM")
+fn DM(alpha: Angle) -> List<Angle> =
+  unit_list([degree, arcminute], alpha)
 
 
 @name("Feet and inches")
 @name("Feet and inches")
 @description("Convert a length to a mixed feet and inches representation.")
 @description("Convert a length to a mixed feet and inches representation.")
 @url("https://en.wikipedia.org/wiki/Foot_(unit)")
 @url("https://en.wikipedia.org/wiki/Foot_(unit)")
-fn feet_and_inches(length: Length) -> String =
-  _mixed_units(length, [foot, inch], [" ft ", " in"], false)
+@example("180 cm -> feet_and_inches")
+fn feet_and_inches(length: Length) -> List<Length> =
+  unit_list([foot, inch], length)
 
 
 @name("Pounds and ounces")
 @name("Pounds and ounces")
 @description("Convert a mass to a mixed pounds and ounces representation.")
 @description("Convert a mass to a mixed pounds and ounces representation.")
 @url("https://en.wikipedia.org/wiki/Pound_(mass)")
 @url("https://en.wikipedia.org/wiki/Pound_(mass)")
-fn pounds_and_ounces(mass: Mass) -> String =
-  _mixed_units(mass, [pound, ounce], [" lb ", " oz"], false)
+@example("1 kg -> pounds_and_ounces")
+fn pounds_and_ounces(mass: Mass) -> List<Mass> =
+  unit_list([pound, ounce], mass)
+

+ 2 - 2
numbat/modules/units/si.nbt

@@ -202,12 +202,12 @@ unit degree: Angle = π / 180 × radian
 
 
 @name("Minute of arc")
 @name("Minute of arc")
 @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc")
 @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc")
-@aliases(arcminutes, arcmin, ′)
+@aliases(arcminutes, arcmin, ′: short)
 unit arcminute: Angle = 1 / 60 × degree
 unit arcminute: Angle = 1 / 60 × degree
 
 
 @name("Second of arc")
 @name("Second of arc")
 @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc")
 @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc")
-@aliases(arcseconds, arcsec, ″)
+@aliases(arcseconds, arcsec, ″: short)
 unit arcsecond: Angle = 1 / 60 × arcminute
 unit arcsecond: Angle = 1 / 60 × arcminute
 
 
 @name("Are")
 @name("Are")

+ 23 - 18
numbat/src/ast.rs

@@ -36,21 +36,26 @@ impl PrettyPrint for BinaryOperator {
     fn pretty_print(&self) -> Markup {
     fn pretty_print(&self) -> Markup {
         use BinaryOperator::*;
         use BinaryOperator::*;
 
 
+        let operator = m::operator(match self {
+            Add => "+",
+            Sub => "-",
+            Mul => "×",
+            Div => "/",
+            Power => "^",
+            ConvertTo => "➞",
+            LessThan => "<",
+            GreaterThan => ">",
+            LessOrEqual => "≤",
+            GreaterOrEqual => "≥",
+            Equal => "==",
+            NotEqual => "≠",
+            LogicalAnd => "&&",
+            LogicalOr => "||",
+        });
+
         match self {
         match self {
-            Add => m::space() + m::operator("+") + m::space(),
-            Sub => m::space() + m::operator("-") + m::space(),
-            Mul => m::space() + m::operator("×") + m::space(),
-            Div => m::space() + m::operator("/") + m::space(),
-            Power => m::operator("^"),
-            ConvertTo => m::space() + m::operator("➞") + m::space(),
-            LessThan => m::space() + m::operator("<") + m::space(),
-            GreaterThan => m::space() + m::operator(">") + m::space(),
-            LessOrEqual => m::space() + m::operator("≤") + m::space(),
-            GreaterOrEqual => m::space() + m::operator("≥") + m::space(),
-            Equal => m::space() + m::operator("==") + m::space(),
-            NotEqual => m::space() + m::operator("≠") + m::space(),
-            LogicalAnd => m::space() + m::operator("&&") + m::space(),
-            LogicalOr => m::space() + m::operator("||") + m::space(),
+            Power => operator,
+            _ => m::space() + operator + m::space(),
         }
         }
     }
     }
 }
 }
@@ -61,7 +66,7 @@ pub enum StringPart<'a> {
     Interpolation {
     Interpolation {
         span: Span,
         span: Span,
         expr: Box<Expression<'a>>,
         expr: Box<Expression<'a>>,
-        format_specifiers: Option<String>,
+        format_specifiers: Option<&'a str>,
     },
     },
 }
 }
 
 
@@ -69,7 +74,7 @@ pub enum StringPart<'a> {
 pub enum Expression<'a> {
 pub enum Expression<'a> {
     Scalar(Span, Number),
     Scalar(Span, Number),
     Identifier(Span, &'a str),
     Identifier(Span, &'a str),
-    UnitIdentifier(Span, Prefix, String, String),
+    UnitIdentifier(Span, Prefix, String, String), // can't easily be made &'a str
     TypedHole(Span),
     TypedHole(Span),
     UnaryOperator {
     UnaryOperator {
         op: UnaryOperator,
         op: UnaryOperator,
@@ -365,7 +370,7 @@ impl PrettyPrint for TypeExpression {
     fn pretty_print(&self) -> Markup {
     fn pretty_print(&self) -> Markup {
         match self {
         match self {
             TypeExpression::Unity(_) => m::type_identifier("1"),
             TypeExpression::Unity(_) => m::type_identifier("1"),
-            TypeExpression::TypeIdentifier(_, ident) => m::type_identifier(ident),
+            TypeExpression::TypeIdentifier(_, ident) => m::type_identifier(ident.clone()),
             TypeExpression::Multiply(_, lhs, rhs) => {
             TypeExpression::Multiply(_, lhs, rhs) => {
                 lhs.pretty_print() + m::space() + m::operator("×") + m::space() + rhs.pretty_print()
                 lhs.pretty_print() + m::space() + m::operator("×") + m::space() + rhs.pretty_print()
             }
             }
@@ -507,7 +512,7 @@ impl ReplaceSpans for StringPart<'_> {
             } => StringPart::Interpolation {
             } => StringPart::Interpolation {
                 span: Span::dummy(),
                 span: Span::dummy(),
                 expr: Box::new(expr.replace_spans()),
                 expr: Box::new(expr.replace_spans()),
-                format_specifiers: format_specifiers.clone(),
+                format_specifiers: *format_specifiers,
             },
             },
         }
         }
     }
     }

+ 13 - 17
numbat/src/bytecode_interpreter.rs

@@ -69,15 +69,15 @@ impl BytecodeInterpreter {
                     .rposition(|l| &l.identifier == identifier)
                     .rposition(|l| &l.identifier == identifier)
                 {
                 {
                     self.vm.add_op1(Op::GetUpvalue, upvalue_position as u16);
                     self.vm.add_op1(Op::GetUpvalue, upvalue_position as u16);
-                } else if LAST_RESULT_IDENTIFIERS.contains(&identifier.as_str()) {
+                } else if LAST_RESULT_IDENTIFIERS.contains(identifier) {
                     self.vm.add_op(Op::GetLastResult);
                     self.vm.add_op(Op::GetLastResult);
-                } else if let Some(is_foreign) = self.functions.get(identifier) {
+                } else if let Some(is_foreign) = self.functions.get(*identifier) {
                     let index = self
                     let index = self
                         .vm
                         .vm
                         .add_constant(Constant::FunctionReference(if *is_foreign {
                         .add_constant(Constant::FunctionReference(if *is_foreign {
-                            FunctionReference::Foreign(identifier.clone())
+                            FunctionReference::Foreign(identifier.to_string())
                         } else {
                         } else {
-                            FunctionReference::Normal(identifier.clone())
+                            FunctionReference::Normal(identifier.to_string())
                         }));
                         }));
                     self.vm.add_op1(Op::LoadConstant, index);
                     self.vm.add_op1(Op::LoadConstant, index);
                 } else {
                 } else {
@@ -178,7 +178,7 @@ impl BytecodeInterpreter {
 
 
                 let sorted_exprs = exprs
                 let sorted_exprs = exprs
                     .iter()
                     .iter()
-                    .sorted_by_key(|(n, _)| struct_info.fields.get_index_of(n).unwrap());
+                    .sorted_by_key(|(n, _)| struct_info.fields.get_index_of(*n).unwrap());
 
 
                 for (_, expr) in sorted_exprs.rev() {
                 for (_, expr) in sorted_exprs.rev() {
                     self.compile_expression(expr)?;
                     self.compile_expression(expr)?;
@@ -198,7 +198,7 @@ impl BytecodeInterpreter {
                     );
                     );
                 };
                 };
 
 
-                let idx = struct_info.fields.get_index_of(attr).unwrap();
+                let idx = struct_info.fields.get_index_of(*attr).unwrap();
 
 
                 self.vm.add_op1(Op::AccessStructField, idx as u16);
                 self.vm.add_op1(Op::AccessStructField, idx as u16);
             }
             }
@@ -221,7 +221,7 @@ impl BytecodeInterpreter {
                 for part in string_parts {
                 for part in string_parts {
                     match part {
                     match part {
                         StringPart::Fixed(s) => {
                         StringPart::Fixed(s) => {
-                            let index = self.vm.add_constant(Constant::String(s.clone()));
+                            let index = self.vm.add_constant(Constant::String(s.to_string()));
                             self.vm.add_op1(Op::LoadConstant, index)
                             self.vm.add_op1(Op::LoadConstant, index)
                         }
                         }
                         StringPart::Interpolation {
                         StringPart::Interpolation {
@@ -231,7 +231,7 @@ impl BytecodeInterpreter {
                         } => {
                         } => {
                             self.compile_expression(expr)?;
                             self.compile_expression(expr)?;
                             let index = self.vm.add_constant(Constant::FormatSpecifiers(
                             let index = self.vm.add_constant(Constant::FormatSpecifiers(
-                                format_specifiers.clone(),
+                                format_specifiers.map(|s| s.to_string()),
                             ));
                             ));
                             self.vm.add_op1(Op::LoadConstant, index)
                             self.vm.add_op1(Op::LoadConstant, index)
                         }
                         }
@@ -335,7 +335,7 @@ impl BytecodeInterpreter {
                 let current_depth = self.current_depth();
                 let current_depth = self.current_depth();
                 for parameter in parameters {
                 for parameter in parameters {
                     self.locals[current_depth].push(Local {
                     self.locals[current_depth].push(Local {
-                        identifier: parameter.1.clone(),
+                        identifier: parameter.1.to_string(),
                         depth: current_depth,
                         depth: current_depth,
                         metadata: LocalMetadata::default(),
                         metadata: LocalMetadata::default(),
                     });
                     });
@@ -352,7 +352,7 @@ impl BytecodeInterpreter {
 
 
                 self.vm.end_function();
                 self.vm.end_function();
 
 
-                self.functions.insert(name.clone(), false);
+                self.functions.insert(name.to_string(), false);
             }
             }
             Statement::DefineFunction(
             Statement::DefineFunction(
                 name,
                 name,
@@ -371,7 +371,7 @@ impl BytecodeInterpreter {
                 self.vm
                 self.vm
                     .add_foreign_function(name, parameters.len()..=parameters.len());
                     .add_foreign_function(name, parameters.len()..=parameters.len());
 
 
-                self.functions.insert(name.clone(), true);
+                self.functions.insert(name.to_string(), true);
             }
             }
             Statement::DefineDimension(_name, _dexprs) => {
             Statement::DefineDimension(_name, _dexprs) => {
                 // Declaring a dimension is like introducing a new type. The information
                 // Declaring a dimension is like introducing a new type. The information
@@ -407,7 +407,7 @@ impl BytecodeInterpreter {
 
 
                 let constant_idx = self.vm.add_constant(Constant::Unit(Unit::new_base(
                 let constant_idx = self.vm.add_constant(Constant::Unit(Unit::new_base(
                     unit_name,
                     unit_name,
-                    crate::decorator::get_canonical_unit_name(unit_name.as_str(), &decorators[..]),
+                    crate::decorator::get_canonical_unit_name(unit_name, &decorators[..]),
                 )));
                 )));
                 for (name, _) in decorator::name_and_aliases(unit_name, decorators) {
                 for (name, _) in decorator::name_and_aliases(unit_name, decorators) {
                     self.unit_name_to_constant_index
                     self.unit_name_to_constant_index
@@ -436,11 +436,7 @@ impl BytecodeInterpreter {
                 let unit_information_idx = self.vm.add_unit_information(
                 let unit_information_idx = self.vm.add_unit_information(
                     unit_name,
                     unit_name,
                     Some(
                     Some(
-                        &crate::decorator::get_canonical_unit_name(
-                            unit_name.as_str(),
-                            &decorators[..],
-                        )
-                        .name,
+                        &crate::decorator::get_canonical_unit_name(unit_name, &decorators[..]).name,
                     ),
                     ),
                     UnitMetadata {
                     UnitMetadata {
                         type_: type_.to_concrete_type(), // We guarantee that derived-unit definitions do not contain generics, so no TGen(..)s can escape
                         type_: type_.to_concrete_type(), // We guarantee that derived-unit definitions do not contain generics, so no TGen(..)s can escape

+ 5 - 6
numbat/src/column_formatter.rs

@@ -37,16 +37,15 @@ impl ColumnFormatter {
 
 
             if min_num_columns < 1 {
             if min_num_columns < 1 {
                 for entry in entries {
                 for entry in entries {
-                    result += Markup::from(FormattedString(OutputType::Normal, format, entry))
-                        + m::whitespace(" ".repeat(self.padding));
+                    result +=
+                        Markup::from(FormattedString(OutputType::Normal, format, entry.into()))
+                            + m::whitespace(" ".repeat(self.padding));
                 }
                 }
                 return result;
                 return result;
             }
             }
 
 
             for num_columns in min_num_columns..=self.terminal_width {
             for num_columns in min_num_columns..=self.terminal_width {
-                // TODO: once we have Rust 1.73, use the div_ceil implementation:
-                // let num_rows = entries.len().div_ceil(num_columns);
-                let num_rows = (entries.len() + num_columns - 1) / num_columns;
+                let num_rows = entries.len().div_ceil(num_columns);
 
 
                 let mut table: Vec<Vec<Option<&str>>> = vec![vec![None; num_columns]; num_rows];
                 let mut table: Vec<Vec<Option<&str>>> = vec![vec![None; num_columns]; num_rows];
                 for (idx, entry) in entries.iter().enumerate() {
                 for (idx, entry) in entries.iter().enumerate() {
@@ -81,7 +80,7 @@ impl ColumnFormatter {
                             result += Markup::from(FormattedString(
                             result += Markup::from(FormattedString(
                                 OutputType::Normal,
                                 OutputType::Normal,
                                 format,
                                 format,
-                                (*entry).into(),
+                                entry.to_string().into(),
                             ));
                             ));
                             result += m::whitespace(" ".repeat(whitespace_length));
                             result += m::whitespace(" ".repeat(whitespace_length));
                         } else {
                         } else {

+ 1 - 1
numbat/src/datetime.rs

@@ -37,7 +37,7 @@ pub fn parse_datetime(input: &str) -> Result<Zoned, jiff::Error> {
 
 
     for format in FORMATS {
     for format in FORMATS {
         // Try to match the given format plus an additional UTC offset (%z)
         // Try to match the given format plus an additional UTC offset (%z)
-        if let Ok(dt) = Zoned::strptime(&format!("{format} %z"), input) {
+        if let Ok(dt) = Zoned::strptime(format!("{format} %z"), input) {
             return Ok(dt);
             return Ok(dt);
         }
         }
 
 

+ 74 - 25
numbat/src/decorator.rs

@@ -1,46 +1,75 @@
-use crate::{prefix_parser::AcceptsPrefix, unit::CanonicalName};
+use crate::{prefix_parser::AcceptsPrefix, span::Span, unit::CanonicalName};
 
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum Decorator<'a> {
 pub enum Decorator<'a> {
     MetricPrefixes,
     MetricPrefixes,
     BinaryPrefixes,
     BinaryPrefixes,
-    Aliases(Vec<(&'a str, Option<AcceptsPrefix>)>),
+    Aliases(Vec<(&'a str, Option<AcceptsPrefix>, Span)>),
     Url(String),
     Url(String),
     Name(String),
     Name(String),
     Description(String),
     Description(String),
+    Example(String, Option<String>),
 }
 }
 
 
-pub fn name_and_aliases<'a>(
+/// Get an iterator of data computed from a name and/or its alias's `AcceptsPrefix` and
+/// `Span`. If `name` itself is in the list of aliases, then it (or more precisely, the
+/// data computed from it) will be placed at the front of the iterator
+///
+/// `f` says how to turn a triple of data associated with `name` or an alias, `(name,
+/// accepts_prefix, Option<span>)`, into a `T`. The generality is really just here to
+/// decide whether to yield `(&'a String, AcceptsPrefix)` or a `(&'a String,
+/// AcceptsPrefix, Span)`.
+fn name_and_aliases_inner<'a, T: 'a>(
     name: &'a str,
     name: &'a str,
-    decorators: &[Decorator<'a>],
-) -> Box<dyn Iterator<Item = (&'a str, AcceptsPrefix)> + 'a> {
-    let aliases = {
-        let mut aliases_vec = vec![];
-        for decorator in decorators {
-            if let Decorator::Aliases(aliases) = decorator {
-                aliases_vec = aliases
-                    .iter()
-                    .map(|(name, accepts_prefix)| {
-                        (*name, accepts_prefix.unwrap_or(AcceptsPrefix::only_long()))
-                    })
-                    .collect();
+    decorators: &'a [Decorator],
+    f: impl 'a + Fn(&'a str, AcceptsPrefix, Option<Span>) -> T,
+) -> impl 'a + Iterator<Item = T> {
+    // contains all the aliases of `name`, starting with `name` itself
+    let mut aliases_vec = vec![f(name, AcceptsPrefix::only_long(), None)];
+
+    for decorator in decorators {
+        if let Decorator::Aliases(aliases) = decorator {
+            for (n, ap, span) in aliases {
+                let ap = ap.unwrap_or(AcceptsPrefix::only_long());
+                if *n == name {
+                    // use the AcceptsPrefix from the alias, but the span from `name`
+                    // itself; this way we always report a conflicting `name` first
+                    // before reporting any of its aliases. in effect we swallow aliases
+                    // equal to `name` itself (but keep their metadata)
+                    aliases_vec[0] = f(n, ap, None);
+                } else {
+                    aliases_vec.push(f(n, ap, Some(*span)));
+                }
             }
             }
         }
         }
-        aliases_vec
-    };
-
-    if !aliases.iter().any(|(n, _)| n == &name) {
-        let name_iter = std::iter::once((name, AcceptsPrefix::only_long()));
-        Box::new(name_iter.chain(aliases))
-    } else {
-        Box::new(aliases.into_iter())
     }
     }
+
+    aliases_vec.into_iter()
+}
+
+/// Returns iterator of `(name_or_alias, accepts_prefix)` for the given name
+pub fn name_and_aliases<'a>(
+    name: &'a str,
+    decorators: &'a [Decorator],
+) -> impl 'a + Iterator<Item = (&'a str, AcceptsPrefix)> {
+    name_and_aliases_inner(name, decorators, |n, accepts_prefix, _| (n, accepts_prefix))
+}
+
+/// Returns iterator of `(name_or_alias, accepts_prefix, span)` for the given name
+pub fn name_and_aliases_spans<'a>(
+    name: &'a str,
+    name_span: Span,
+    decorators: &'a [Decorator],
+) -> impl 'a + Iterator<Item = (&'a str, AcceptsPrefix, Span)> {
+    name_and_aliases_inner(name, decorators, move |n, accepts_prefix, span| {
+        (n, accepts_prefix, span.unwrap_or(name_span))
+    })
 }
 }
 
 
 pub fn get_canonical_unit_name(unit_name: &str, decorators: &[Decorator]) -> CanonicalName {
 pub fn get_canonical_unit_name(unit_name: &str, decorators: &[Decorator]) -> CanonicalName {
     for decorator in decorators {
     for decorator in decorators {
         if let Decorator::Aliases(aliases) = decorator {
         if let Decorator::Aliases(aliases) = decorator {
-            for (alias, accepts_prefix) in aliases {
+            for (alias, accepts_prefix, _) in aliases {
                 match accepts_prefix {
                 match accepts_prefix {
                     &Some(ap) if ap.short => {
                     &Some(ap) if ap.short => {
                         return CanonicalName::new(alias, ap);
                         return CanonicalName::new(alias, ap);
@@ -89,10 +118,20 @@ pub fn description(decorators: &[Decorator]) -> Option<String> {
     }
     }
 }
 }
 
 
+pub fn examples(decorators: &[Decorator]) -> Vec<(String, Option<String>)> {
+    let mut examples = Vec::new();
+    for decorator in decorators {
+        if let Decorator::Example(example_code, example_description) = decorator {
+            examples.push((example_code.clone(), example_description.clone()));
+        }
+    }
+    examples
+}
+
 pub fn contains_aliases_with_prefixes(decorates: &[Decorator]) -> bool {
 pub fn contains_aliases_with_prefixes(decorates: &[Decorator]) -> bool {
     for decorator in decorates {
     for decorator in decorates {
         if let Decorator::Aliases(aliases) = decorator {
         if let Decorator::Aliases(aliases) = decorator {
-            if aliases.iter().any(|(_, prefixes)| prefixes.is_some()) {
+            if aliases.iter().any(|(_, prefixes, _)| prefixes.is_some()) {
                 return true;
                 return true;
             }
             }
         }
         }
@@ -110,3 +149,13 @@ pub fn contains_aliases(decorators: &[Decorator]) -> bool {
 
 
     false
     false
 }
 }
+
+pub fn contains_examples(decorators: &[Decorator]) -> bool {
+    for decorator in decorators {
+        if let Decorator::Example(_, _) = decorator {
+            return true;
+        }
+    }
+
+    false
+}

+ 1 - 1
numbat/src/help.rs

@@ -62,7 +62,7 @@ pub fn help_markup() -> m::Markup {
     let mut example_context = Context::new(BuiltinModuleImporter::default());
     let mut example_context = Context::new(BuiltinModuleImporter::default());
     let _use_prelude_output = evaluate_example(&mut example_context, "use prelude");
     let _use_prelude_output = evaluate_example(&mut example_context, "use prelude");
     for example in examples.iter() {
     for example in examples.iter() {
-        output += m::text(">>> ") + m::text(example) + m::nl();
+        output += m::text(">>> ") + m::text(*example) + m::nl();
         output += evaluate_example(&mut example_context, example) + m::nl();
         output += evaluate_example(&mut example_context, example) + m::nl();
     }
     }
     output
     output

+ 1 - 1
numbat/src/interpreter/mod.rs

@@ -210,7 +210,7 @@ mod tests {
             .expect("No name resolution errors for inputs in this test suite");
             .expect("No name resolution errors for inputs in this test suite");
         let mut typechecker = crate::typechecker::TypeChecker::default();
         let mut typechecker = crate::typechecker::TypeChecker::default();
         let statements_typechecked = typechecker
         let statements_typechecked = typechecker
-            .check(statements_transformed)
+            .check(&statements_transformed)
             .expect("No type check errors for inputs in this test suite");
             .expect("No type check errors for inputs in this test suite");
         BytecodeInterpreter::new().interpret_statements(
         BytecodeInterpreter::new().interpret_statements(
             &mut InterpreterSettings::default(),
             &mut InterpreterSettings::default(),

+ 71 - 60
numbat/src/lib.rs

@@ -47,6 +47,8 @@ mod unit_registry;
 pub mod value;
 pub mod value;
 mod vm;
 mod vm;
 
 
+use std::borrow::Cow;
+
 use bytecode_interpreter::BytecodeInterpreter;
 use bytecode_interpreter::BytecodeInterpreter;
 use column_formatter::ColumnFormatter;
 use column_formatter::ColumnFormatter;
 use currency::ExchangeRatesCache;
 use currency::ExchangeRatesCache;
@@ -168,6 +170,7 @@ impl Context {
             String,
             String,
             Option<String>,
             Option<String>,
             Option<String>,
             Option<String>,
+            Vec<(String, Option<String>)>,
             CodeSource,
             CodeSource,
         ),
         ),
     > + '_ {
     > + '_ {
@@ -185,6 +188,7 @@ impl Context {
                         .to_string(),
                         .to_string(),
                     meta.description.clone(),
                     meta.description.clone(),
                     meta.url.clone(),
                     meta.url.clone(),
+                    meta.examples.clone(),
                     self.resolver
                     self.resolver
                         .get_code_source(signature.definition_span.code_source_id),
                         .get_code_source(signature.definition_span.code_source_id),
                 )
                 )
@@ -200,15 +204,6 @@ impl Context {
     }
     }
 
 
     pub fn print_environment(&self) -> Markup {
     pub fn print_environment(&self) -> Markup {
-        let mut functions: Vec<_> = self.function_names().collect();
-        functions.sort();
-        let mut dimensions = Vec::from(self.dimension_names());
-        dimensions.sort();
-        let mut units = Vec::from(self.unit_names());
-        units.sort();
-        let mut variables: Vec<_> = self.variable_names().collect();
-        variables.sort();
-
         let mut output = m::empty();
         let mut output = m::empty();
 
 
         output += m::emphasized("List of functions:") + m::nl();
         output += m::emphasized("List of functions:") + m::nl();
@@ -253,11 +248,11 @@ impl Context {
     /// Gets completions for the given word_part
     /// Gets completions for the given word_part
     ///
     ///
     /// If `add_paren` is true, then an opening paren will be added to the end of function names
     /// If `add_paren` is true, then an opening paren will be added to the end of function names
-    pub fn get_completions_for<'a>(
+    pub fn get_completions_for(
         &self,
         &self,
-        word_part: &'a str,
+        word_part: &str,
         add_paren: bool,
         add_paren: bool,
-    ) -> impl Iterator<Item = String> + 'a {
+    ) -> impl Iterator<Item = String> {
         const COMMON_METRIC_PREFIXES: &[&str] = &[
         const COMMON_METRIC_PREFIXES: &[&str] = &[
             "pico", "nano", "micro", "milli", "centi", "kilo", "mega", "giga", "tera",
             "pico", "nano", "micro", "milli", "centi", "kilo", "mega", "giga", "tera",
         ];
         ];
@@ -270,53 +265,60 @@ impl Context {
             })
             })
             .collect();
             .collect();
 
 
-        let mut words: Vec<_> = KEYWORDS.iter().map(|k| k.to_string()).collect();
+        let mut words = Vec::new();
+
+        let mut add_if_valid = |word: Cow<'_, str>| {
+            if word.starts_with(word_part) {
+                words.push(word.into_owned());
+            }
+        };
+
+        for kw in KEYWORDS {
+            add_if_valid((*kw).into());
+        }
 
 
         for (patterns, _) in UNICODE_INPUT {
         for (patterns, _) in UNICODE_INPUT {
             for pattern in *patterns {
             for pattern in *patterns {
-                words.push(pattern.to_string());
+                add_if_valid((*pattern).into());
             }
             }
         }
         }
 
 
-        {
-            for variable in self.variable_names() {
-                words.push(variable.clone());
-            }
+        for variable in self.variable_names() {
+            add_if_valid(variable.into());
+        }
 
 
-            for function in self.function_names() {
-                if add_paren {
-                    words.push(format!("{function}("));
-                } else {
-                    words.push(function.to_string());
-                }
+        for mut function in self.function_names() {
+            if add_paren {
+                function.push('(');
             }
             }
+            add_if_valid(function.into());
+        }
 
 
-            for dimension in self.dimension_names() {
-                words.push(dimension.clone());
-            }
+        for dimension in self.dimension_names() {
+            add_if_valid(dimension.into());
+        }
 
 
-            for (_, (_, meta)) in self.unit_representations() {
-                for (unit, accepts_prefix) in meta.aliases {
-                    words.push(unit.clone());
-
-                    // Add some of the common long prefixes for units that accept them.
-                    // We do not add all possible prefixes here in order to keep the
-                    // number of completions to a reasonable size. Also, we do not add
-                    // short prefixes for units that accept them, as that leads to lots
-                    // and lots of 2-3 character words.
-                    if accepts_prefix.long && meta.metric_prefixes {
-                        for prefix in &metric_prefixes {
-                            words.push(format!("{prefix}{unit}"));
-                        }
+        for (_, (_, meta)) in self.unit_representations() {
+            for (unit, accepts_prefix) in meta.aliases {
+                // Add some of the common long prefixes for units that accept them.
+                // We do not add all possible prefixes here in order to keep the
+                // number of completions to a reasonable size. Also, we do not add
+                // short prefixes for units that accept them, as that leads to lots
+                // and lots of 2-3 character words.
+                if accepts_prefix.long && meta.metric_prefixes {
+                    for prefix in &metric_prefixes {
+                        add_if_valid(format!("{prefix}{unit}").into());
                     }
                     }
                 }
                 }
+
+                add_if_valid(unit.into());
             }
             }
         }
         }
 
 
         words.sort();
         words.sort();
         words.dedup();
         words.dedup();
 
 
-        words.into_iter().filter(move |w| w.starts_with(word_part))
+        words.into_iter()
     }
     }
 
 
     pub fn print_info_for_keyword(&mut self, keyword: &str) -> Markup {
     pub fn print_info_for_keyword(&mut self, keyword: &str) -> Markup {
@@ -336,7 +338,8 @@ impl Context {
                 .ok()
                 .ok()
                 .map(|(_, md)| md)
                 .map(|(_, md)| md)
             {
             {
-                let mut help = m::text("Unit: ") + m::unit(md.name.as_deref().unwrap_or(keyword));
+                let mut help =
+                    m::text("Unit: ") + m::unit(md.name.unwrap_or_else(|| keyword.to_string()));
                 if let Some(url) = &md.url {
                 if let Some(url) = &md.url {
                     help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
                     help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
                 }
                 }
@@ -357,12 +360,13 @@ impl Context {
                     let desc = "Description: ";
                     let desc = "Description: ";
                     let mut lines = description.lines();
                     let mut lines = description.lines();
                     help += m::text(desc)
                     help += m::text(desc)
-                        + m::text(lines.by_ref().next().unwrap_or("").trim())
+                        + m::text(lines.by_ref().next().unwrap_or("").trim().to_string())
                         + m::nl();
                         + m::nl();
 
 
                     for line in lines {
                     for line in lines {
-                        help +=
-                            m::whitespace(" ".repeat(desc.len())) + m::text(line.trim()) + m::nl();
+                        help += m::whitespace(" ".repeat(desc.len()))
+                            + m::text(line.trim().to_string())
+                            + m::nl();
                     }
                     }
                 }
                 }
 
 
@@ -385,17 +389,17 @@ impl Context {
                     if !prefix.is_none() {
                     if !prefix.is_none() {
                         help += m::nl()
                         help += m::nl()
                             + m::value("1 ")
                             + m::value("1 ")
-                            + m::unit(keyword)
+                            + m::unit(keyword.to_string())
                             + m::text(" = ")
                             + m::text(" = ")
                             + m::value(prefix.factor().pretty_print())
                             + m::value(prefix.factor().pretty_print())
                             + m::space()
                             + m::space()
-                            + m::unit(&full_name);
+                            + m::unit(full_name.to_string());
                     }
                     }
 
 
                     if let Some(BaseUnitAndFactor(prod, num)) = x {
                     if let Some(BaseUnitAndFactor(prod, num)) = x {
                         help += m::nl()
                         help += m::nl()
                             + m::value("1 ")
                             + m::value("1 ")
-                            + m::unit(&full_name)
+                            + m::unit(full_name.to_string())
                             + m::text(" = ")
                             + m::text(" = ")
                             + m::value(num.pretty_print())
                             + m::value(num.pretty_print())
                             + m::space()
                             + m::space()
@@ -407,7 +411,8 @@ impl Context {
                                 Some(m::FormatType::Unit),
                                 Some(m::FormatType::Unit),
                             );
                             );
                     } else {
                     } else {
-                        help += m::nl() + m::unit(&full_name) + m::text(" is a base unit");
+                        help +=
+                            m::nl() + m::unit(full_name.to_string()) + m::text(" is a base unit");
                     }
                     }
                 };
                 };
 
 
@@ -420,9 +425,9 @@ impl Context {
         if let Some(l) = self.interpreter.lookup_global(keyword) {
         if let Some(l) = self.interpreter.lookup_global(keyword) {
             let mut help = m::text("Variable: ");
             let mut help = m::text("Variable: ");
             if let Some(name) = &l.metadata.name {
             if let Some(name) = &l.metadata.name {
-                help += m::text(name);
+                help += m::text(name.clone());
             } else {
             } else {
-                help += m::identifier(keyword);
+                help += m::identifier(keyword.to_string());
             }
             }
             if let Some(url) = &l.metadata.url {
             if let Some(url) = &l.metadata.url {
                 help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
                 help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
@@ -432,11 +437,14 @@ impl Context {
             if let Some(description) = &l.metadata.description {
             if let Some(description) = &l.metadata.description {
                 let desc = "Description: ";
                 let desc = "Description: ";
                 let mut lines = description.lines();
                 let mut lines = description.lines();
-                help +=
-                    m::text(desc) + m::text(lines.by_ref().next().unwrap_or("").trim()) + m::nl();
+                help += m::text(desc)
+                    + m::text(lines.by_ref().next().unwrap_or("").trim().to_string())
+                    + m::nl();
 
 
                 for line in lines {
                 for line in lines {
-                    help += m::whitespace(" ".repeat(desc.len())) + m::text(line.trim()) + m::nl();
+                    help += m::whitespace(" ".repeat(desc.len()))
+                        + m::text(line.trim().to_string())
+                        + m::nl();
                 }
                 }
             }
             }
 
 
@@ -465,9 +473,9 @@ impl Context {
 
 
             let mut help = m::text("Function:    ");
             let mut help = m::text("Function:    ");
             if let Some(name) = &metadata.name {
             if let Some(name) = &metadata.name {
-                help += m::text(name);
+                help += m::text(name.to_string());
             } else {
             } else {
-                help += m::identifier(keyword);
+                help += m::identifier(keyword.to_string());
             }
             }
             if let Some(url) = &metadata.url {
             if let Some(url) = &metadata.url {
                 help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
                 help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
@@ -482,11 +490,14 @@ impl Context {
             if let Some(description) = &metadata.description {
             if let Some(description) = &metadata.description {
                 let desc = "Description: ";
                 let desc = "Description: ";
                 let mut lines = description.lines();
                 let mut lines = description.lines();
-                help +=
-                    m::text(desc) + m::text(lines.by_ref().next().unwrap_or("").trim()) + m::nl();
+                help += m::text(desc)
+                    + m::text(lines.by_ref().next().unwrap_or("").trim().to_string())
+                    + m::nl();
 
 
                 for line in lines {
                 for line in lines {
-                    help += m::whitespace(" ".repeat(desc.len())) + m::text(line.trim()) + m::nl();
+                    help += m::whitespace(" ".repeat(desc.len()))
+                        + m::text(line.trim().to_string())
+                        + m::nl();
                 }
                 }
             }
             }
 
 
@@ -584,7 +595,7 @@ impl Context {
 
 
         let result = self
         let result = self
             .typechecker
             .typechecker
-            .check(transformed_statements)
+            .check(&transformed_statements)
             .map_err(|err| NumbatError::TypeCheckError(*err));
             .map_err(|err| NumbatError::TypeCheckError(*err));
 
 
         if result.is_err() {
         if result.is_err() {

+ 35 - 32
numbat/src/markup.rs

@@ -1,4 +1,4 @@
-use std::fmt::Display;
+use std::{borrow::Cow, fmt::Display};
 
 
 #[derive(Debug, Copy, Clone, PartialEq)]
 #[derive(Debug, Copy, Clone, PartialEq)]
 pub enum FormatType {
 pub enum FormatType {
@@ -23,7 +23,7 @@ pub enum OutputType {
 }
 }
 
 
 #[derive(Debug, Clone, PartialEq)]
 #[derive(Debug, Clone, PartialEq)]
-pub struct FormattedString(pub OutputType, pub FormatType, pub String);
+pub struct FormattedString(pub OutputType, pub FormatType, pub Cow<'static, str>);
 
 
 #[derive(Debug, Clone, Default, PartialEq)]
 #[derive(Debug, Clone, Default, PartialEq)]
 pub struct Markup(pub Vec<FormattedString>);
 pub struct Markup(pub Vec<FormattedString>);
@@ -43,10 +43,9 @@ impl Display for Markup {
 impl std::ops::Add for Markup {
 impl std::ops::Add for Markup {
     type Output = Markup;
     type Output = Markup;
 
 
-    fn add(self, rhs: Self) -> Self::Output {
-        let mut res = self.0;
-        res.extend_from_slice(&rhs.0);
-        Markup(res)
+    fn add(mut self, rhs: Self) -> Self::Output {
+        self.0.extend(rhs.0);
+        self
     }
     }
 }
 }
 
 
@@ -66,7 +65,7 @@ pub fn space() -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Whitespace,
         FormatType::Whitespace,
-        " ".to_string(),
+        " ".into(),
     ))
     ))
 }
 }
 
 
@@ -74,99 +73,99 @@ pub fn empty() -> Markup {
     Markup::default()
     Markup::default()
 }
 }
 
 
-pub fn whitespace(text: impl AsRef<str>) -> Markup {
+pub fn whitespace(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Whitespace,
         FormatType::Whitespace,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn emphasized(text: impl AsRef<str>) -> Markup {
+pub fn emphasized(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Emphasized,
         FormatType::Emphasized,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn dimmed(text: impl AsRef<str>) -> Markup {
+pub fn dimmed(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Dimmed,
         FormatType::Dimmed,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn text(text: impl AsRef<str>) -> Markup {
+pub fn text(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Text,
         FormatType::Text,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn string(text: impl AsRef<str>) -> Markup {
+pub fn string(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::String,
         FormatType::String,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn keyword(text: impl AsRef<str>) -> Markup {
+pub fn keyword(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Keyword,
         FormatType::Keyword,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn value(text: impl AsRef<str>) -> Markup {
+pub fn value(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Value,
         FormatType::Value,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn unit(text: impl AsRef<str>) -> Markup {
+pub fn unit(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Unit,
         FormatType::Unit,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn identifier(text: impl AsRef<str>) -> Markup {
+pub fn identifier(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Identifier,
         FormatType::Identifier,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn type_identifier(text: impl AsRef<str>) -> Markup {
+pub fn type_identifier(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::TypeIdentifier,
         FormatType::TypeIdentifier,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn operator(text: impl AsRef<str>) -> Markup {
+pub fn operator(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Operator,
         FormatType::Operator,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
-pub fn decorator(text: impl AsRef<str>) -> Markup {
+pub fn decorator(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
     Markup::from(FormattedString(
         OutputType::Normal,
         OutputType::Normal,
         FormatType::Decorator,
         FormatType::Decorator,
-        text.as_ref().to_string(),
+        text.into(),
     ))
     ))
 }
 }
 
 
@@ -206,6 +205,10 @@ pub struct PlainTextFormatter;
 
 
 impl Formatter for PlainTextFormatter {
 impl Formatter for PlainTextFormatter {
     fn format_part(&self, FormattedString(_, _, text): &FormattedString) -> String {
     fn format_part(&self, FormattedString(_, _, text): &FormattedString) -> String {
-        text.clone()
+        text.to_string()
     }
     }
 }
 }
+
+pub fn plain_text_format(m: &Markup, indent: bool) -> String {
+    PlainTextFormatter {}.format(m, indent)
+}

+ 135 - 12
numbat/src/parser.rs

@@ -96,6 +96,9 @@ pub enum ParseErrorKind {
     #[error("Trailing '=' sign. Use `let {0} = …` if you intended to define a new constant.")]
     #[error("Trailing '=' sign. Use `let {0} = …` if you intended to define a new constant.")]
     TrailingEqualSign(String),
     TrailingEqualSign(String),
 
 
+    #[error("Trailing '=' sign. Use `fn {0} = …` if you intended to define a function.")]
+    TrailingEqualSignFunction(String),
+
     #[error("Expected identifier after 'let' keyword")]
     #[error("Expected identifier after 'let' keyword")]
     ExpectedIdentifierAfterLet,
     ExpectedIdentifierAfterLet,
 
 
@@ -198,6 +201,9 @@ pub enum ParseErrorKind {
     #[error("Aliases cannot be used on functions.")]
     #[error("Aliases cannot be used on functions.")]
     AliasUsedOnFunction,
     AliasUsedOnFunction,
 
 
+    #[error("Example decorators can only be used on functions.")]
+    ExampleUsedOnUnsuitableKind,
+
     #[error("Numerical overflow in dimension exponent")]
     #[error("Numerical overflow in dimension exponent")]
     OverflowInDimensionExponent,
     OverflowInDimensionExponent,
 
 
@@ -310,12 +316,22 @@ impl<'a> Parser<'a> {
                     break;
                     break;
                 }
                 }
                 TokenKind::Equal => {
                 TokenKind::Equal => {
+                    let last_token = self.last(tokens).unwrap();
+
+                    let mut input = String::new();
+                    for token in tokens.iter().take(self.current) {
+                        input.push_str(token.lexeme);
+                    }
+
                     errors.push(ParseError {
                     errors.push(ParseError {
-                        kind: ParseErrorKind::TrailingEqualSign(
-                            self.last(tokens).unwrap().lexeme.to_owned(),
-                        ),
+                        kind: if last_token.kind == TokenKind::RightParen {
+                            ParseErrorKind::TrailingEqualSignFunction(input)
+                        } else {
+                            ParseErrorKind::TrailingEqualSign(input)
+                        },
                         span: self.peek(tokens).span,
                         span: self.peek(tokens).span,
                     });
                     });
+
                     self.recover_from_error(tokens);
                     self.recover_from_error(tokens);
                 }
                 }
                 _ => {
                 _ => {
@@ -369,14 +385,17 @@ impl<'a> Parser<'a> {
     fn list_of_aliases(
     fn list_of_aliases(
         &mut self,
         &mut self,
         tokens: &[Token<'a>],
         tokens: &[Token<'a>],
-    ) -> Result<Vec<(&'a str, Option<AcceptsPrefix>)>> {
+    ) -> Result<Vec<(&'a str, Option<AcceptsPrefix>, Span)>> {
         if self.match_exact(tokens, TokenKind::RightParen).is_some() {
         if self.match_exact(tokens, TokenKind::RightParen).is_some() {
             return Ok(vec![]);
             return Ok(vec![]);
         }
         }
 
 
-        let mut identifiers = vec![(self.identifier(tokens)?, self.accepts_prefix(tokens)?)];
+        let span = self.peek(tokens).span;
+        let mut identifiers = vec![(self.identifier(tokens)?, self.accepts_prefix(tokens)?, span)];
+
         while self.match_exact(tokens, TokenKind::Comma).is_some() {
         while self.match_exact(tokens, TokenKind::Comma).is_some() {
-            identifiers.push((self.identifier(tokens)?, self.accepts_prefix(tokens)?));
+            let span = self.peek(tokens).span;
+            identifiers.push((self.identifier(tokens)?, self.accepts_prefix(tokens)?, span));
         }
         }
 
 
         if self.match_exact(tokens, TokenKind::RightParen).is_none() {
         if self.match_exact(tokens, TokenKind::RightParen).is_none() {
@@ -455,6 +474,14 @@ impl<'a> Parser<'a> {
                             span: self.peek(tokens).span,
                             span: self.peek(tokens).span,
                         });
                         });
                     }
                     }
+
+                    if decorator::contains_examples(&self.decorator_stack) {
+                        return Err(ParseError {
+                            kind: ParseErrorKind::ExampleUsedOnUnsuitableKind,
+                            span: self.peek(tokens).span,
+                        });
+                    }
+
                     std::mem::swap(&mut decorators, &mut self.decorator_stack);
                     std::mem::swap(&mut decorators, &mut self.decorator_stack);
                 }
                 }
 
 
@@ -734,6 +761,55 @@ impl<'a> Parser<'a> {
                         });
                         });
                     }
                     }
                 }
                 }
+                "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() {
+                                //Code and description
+                                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,
+                                        ));
+                                    }
+
+                                    Decorator::Example(
+                                        strip_and_escape(token_code.lexeme),
+                                        Some(strip_and_escape(token_description.lexeme)),
+                                    )
+                                } else {
+                                    return Err(ParseError {
+                                        kind: ParseErrorKind::ExpectedString,
+                                        span: self.peek(tokens).span,
+                                    });
+                                }
+                            } else {
+                                //Code but no description
+                                if self.match_exact(tokens, TokenKind::RightParen).is_none() {
+                                    return Err(ParseError::new(
+                                        ParseErrorKind::MissingClosingParen,
+                                        self.peek(tokens).span,
+                                    ));
+                                }
+
+                                Decorator::Example(strip_and_escape(token_code.lexeme), None)
+                            }
+                        } else {
+                            return Err(ParseError {
+                                kind: ParseErrorKind::ExpectedString,
+                                span: self.peek(tokens).span,
+                            });
+                        }
+                    } else {
+                        return Err(ParseError {
+                            kind: ParseErrorKind::ExpectedLeftParenAfterDecorator,
+                            span: self.peek(tokens).span,
+                        });
+                    }
+                }
                 _ => {
                 _ => {
                     return Err(ParseError {
                     return Err(ParseError {
                         kind: ParseErrorKind::UnknownDecorator,
                         kind: ParseErrorKind::UnknownDecorator,
@@ -768,6 +844,13 @@ impl<'a> Parser<'a> {
 
 
             let unit_name = identifier.lexeme;
             let unit_name = identifier.lexeme;
 
 
+            if decorator::contains_examples(&self.decorator_stack) {
+                return Err(ParseError {
+                    kind: ParseErrorKind::ExampleUsedOnUnsuitableKind,
+                    span: self.peek(tokens).span,
+                });
+            }
+
             let mut decorators = vec![];
             let mut decorators = vec![];
             std::mem::swap(&mut decorators, &mut self.decorator_stack);
             std::mem::swap(&mut decorators, &mut self.decorator_stack);
 
 
@@ -1578,7 +1661,7 @@ impl<'a> Parser<'a> {
 
 
         let format_specifiers = self
         let format_specifiers = self
             .match_exact(tokens, TokenKind::StringInterpolationSpecifiers)
             .match_exact(tokens, TokenKind::StringInterpolationSpecifiers)
-            .map(|token| token.lexeme.to_owned());
+            .map(|token| token.lexeme);
 
 
         parts.push(StringPart::Interpolation {
         parts.push(StringPart::Interpolation {
             span: expr.full_span(),
             span: expr.full_span(),
@@ -1987,9 +2070,12 @@ mod tests {
     use std::fmt::Write;
     use std::fmt::Write;
 
 
     use super::*;
     use super::*;
-    use crate::ast::{
-        binop, boolean, conditional, factorial, identifier, list, logical_neg, negate, scalar,
-        struct_, ReplaceSpans,
+    use crate::{
+        ast::{
+            binop, boolean, conditional, factorial, identifier, list, logical_neg, negate, scalar,
+            struct_, ReplaceSpans,
+        },
+        span::ByteIndex,
     };
     };
 
 
     #[track_caller]
     #[track_caller]
@@ -2438,7 +2524,26 @@ mod tests {
                 )),
                 )),
                 decorators: vec![
                 decorators: vec![
                     decorator::Decorator::Name("myvar".into()),
                     decorator::Decorator::Name("myvar".into()),
-                    decorator::Decorator::Aliases(vec![("foo", None), ("bar", None)]),
+                    decorator::Decorator::Aliases(vec![
+                        (
+                            "foo",
+                            None,
+                            Span {
+                                start: ByteIndex(24),
+                                end: ByteIndex(27),
+                                code_source_id: 0,
+                            },
+                        ),
+                        (
+                            "bar",
+                            None,
+                            Span {
+                                start: ByteIndex(29),
+                                end: ByteIndex(32),
+                                code_source_id: 0,
+                            },
+                        ),
+                    ]),
                 ],
                 ],
             }),
             }),
         );
         );
@@ -2793,6 +2898,24 @@ mod tests {
             },
             },
         );
         );
 
 
+        parse_as(
+            &["@name(\"Some function\") @example(\"some_function(2)\", \"Use this function:\") @example(\"let some_var = some_function(0)\") fn some_function(x) = 1"],
+            Statement::DefineFunction {
+                function_name_span: Span::dummy(),
+                function_name: "some_function".into(),
+                type_parameters: vec![],
+                parameters: vec![(Span::dummy(), "x".into(), None)],
+                body: Some(scalar!(1.0)),
+                local_variables: vec![],
+                return_type_annotation: None,
+                decorators: vec![
+                    decorator::Decorator::Name("Some function".into()),
+                    decorator::Decorator::Example("some_function(2)".into(), Some("Use this function:".into())),
+                    decorator::Decorator::Example("let some_var = some_function(0)".into(), None),
+                ],
+            },
+        );
+
         parse_as(
         parse_as(
             &["fn double_kef(x) = y where y = x * 2"],
             &["fn double_kef(x) = y where y = x * 2"],
             Statement::DefineFunction {
             Statement::DefineFunction {
@@ -3279,7 +3402,7 @@ mod tests {
                     StringPart::Interpolation {
                     StringPart::Interpolation {
                         span: Span::dummy(),
                         span: Span::dummy(),
                         expr: Box::new(binop!(scalar!(1.0), Add, scalar!(2.0))),
                         expr: Box::new(binop!(scalar!(1.0), Add, scalar!(2.0))),
-                        format_specifiers: Some(":0.2".to_string()),
+                        format_specifiers: Some(":0.2"),
                     },
                     },
                 ],
                 ],
             ),
             ),

+ 41 - 24
numbat/src/prefix_parser.rs

@@ -1,5 +1,5 @@
+use indexmap::IndexMap;
 use std::collections::HashMap;
 use std::collections::HashMap;
-
 use std::sync::OnceLock;
 use std::sync::OnceLock;
 
 
 use crate::span::Span;
 use crate::span::Span;
@@ -52,6 +52,25 @@ impl AcceptsPrefix {
     }
     }
 }
 }
 
 
+/// The spans associated with an alias passed to `@aliases`
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct AliasSpanInfo {
+    /// The span of the name to which the alias refers
+    pub(crate) name_span: Span,
+    /// The span of the alias itself (in an `@aliases` decorator)
+    pub(crate) alias_span: Span,
+}
+
+impl AliasSpanInfo {
+    #[cfg(test)]
+    fn dummy() -> Self {
+        Self {
+            name_span: Span::dummy(),
+            alias_span: Span::dummy(),
+        }
+    }
+}
+
 #[derive(Debug, Clone)]
 #[derive(Debug, Clone)]
 struct UnitInfo {
 struct UnitInfo {
     definition_span: Span,
     definition_span: Span,
@@ -63,10 +82,7 @@ struct UnitInfo {
 
 
 #[derive(Debug, Clone)]
 #[derive(Debug, Clone)]
 pub struct PrefixParser {
 pub struct PrefixParser {
-    units: HashMap<String, UnitInfo>,
-    // This is the exact same information as in the "units" hashmap, only faster to iterate over.
-    // TODO: maybe use an external crate for this (e.g. indexmap?)
-    units_vec: Vec<(String, UnitInfo)>,
+    units: IndexMap<String, UnitInfo>,
 
 
     other_identifiers: HashMap<String, Span>,
     other_identifiers: HashMap<String, Span>,
 
 
@@ -76,8 +92,7 @@ pub struct PrefixParser {
 impl PrefixParser {
 impl PrefixParser {
     pub fn new() -> Self {
     pub fn new() -> Self {
         Self {
         Self {
-            units: HashMap::new(),
-            units_vec: Vec::new(),
+            units: IndexMap::new(),
             other_identifiers: HashMap::new(),
             other_identifiers: HashMap::new(),
             reserved_identifiers: &["_", "ans"],
             reserved_identifiers: &["_", "ans"],
         }
         }
@@ -152,23 +167,23 @@ impl PrefixParser {
     fn ensure_name_is_available(
     fn ensure_name_is_available(
         &self,
         &self,
         name: &str,
         name: &str,
-        conflict_span: Span,
+        definition_span: Span,
         clash_with_other_identifiers: bool,
         clash_with_other_identifiers: bool,
     ) -> Result<()> {
     ) -> Result<()> {
         if self.reserved_identifiers.contains(&name) {
         if self.reserved_identifiers.contains(&name) {
-            return Err(NameResolutionError::ReservedIdentifier(conflict_span));
+            return Err(NameResolutionError::ReservedIdentifier(definition_span));
         }
         }
 
 
         if clash_with_other_identifiers {
         if clash_with_other_identifiers {
             if let Some(original_span) = self.other_identifiers.get(name) {
             if let Some(original_span) = self.other_identifiers.get(name) {
-                return Err(self.identifier_clash_error(name, conflict_span, *original_span));
+                return Err(self.identifier_clash_error(name, definition_span, *original_span));
             }
             }
         }
         }
 
 
         match self.parse(name) {
         match self.parse(name) {
             PrefixParserResult::Identifier(_) => Ok(()),
             PrefixParserResult::Identifier(_) => Ok(()),
             PrefixParserResult::UnitIdentifier(original_span, _, _, _) => {
             PrefixParserResult::UnitIdentifier(original_span, _, _, _) => {
-                Err(self.identifier_clash_error(name, conflict_span, original_span))
+                Err(self.identifier_clash_error(name, definition_span, original_span))
             }
             }
         }
         }
     }
     }
@@ -180,9 +195,12 @@ impl PrefixParser {
         metric: bool,
         metric: bool,
         binary: bool,
         binary: bool,
         full_name: &str,
         full_name: &str,
-        definition_span: Span,
+        AliasSpanInfo {
+            name_span,
+            alias_span,
+        }: AliasSpanInfo,
     ) -> Result<()> {
     ) -> Result<()> {
-        self.ensure_name_is_available(unit_name, definition_span, true)?;
+        self.ensure_name_is_available(unit_name, alias_span, true)?;
 
 
         for (prefix_long, prefixes_short, prefix) in Self::prefixes() {
         for (prefix_long, prefixes_short, prefix) in Self::prefixes() {
             if !(prefix.is_metric() && metric || prefix.is_binary() && binary) {
             if !(prefix.is_metric() && metric || prefix.is_binary() && binary) {
@@ -192,7 +210,7 @@ impl PrefixParser {
             if accepts_prefix.long {
             if accepts_prefix.long {
                 self.ensure_name_is_available(
                 self.ensure_name_is_available(
                     &format!("{prefix_long}{unit_name}"),
                     &format!("{prefix_long}{unit_name}"),
-                    definition_span,
+                    alias_span,
                     true,
                     true,
                 )?;
                 )?;
             }
             }
@@ -200,7 +218,7 @@ impl PrefixParser {
                 for prefix_short in *prefixes_short {
                 for prefix_short in *prefixes_short {
                     self.ensure_name_is_available(
                     self.ensure_name_is_available(
                         &format!("{prefix_short}{unit_name}"),
                         &format!("{prefix_short}{unit_name}"),
-                        definition_span,
+                        alias_span,
                         true,
                         true,
                     )?;
                     )?;
                 }
                 }
@@ -208,14 +226,13 @@ impl PrefixParser {
         }
         }
 
 
         let unit_info = UnitInfo {
         let unit_info = UnitInfo {
-            definition_span,
+            definition_span: name_span,
             accepts_prefix,
             accepts_prefix,
             metric_prefixes: metric,
             metric_prefixes: metric,
             binary_prefixes: binary,
             binary_prefixes: binary,
             full_name: full_name.into(),
             full_name: full_name.into(),
         };
         };
         self.units.insert(unit_name.into(), unit_info.clone());
         self.units.insert(unit_name.into(), unit_info.clone());
-        self.units_vec.push((unit_name.into(), unit_info));
 
 
         Ok(())
         Ok(())
     }
     }
@@ -233,12 +250,12 @@ impl PrefixParser {
             return PrefixParserResult::UnitIdentifier(
             return PrefixParserResult::UnitIdentifier(
                 info.definition_span,
                 info.definition_span,
                 Prefix::none(),
                 Prefix::none(),
-                input.into(),
+                input.to_string(),
                 info.full_name.clone(),
                 info.full_name.clone(),
             );
             );
         }
         }
 
 
-        for (unit_name, info) in &self.units_vec {
+        for (unit_name, info) in &self.units {
             if !input.ends_with(unit_name.as_str()) {
             if !input.ends_with(unit_name.as_str()) {
                 continue;
                 continue;
             }
             }
@@ -294,7 +311,7 @@ mod tests {
                 true,
                 true,
                 false,
                 false,
                 "meter",
                 "meter",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             )
             .unwrap();
             .unwrap();
         prefix_parser
         prefix_parser
@@ -304,7 +321,7 @@ mod tests {
                 true,
                 true,
                 false,
                 false,
                 "meter",
                 "meter",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             )
             .unwrap();
             .unwrap();
 
 
@@ -315,7 +332,7 @@ mod tests {
                 true,
                 true,
                 true,
                 true,
                 "byte",
                 "byte",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             )
             .unwrap();
             .unwrap();
         prefix_parser
         prefix_parser
@@ -325,7 +342,7 @@ mod tests {
                 true,
                 true,
                 true,
                 true,
                 "byte",
                 "byte",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             )
             .unwrap();
             .unwrap();
 
 
@@ -336,7 +353,7 @@ mod tests {
                 false,
                 false,
                 false,
                 false,
                 "me",
                 "me",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             )
             .unwrap();
             .unwrap();
 
 

+ 12 - 6
numbat/src/prefix_transformer.rs

@@ -2,7 +2,7 @@ use crate::{
     ast::{DefineVariable, Expression, Statement, StringPart},
     ast::{DefineVariable, Expression, Statement, StringPart},
     decorator::{self, Decorator},
     decorator::{self, Decorator},
     name_resolution::NameResolutionError,
     name_resolution::NameResolutionError,
-    prefix_parser::{PrefixParser, PrefixParserResult},
+    prefix_parser::{AliasSpanInfo, PrefixParser, PrefixParserResult},
     span::Span,
     span::Span,
 };
 };
 
 
@@ -135,20 +135,26 @@ impl Transformer {
     pub(crate) fn register_name_and_aliases(
     pub(crate) fn register_name_and_aliases(
         &mut self,
         &mut self,
         name: &str,
         name: &str,
+        name_span: Span,
         decorators: &[Decorator],
         decorators: &[Decorator],
-        conflict_span: Span,
     ) -> Result<()> {
     ) -> Result<()> {
         let mut unit_names = vec![];
         let mut unit_names = vec![];
         let metric_prefixes = Self::has_decorator(decorators, Decorator::MetricPrefixes);
         let metric_prefixes = Self::has_decorator(decorators, Decorator::MetricPrefixes);
         let binary_prefixes = Self::has_decorator(decorators, Decorator::BinaryPrefixes);
         let binary_prefixes = Self::has_decorator(decorators, Decorator::BinaryPrefixes);
-        for (alias, accepts_prefix) in decorator::name_and_aliases(name, decorators) {
+
+        for (alias, accepts_prefix, alias_span) in
+            decorator::name_and_aliases_spans(name, name_span, decorators)
+        {
             self.prefix_parser.add_unit(
             self.prefix_parser.add_unit(
                 alias,
                 alias,
                 accepts_prefix,
                 accepts_prefix,
                 metric_prefixes,
                 metric_prefixes,
                 binary_prefixes,
                 binary_prefixes,
                 name,
                 name,
-                conflict_span,
+                AliasSpanInfo {
+                    name_span,
+                    alias_span,
+                },
             )?;
             )?;
             unit_names.push(alias.to_string());
             unit_names.push(alias.to_string());
         }
         }
@@ -189,7 +195,7 @@ impl Transformer {
         Ok(match statement {
         Ok(match statement {
             Statement::Expression(expr) => Statement::Expression(self.transform_expression(expr)),
             Statement::Expression(expr) => Statement::Expression(self.transform_expression(expr)),
             Statement::DefineBaseUnit(span, name, dexpr, decorators) => {
             Statement::DefineBaseUnit(span, name, dexpr, decorators) => {
-                self.register_name_and_aliases(name, &decorators, span)?;
+                self.register_name_and_aliases(name, span, &decorators)?;
                 Statement::DefineBaseUnit(span, name, dexpr, decorators)
                 Statement::DefineBaseUnit(span, name, dexpr, decorators)
             }
             }
             Statement::DefineDerivedUnit {
             Statement::DefineDerivedUnit {
@@ -200,7 +206,7 @@ impl Transformer {
                 type_annotation,
                 type_annotation,
                 decorators,
                 decorators,
             } => {
             } => {
-                self.register_name_and_aliases(identifier, &decorators, identifier_span)?;
+                self.register_name_and_aliases(identifier, identifier_span, &decorators)?;
                 Statement::DefineDerivedUnit {
                 Statement::DefineDerivedUnit {
                     identifier_span,
                     identifier_span,
                     identifier,
                     identifier,

+ 5 - 33
numbat/src/product.rs

@@ -54,7 +54,7 @@ impl<Factor: Power + Clone + Canonicalize + Ord + Display, const CANONICALIZE: b
                     + m::Markup::from(m::FormattedString(
                     + m::Markup::from(m::FormattedString(
                         m::OutputType::Normal,
                         m::OutputType::Normal,
                         format_type,
                         format_type,
-                        factor.to_string(),
+                        factor.to_string().into(),
                     ))
                     ))
                     + if i == num_factors - 1 {
                     + if i == num_factors - 1 {
                         m::empty()
                         m::empty()
@@ -145,10 +145,8 @@ impl<Factor: Clone + Ord + Canonicalize, const CANONICALIZE: bool> Product<Facto
         product
         product
     }
     }
 
 
-    pub fn iter(&self) -> ProductIter<Factor> {
-        ProductIter {
-            inner: self.factors.iter(),
-        }
+    pub fn iter(&self) -> std::slice::Iter<'_, Factor> {
+        self.factors.iter()
     }
     }
 
 
     #[cfg(test)]
     #[cfg(test)]
@@ -245,13 +243,11 @@ impl<Factor: Clone + Ord + Canonicalize + Eq, const CANONICALIZE: bool> Eq
 }
 }
 
 
 impl<Factor, const CANONICALIZE: bool> IntoIterator for Product<Factor, CANONICALIZE> {
 impl<Factor, const CANONICALIZE: bool> IntoIterator for Product<Factor, CANONICALIZE> {
-    type IntoIter = ProductIntoIter<Factor>;
+    type IntoIter = <Vec<Factor> as IntoIterator>::IntoIter;
     type Item = Factor;
     type Item = Factor;
 
 
     fn into_iter(self) -> Self::IntoIter {
     fn into_iter(self) -> Self::IntoIter {
-        ProductIntoIter {
-            inner: self.factors.into_iter(),
-        }
+        self.factors.into_iter()
     }
     }
 }
 }
 
 
@@ -277,30 +273,6 @@ impl<Factor: Clone + Ord + Canonicalize, const CANONICALIZE: bool> std::iter::Pr
     }
     }
 }
 }
 
 
-pub struct ProductIter<'a, Factor> {
-    inner: std::slice::Iter<'a, Factor>,
-}
-
-impl<'a, Factor> Iterator for ProductIter<'a, Factor> {
-    type Item = &'a Factor;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.inner.next()
-    }
-}
-
-pub struct ProductIntoIter<Factor> {
-    inner: std::vec::IntoIter<Factor>,
-}
-
-impl<Factor> Iterator for ProductIntoIter<Factor> {
-    type Item = Factor;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.inner.next()
-    }
-}
-
 #[cfg(test)]
 #[cfg(test)]
 mod tests {
 mod tests {
     use super::*;
     use super::*;

+ 2 - 1
numbat/src/quantity.rs

@@ -192,7 +192,8 @@ impl Quantity {
             let group_representative = group_as_unit
             let group_representative = group_as_unit
                 .iter()
                 .iter()
                 .max_by(|&f1, &f2| {
                 .max_by(|&f1, &f2| {
-                    // TODO: describe this heuristic
+                    // prefer base units over non-base. if multiple base units, prefer
+                    // those with a larger exponent
                     (f1.unit_id.is_base().cmp(&f2.unit_id.is_base()))
                     (f1.unit_id.is_base().cmp(&f2.unit_id.is_base()))
                         .then(f1.exponent.cmp(&f2.exponent))
                         .then(f1.exponent.cmp(&f2.exponent))
                 })
                 })

+ 0 - 3
numbat/src/registry.rs

@@ -24,9 +24,6 @@ pub type Result<T> = std::result::Result<T, RegistryError>;
 
 
 pub type BaseEntry = String;
 pub type BaseEntry = String;
 
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct BaseIndex(isize);
-
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
 pub struct BaseRepresentationFactor(pub BaseEntry, pub Exponent);
 pub struct BaseRepresentationFactor(pub BaseEntry, pub Exponent);
 
 

+ 1 - 1
numbat/src/resolver.rs

@@ -49,7 +49,7 @@ pub struct Resolver {
     pub files: SimpleFiles<String, String>,
     pub files: SimpleFiles<String, String>,
     text_code_source_count: usize,
     text_code_source_count: usize,
     internal_code_source_count: usize,
     internal_code_source_count: usize,
-    imported_modules: Vec<ModulePath>,
+    pub imported_modules: Vec<ModulePath>,
     codesources: HashMap<usize, CodeSource>,
     codesources: HashMap<usize, CodeSource>,
 }
 }
 
 

+ 22 - 14
numbat/src/tokenizer.rs

@@ -22,10 +22,7 @@ pub enum TokenizerErrorKind {
     ExpectedDigit { character: Option<char> },
     ExpectedDigit { character: Option<char> },
 
 
     #[error("Expected base-{base} digit")]
     #[error("Expected base-{base} digit")]
-    ExpectedDigitInBase {
-        base: usize,
-        character: Option<char>,
-    },
+    ExpectedDigitInBase { base: u8, character: Option<char> },
 
 
     #[error("Unterminated string")]
     #[error("Unterminated string")]
     UnterminatedString,
     UnterminatedString,
@@ -125,7 +122,7 @@ pub enum TokenKind {
 
 
     // Variable-length tokens
     // Variable-length tokens
     Number,
     Number,
-    IntegerWithBase(usize),
+    IntegerWithBase(u8),
     Identifier,
     Identifier,
 
 
     // A normal string without interpolation: `"hello world"`
     // A normal string without interpolation: `"hello world"`
@@ -378,6 +375,18 @@ impl Tokenizer {
     }
     }
 
 
     fn scan_single_token<'a>(&mut self, input: &'a str) -> Result<Option<Token<'a>>> {
     fn scan_single_token<'a>(&mut self, input: &'a str) -> Result<Option<Token<'a>>> {
+        fn is_ascii_hex_digit(c: char) -> bool {
+            c.is_ascii_hexdigit()
+        }
+
+        fn is_ascii_octal_digit(c: char) -> bool {
+            ('0'..='7').contains(&c)
+        }
+
+        fn is_ascii_binary_digit(c: char) -> bool {
+            c == '0' || c == '1'
+        }
+
         static KEYWORDS: OnceLock<HashMap<&'static str, TokenKind>> = OnceLock::new();
         static KEYWORDS: OnceLock<HashMap<&'static str, TokenKind>> = OnceLock::new();
         let keywords = KEYWORDS.get_or_init(|| {
         let keywords = KEYWORDS.get_or_init(|| {
             let mut m = HashMap::new();
             let mut m = HashMap::new();
@@ -463,18 +472,17 @@ impl Tokenizer {
                 .map(|c| c == 'x' || c == 'o' || c == 'b')
                 .map(|c| c == 'x' || c == 'o' || c == 'b')
                 .unwrap_or(false) =>
                 .unwrap_or(false) =>
             {
             {
-                let (base, is_digit_in_base): (_, Box<dyn Fn(char) -> bool>) =
-                    match self.peek(input).unwrap() {
-                        'x' => (16, Box::new(|c| c.is_ascii_hexdigit())),
-                        'o' => (8, Box::new(|c| ('0'..='7').contains(&c))),
-                        'b' => (2, Box::new(|c| c == '0' || c == '1')),
-                        _ => unreachable!(),
-                    };
+                let (base, is_digit_in_base) = match self.peek(input).unwrap() {
+                    'x' => (16, is_ascii_hex_digit as fn(char) -> bool),
+                    'o' => (8, is_ascii_octal_digit as _),
+                    'b' => (2, is_ascii_binary_digit as _),
+                    _ => unreachable!(),
+                };
 
 
                 self.advance(input); // skip over the x/o/b
                 self.advance(input); // skip over the x/o/b
 
 
-                // If the first character is not a digits, that's an error.
-                if !self.peek(input).map(&is_digit_in_base).unwrap_or(false) {
+                // If the first character is not a digit, that's an error.
+                if !self.peek(input).map(is_digit_in_base).unwrap_or(false) {
                     return tokenizer_error(
                     return tokenizer_error(
                         self.current,
                         self.current,
                         TokenizerErrorKind::ExpectedDigitInBase {
                         TokenizerErrorKind::ExpectedDigitInBase {

+ 2 - 2
numbat/src/traversal.rs

@@ -11,7 +11,7 @@ impl ForAllTypeSchemes for StructInfo {
     }
     }
 }
 }
 
 
-impl ForAllTypeSchemes for Expression {
+impl ForAllTypeSchemes for Expression<'_> {
     fn for_all_type_schemes(&mut self, f: &mut dyn FnMut(&mut TypeScheme)) {
     fn for_all_type_schemes(&mut self, f: &mut dyn FnMut(&mut TypeScheme)) {
         match self {
         match self {
             Expression::Scalar(_, _, type_) => f(type_),
             Expression::Scalar(_, _, type_) => f(type_),
@@ -143,7 +143,7 @@ impl ForAllExpressions for Statement<'_> {
     }
     }
 }
 }
 
 
-impl ForAllExpressions for Expression {
+impl ForAllExpressions for Expression<'_> {
     fn for_all_expressions(&self, f: &mut dyn FnMut(&Expression)) {
     fn for_all_expressions(&self, f: &mut dyn FnMut(&Expression)) {
         f(self);
         f(self);
         match self {
         match self {

+ 2 - 4
numbat/src/typechecker/constraints.rs

@@ -160,6 +160,7 @@ pub enum TrivialResolution {
 }
 }
 
 
 impl TrivialResolution {
 impl TrivialResolution {
+    #[allow(clippy::wrong_self_convention)]
     pub fn is_trivially_violated(self) -> bool {
     pub fn is_trivially_violated(self) -> bool {
         matches!(self, TrivialResolution::Violated)
         matches!(self, TrivialResolution::Violated)
     }
     }
@@ -296,10 +297,7 @@ impl Constraint {
             Constraint::EqualScalar(dtype) => match dtype.split_first_factor() {
             Constraint::EqualScalar(dtype) => match dtype.split_first_factor() {
                 Some(((DTypeFactor::TVar(tv), k), rest)) => {
                 Some(((DTypeFactor::TVar(tv), k), rest)) => {
                     let result = DType::from_factors(
                     let result = DType::from_factors(
-                        &rest
-                            .iter()
-                            .map(|(f, j)| (f.clone(), -j / k))
-                            .collect::<Vec<_>>(),
+                        rest.iter().map(|(f, j)| (f.clone(), -j / k)).collect(),
                     );
                     );
                     Some(Satisfied::with_substitution(Substitution::single(
                     Some(Satisfied::with_substitution(Substitution::single(
                         tv.clone(),
                         tv.clone(),

+ 1 - 0
numbat/src/typechecker/environment.rs

@@ -67,6 +67,7 @@ pub struct FunctionMetadata {
     pub name: Option<String>,
     pub name: Option<String>,
     pub url: Option<String>,
     pub url: Option<String>,
     pub description: Option<String>,
     pub description: Option<String>,
+    pub examples: Vec<(String, Option<String>)>,
 }
 }
 
 
 #[derive(Clone, Debug)]
 #[derive(Clone, Debug)]

+ 53 - 45
numbat/src/typechecker/mod.rs

@@ -65,16 +65,16 @@ pub struct TypeChecker {
     constraints: ConstraintSet,
     constraints: ConstraintSet,
 }
 }
 
 
-struct ElaborationDefinitionArgs<'a> {
+struct ElaborationDefinitionArgs<'a, 'b> {
     identifier_span: Span,
     identifier_span: Span,
-    expr: &'a ast::Expression<'a>,
+    expr: &'b ast::Expression<'a>,
     type_annotation_span: Option<Span>,
     type_annotation_span: Option<Span>,
-    type_annotation: Option<&'a TypeAnnotation>,
-    operation: &'a str,
+    type_annotation: Option<&'b TypeAnnotation>,
+    operation: &'b str,
     expected_name: &'static str,
     expected_name: &'static str,
     actual_name: &'static str,
     actual_name: &'static str,
     actual_name_for_fix: &'static str,
     actual_name_for_fix: &'static str,
-    elaboration_kind: &'a str,
+    elaboration_kind: &'b str,
 }
 }
 
 
 impl TypeChecker {
 impl TypeChecker {
@@ -118,14 +118,15 @@ impl TypeChecker {
                     }
                     }
                 }
                 }
 
 
-                let mut dtype: DType = self
+                let mut factors = self
                     .registry
                     .registry
                     .get_base_representation(dexpr)
                     .get_base_representation(dexpr)
-                    .map(|br| br.into())
-                    .map_err(TypeCheckError::RegistryError)?;
+                    .map(DType::from)
+                    .map_err(TypeCheckError::RegistryError)?
+                    .into_factors();
 
 
                 // Replace BaseDimension("D") with TVar("D") for all type parameters
                 // Replace BaseDimension("D") with TVar("D") for all type parameters
-                for (factor, _) in dtype.factors.iter_mut() {
+                for (factor, _) in &mut factors {
                     *factor = match factor {
                     *factor = match factor {
                         DTypeFactor::BaseDimension(ref n)
                         DTypeFactor::BaseDimension(ref n)
                             if self
                             if self
@@ -140,7 +141,7 @@ impl TypeChecker {
                     }
                     }
                 }
                 }
 
 
-                Ok(Type::Dimension(dtype))
+                Ok(Type::Dimension(DType::from_factors(factors)))
             }
             }
             TypeAnnotation::Bool(_) => Ok(Type::Boolean),
             TypeAnnotation::Bool(_) => Ok(Type::Boolean),
             TypeAnnotation::String(_) => Ok(Type::String),
             TypeAnnotation::String(_) => Ok(Type::String),
@@ -172,28 +173,28 @@ impl TypeChecker {
         })?)
         })?)
     }
     }
 
 
-    fn get_proper_function_reference(
+    fn get_proper_function_reference<'a>(
         &self,
         &self,
-        expr: &ast::Expression,
-    ) -> Option<(String, &FunctionSignature)> {
+        expr: &ast::Expression<'a>,
+    ) -> Option<(&'a str, &FunctionSignature)> {
         match expr {
         match expr {
             ast::Expression::Identifier(_, name) => self
             ast::Expression::Identifier(_, name) => self
                 .env
                 .env
                 .get_function_info(name)
                 .get_function_info(name)
-                .map(|(signature, _)| (name.to_string(), signature)),
+                .map(|(signature, _)| (*name, signature)),
             _ => None,
             _ => None,
         }
         }
     }
     }
 
 
-    fn proper_function_call(
+    fn proper_function_call<'a>(
         &mut self,
         &mut self,
         span: &Span,
         span: &Span,
         full_span: &Span,
         full_span: &Span,
-        function_name: &str,
+        function_name: &'a str,
         signature: &FunctionSignature,
         signature: &FunctionSignature,
-        arguments: Vec<typed_ast::Expression>,
+        arguments: Vec<typed_ast::Expression<'a>>,
         argument_types: Vec<Type>,
         argument_types: Vec<Type>,
-    ) -> Result<typed_ast::Expression> {
+    ) -> Result<typed_ast::Expression<'a>> {
         let FunctionSignature {
         let FunctionSignature {
             name: _,
             name: _,
             definition_span,
             definition_span,
@@ -288,13 +289,16 @@ impl TypeChecker {
         Ok(typed_ast::Expression::FunctionCall(
         Ok(typed_ast::Expression::FunctionCall(
             *span,
             *span,
             *full_span,
             *full_span,
-            function_name.into(),
+            function_name,
             arguments,
             arguments,
             TypeScheme::concrete(return_type.as_ref().clone()),
             TypeScheme::concrete(return_type.as_ref().clone()),
         ))
         ))
     }
     }
 
 
-    fn elaborate_expression(&mut self, ast: &ast::Expression) -> Result<typed_ast::Expression> {
+    fn elaborate_expression<'a>(
+        &mut self,
+        ast: &ast::Expression<'a>,
+    ) -> Result<typed_ast::Expression<'a>> {
         Ok(match ast {
         Ok(match ast {
             ast::Expression::Scalar(span, n)
             ast::Expression::Scalar(span, n)
                 if n.to_f64().is_zero() || n.to_f64().is_infinite() || n.to_f64().is_nan() =>
                 if n.to_f64().is_zero() || n.to_f64().is_infinite() || n.to_f64().is_nan() =>
@@ -325,7 +329,7 @@ impl TypeChecker {
                     }
                     }
                 };
                 };
 
 
-                typed_ast::Expression::Identifier(*span, name.to_string(), TypeScheme::concrete(ty))
+                typed_ast::Expression::Identifier(*span, name, TypeScheme::concrete(ty))
             }
             }
             ast::Expression::UnitIdentifier(span, prefix, name, full_name) => {
             ast::Expression::UnitIdentifier(span, prefix, name, full_name) => {
                 let type_scheme = self.identifier_type(*span, name)?.clone();
                 let type_scheme = self.identifier_type(*span, name)?.clone();
@@ -780,7 +784,7 @@ impl TypeChecker {
                     self.proper_function_call(
                     self.proper_function_call(
                         span,
                         span,
                         full_span,
                         full_span,
-                        &name,
+                        name,
                         &signature,
                         &signature,
                         arguments_checked,
                         arguments_checked,
                         argument_types,
                         argument_types,
@@ -880,7 +884,7 @@ impl TypeChecker {
                             format_specifiers,
                             format_specifiers,
                         } => Ok(typed_ast::StringPart::Interpolation {
                         } => Ok(typed_ast::StringPart::Interpolation {
                             span: *span,
                             span: *span,
-                            format_specifiers: format_specifiers.clone(),
+                            format_specifiers: format_specifiers.as_ref().copied(),
                             expr: Box::new(self.elaborate_expression(expr)?),
                             expr: Box::new(self.elaborate_expression(expr)?),
                         }),
                         }),
                     })
                     })
@@ -933,7 +937,7 @@ impl TypeChecker {
                 let name = *name;
                 let name = *name;
                 let fields_checked = fields
                 let fields_checked = fields
                     .iter()
                     .iter()
-                    .map(|(_, n, v)| Ok((n.to_string(), self.elaborate_expression(v)?)))
+                    .map(|(_, n, v)| Ok((*n, self.elaborate_expression(v)?)))
                     .collect::<Result<Vec<_>>>()?;
                     .collect::<Result<Vec<_>>>()?;
 
 
                 let Some(struct_info) = self.structs.get(name).cloned() else {
                 let Some(struct_info) = self.structs.get(name).cloned() else {
@@ -958,12 +962,12 @@ impl TypeChecker {
                         ));
                         ));
                     }
                     }
 
 
-                    let Some((expected_field_span, expected_type)) = struct_info.fields.get(field)
+                    let Some((expected_field_span, expected_type)) = struct_info.fields.get(*field)
                     else {
                     else {
                         return Err(Box::new(TypeCheckError::UnknownFieldInStructInstantiation(
                         return Err(Box::new(TypeCheckError::UnknownFieldInStructInstantiation(
                             *span,
                             *span,
                             struct_info.definition_span,
                             struct_info.definition_span,
-                            field.clone(),
+                            field.to_string(),
                             struct_info.name.clone(),
                             struct_info.name.clone(),
                         )));
                         )));
                     };
                     };
@@ -986,7 +990,7 @@ impl TypeChecker {
 
 
                 let missing_fields = {
                 let missing_fields = {
                     let mut fields = struct_info.fields.clone();
                     let mut fields = struct_info.fields.clone();
-                    fields.retain(|f, _| !seen_fields.contains_key(f));
+                    fields.retain(|f, _| !seen_fields.contains_key(&f.as_str()));
                     fields.into_iter().map(|(n, (_, t))| (n, t)).collect_vec()
                     fields.into_iter().map(|(n, (_, t))| (n, t)).collect_vec()
                 };
                 };
 
 
@@ -1050,7 +1054,7 @@ impl TypeChecker {
                     *ident_span,
                     *ident_span,
                     *full_span,
                     *full_span,
                     Box::new(expr_checked),
                     Box::new(expr_checked),
-                    field_name.to_owned(),
+                    field_name,
                     TypeScheme::concrete(type_),
                     TypeScheme::concrete(type_),
                     TypeScheme::concrete(field_type),
                     TypeScheme::concrete(field_type),
                 )
                 )
@@ -1105,10 +1109,10 @@ impl TypeChecker {
         })
         })
     }
     }
 
 
-    fn _elaborate_inner(
+    fn _elaborate_inner<'a>(
         &mut self,
         &mut self,
-        definition: ElaborationDefinitionArgs,
-    ) -> Result<(typed_ast::Expression, typed_ast::Type)> {
+        definition: ElaborationDefinitionArgs<'a, '_>,
+    ) -> Result<(typed_ast::Expression<'a>, typed_ast::Type)> {
         let ElaborationDefinitionArgs {
         let ElaborationDefinitionArgs {
             identifier_span,
             identifier_span,
             expr,
             expr,
@@ -1219,8 +1223,8 @@ impl TypeChecker {
         }
         }
 
 
         Ok(typed_ast::DefineVariable(
         Ok(typed_ast::DefineVariable(
-            identifier.to_string(),
-            decorators.to_owned(),
+            identifier,
+            decorators.clone(),
             expr_checked,
             expr_checked,
             type_annotation.clone(),
             type_annotation.clone(),
             TypeScheme::concrete(type_deduced),
             TypeScheme::concrete(type_deduced),
@@ -1284,7 +1288,7 @@ impl TypeChecker {
                 }
                 }
 
 
                 typed_ast::Statement::DefineBaseUnit(
                 typed_ast::Statement::DefineBaseUnit(
-                    unit_name.to_string(),
+                    unit_name,
                     decorators.clone(),
                     decorators.clone(),
                     type_annotation.clone().map(TypeAnnotation::TypeExpression),
                     type_annotation.clone().map(TypeAnnotation::TypeExpression),
                     TypeScheme::concrete(Type::Dimension(type_specified)),
                     TypeScheme::concrete(Type::Dimension(type_specified)),
@@ -1320,7 +1324,7 @@ impl TypeChecker {
                     );
                     );
                 }
                 }
                 typed_ast::Statement::DefineDerivedUnit(
                 typed_ast::Statement::DefineDerivedUnit(
-                    identifier.to_string(),
+                    identifier,
                     expr_checked,
                     expr_checked,
                     decorators.clone(),
                     decorators.clone(),
                     type_annotation.clone(),
                     type_annotation.clone(),
@@ -1424,7 +1428,7 @@ impl TypeChecker {
                     );
                     );
                     typed_parameters.push((
                     typed_parameters.push((
                         *parameter_span,
                         *parameter_span,
-                        parameter.to_string(),
+                        *parameter,
                         parameter_type,
                         parameter_type,
                         type_annotation,
                         type_annotation,
                     ));
                     ));
@@ -1444,7 +1448,7 @@ impl TypeChecker {
 
 
                 let parameters: Vec<_> = typed_parameters
                 let parameters: Vec<_> = typed_parameters
                     .iter()
                     .iter()
-                    .map(|(span, name, _, annotation)| (*span, name.clone(), (*annotation).clone()))
+                    .map(|(span, name, _, annotation)| (*span, name, (*annotation).clone()))
                     .collect();
                     .collect();
                 let parameter_types = typed_parameters
                 let parameter_types = typed_parameters
                     .iter()
                     .iter()
@@ -1463,7 +1467,10 @@ impl TypeChecker {
                             .iter()
                             .iter()
                             .map(|(span, name, tpb)| (*span, name.to_string(), tpb.clone()).clone())
                             .map(|(span, name, tpb)| (*span, name.to_string(), tpb.clone()).clone())
                             .collect(),
                             .collect(),
-                        parameters,
+                        parameters: parameters
+                            .into_iter()
+                            .map(|(span, s, o)| (span, s.to_string(), o))
+                            .collect(),
                         return_type_annotation: return_type_annotation.clone(),
                         return_type_annotation: return_type_annotation.clone(),
                         fn_type: fn_type.clone(),
                         fn_type: fn_type.clone(),
                     },
                     },
@@ -1471,6 +1478,7 @@ impl TypeChecker {
                         name: crate::decorator::name(decorators).map(ToOwned::to_owned),
                         name: crate::decorator::name(decorators).map(ToOwned::to_owned),
                         url: crate::decorator::url(decorators).map(ToOwned::to_owned),
                         url: crate::decorator::url(decorators).map(ToOwned::to_owned),
                         description: crate::decorator::description(decorators),
                         description: crate::decorator::description(decorators),
+                        examples: crate::decorator::examples(decorators),
                     },
                     },
                 );
                 );
 
 
@@ -1579,18 +1587,18 @@ impl TypeChecker {
                 );
                 );
 
 
                 typed_ast::Statement::DefineFunction(
                 typed_ast::Statement::DefineFunction(
-                    function_name.to_string(),
+                    function_name,
                     decorators.clone(),
                     decorators.clone(),
                     type_parameters
                     type_parameters
                         .iter()
                         .iter()
-                        .map(|(_, name, bound)| (name.to_string(), bound.clone()))
+                        .map(|(_, name, bound)| (*name, bound.clone()))
                         .collect(),
                         .collect(),
                     typed_parameters
                     typed_parameters
                         .iter()
                         .iter()
                         .map(|(span, name, _, type_annotation)| {
                         .map(|(span, name, _, type_annotation)| {
                             (
                             (
                                 *span,
                                 *span,
-                                name.clone(),
+                                *name,
                                 (*type_annotation).clone(),
                                 (*type_annotation).clone(),
                                 crate::markup::empty(),
                                 crate::markup::empty(),
                             )
                             )
@@ -1640,7 +1648,7 @@ impl TypeChecker {
                         .add_base_dimension(name)
                         .add_base_dimension(name)
                         .map_err(TypeCheckError::RegistryError)?;
                         .map_err(TypeCheckError::RegistryError)?;
                 }
                 }
-                typed_ast::Statement::DefineDimension(name.to_string(), dexprs.clone())
+                typed_ast::Statement::DefineDimension(name, dexprs.clone())
             }
             }
             ast::Statement::ProcedureCall(span, kind @ ProcedureKind::Type, args) => {
             ast::Statement::ProcedureCall(span, kind @ ProcedureKind::Type, args) => {
                 if args.len() != 1 {
                 if args.len() != 1 {
@@ -1894,12 +1902,12 @@ impl TypeChecker {
 
 
     pub fn check<'a>(
     pub fn check<'a>(
         &mut self,
         &mut self,
-        statements: impl IntoIterator<Item = ast::Statement<'a>>,
+        statements: &[ast::Statement<'a>],
     ) -> Result<Vec<typed_ast::Statement<'a>>> {
     ) -> Result<Vec<typed_ast::Statement<'a>>> {
         let mut checked_statements = vec![];
         let mut checked_statements = vec![];
 
 
-        for statement in statements.into_iter() {
-            checked_statements.push(self.check_statement(&statement)?);
+        for statement in statements {
+            checked_statements.push(self.check_statement(statement)?);
         }
         }
 
 
         Ok(checked_statements)
         Ok(checked_statements)

+ 2 - 2
numbat/src/typechecker/substitutions.rs

@@ -92,7 +92,7 @@ impl ApplySubstitution for Type {
 impl ApplySubstitution for DType {
 impl ApplySubstitution for DType {
     fn apply(&mut self, substitution: &Substitution) -> Result<(), SubstitutionError> {
     fn apply(&mut self, substitution: &Substitution) -> Result<(), SubstitutionError> {
         let mut new_dtype = self.clone();
         let mut new_dtype = self.clone();
-        for (f, power) in &self.factors {
+        for (f, power) in self.factors() {
             match f {
             match f {
                 DTypeFactor::TVar(tv) => {
                 DTypeFactor::TVar(tv) => {
                     if let Some(type_) = substitution.lookup(tv) {
                     if let Some(type_) = substitution.lookup(tv) {
@@ -148,7 +148,7 @@ impl ApplySubstitution for StructInfo {
     }
     }
 }
 }
 
 
-impl ApplySubstitution for Expression {
+impl ApplySubstitution for Expression<'_> {
     fn apply(&mut self, s: &Substitution) -> Result<(), SubstitutionError> {
     fn apply(&mut self, s: &Substitution) -> Result<(), SubstitutionError> {
         match self {
         match self {
             Expression::Scalar(_, _, type_) => type_.apply(s),
             Expression::Scalar(_, _, type_) => type_.apply(s),

+ 1 - 1
numbat/src/typechecker/tests/mod.rs

@@ -60,7 +60,7 @@ fn run_typecheck(input: &str) -> Result<typed_ast::Statement<'_>> {
         .map_err(|err| Box::new(err.into()))?;
         .map_err(|err| Box::new(err.into()))?;
 
 
     TypeChecker::default()
     TypeChecker::default()
-        .check(transformed_statements)
+        .check(&transformed_statements)
         .map(|mut statements_checked| statements_checked.pop().unwrap())
         .map(|mut statements_checked| statements_checked.pop().unwrap())
 }
 }
 
 

+ 2 - 2
numbat/src/typechecker/type_scheme.rs

@@ -123,7 +123,7 @@ impl TypeScheme {
 
 
             for type_parameter in &type_parameters {
             for type_parameter in &type_parameters {
                 markup += m::space();
                 markup += m::space();
-                markup += m::type_identifier(type_parameter.unsafe_name());
+                markup += m::type_identifier(type_parameter.unsafe_name().to_string());
 
 
                 if instantiated_type.bounds.is_dtype_bound(type_parameter) {
                 if instantiated_type.bounds.is_dtype_bound(type_parameter) {
                     markup += m::operator(":");
                     markup += m::operator(":");
@@ -219,7 +219,7 @@ impl PrettyPrint for TypeScheme {
                 for type_parameter in &type_parameters {
                 for type_parameter in &type_parameters {
                     markup += m::keyword("forall");
                     markup += m::keyword("forall");
                     markup += m::space();
                     markup += m::space();
-                    markup += m::type_identifier(type_parameter.unsafe_name());
+                    markup += m::type_identifier(type_parameter.unsafe_name().to_string());
 
 
                     if instantiated_type.bounds.is_dtype_bound(type_parameter) {
                     if instantiated_type.bounds.is_dtype_bound(type_parameter) {
                         markup += m::operator(":");
                         markup += m::operator(":");

+ 116 - 89
numbat/src/typed_ast.rs

@@ -41,20 +41,26 @@ type DtypeFactorPower = (DTypeFactor, Exponent);
 #[derive(Clone, Debug, PartialEq, Eq)]
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct DType {
 pub struct DType {
     // Always in canonical form
     // Always in canonical form
-    pub factors: Vec<DtypeFactorPower>, // TODO make this private
+    factors: Vec<DtypeFactorPower>,
 }
 }
 
 
 impl DType {
 impl DType {
-    pub fn from_factors(factors: &[DtypeFactorPower]) -> DType {
-        let mut dtype = DType {
-            factors: factors.into(),
-        };
+    pub fn factors(&self) -> &[DtypeFactorPower] {
+        &self.factors
+    }
+
+    pub fn into_factors(self) -> Vec<DtypeFactorPower> {
+        self.factors
+    }
+
+    pub fn from_factors(factors: Vec<DtypeFactorPower>) -> DType {
+        let mut dtype = DType { factors };
         dtype.canonicalize();
         dtype.canonicalize();
         dtype
         dtype
     }
     }
 
 
     pub fn scalar() -> DType {
     pub fn scalar() -> DType {
-        DType::from_factors(&[])
+        DType::from_factors(vec![])
     }
     }
 
 
     pub fn is_scalar(&self) -> bool {
     pub fn is_scalar(&self) -> bool {
@@ -76,11 +82,12 @@ impl DType {
         names.extend(registry.get_derived_entry_names_for(&base_representation));
         names.extend(registry.get_derived_entry_names_for(&base_representation));
         match &names[..] {
         match &names[..] {
             [] => self.pretty_print(),
             [] => self.pretty_print(),
-            [single] => m::type_identifier(single),
-            multiple => {
-                Itertools::intersperse(multiple.iter().map(m::type_identifier), m::dimmed(" or "))
-                    .sum()
-            }
+            [single] => m::type_identifier(single.to_string()),
+            multiple => Itertools::intersperse(
+                multiple.iter().cloned().map(m::type_identifier),
+                m::dimmed(" or "),
+            )
+            .sum(),
         }
         }
     }
     }
 
 
@@ -92,11 +99,11 @@ impl DType {
     }
     }
 
 
     pub fn from_type_variable(v: TypeVariable) -> DType {
     pub fn from_type_variable(v: TypeVariable) -> DType {
-        DType::from_factors(&[(DTypeFactor::TVar(v), Exponent::from_integer(1))])
+        DType::from_factors(vec![(DTypeFactor::TVar(v), Exponent::from_integer(1))])
     }
     }
 
 
     pub fn from_type_parameter(name: String) -> DType {
     pub fn from_type_parameter(name: String) -> DType {
-        DType::from_factors(&[(DTypeFactor::TPar(name), Exponent::from_integer(1))])
+        DType::from_factors(vec![(DTypeFactor::TPar(name), Exponent::from_integer(1))])
     }
     }
 
 
     pub fn deconstruct_as_single_type_variable(&self) -> Option<TypeVariable> {
     pub fn deconstruct_as_single_type_variable(&self) -> Option<TypeVariable> {
@@ -109,14 +116,14 @@ impl DType {
     }
     }
 
 
     pub fn from_tgen(i: usize) -> DType {
     pub fn from_tgen(i: usize) -> DType {
-        DType::from_factors(&[(
+        DType::from_factors(vec![(
             DTypeFactor::TVar(TypeVariable::Quantified(i)),
             DTypeFactor::TVar(TypeVariable::Quantified(i)),
             Exponent::from_integer(1),
             Exponent::from_integer(1),
         )])
         )])
     }
     }
 
 
     pub fn base_dimension(name: &str) -> DType {
     pub fn base_dimension(name: &str) -> DType {
-        DType::from_factors(&[(
+        DType::from_factors(vec![(
             DTypeFactor::BaseDimension(name.into()),
             DTypeFactor::BaseDimension(name.into()),
             Exponent::from_integer(1),
             Exponent::from_integer(1),
         )])
         )])
@@ -157,16 +164,16 @@ impl DType {
     pub fn multiply(&self, other: &DType) -> DType {
     pub fn multiply(&self, other: &DType) -> DType {
         let mut factors = self.factors.clone();
         let mut factors = self.factors.clone();
         factors.extend(other.factors.clone());
         factors.extend(other.factors.clone());
-        DType::from_factors(&factors)
+        DType::from_factors(factors)
     }
     }
 
 
     pub fn power(&self, n: Exponent) -> DType {
     pub fn power(&self, n: Exponent) -> DType {
-        let factors: Vec<_> = self
+        let factors = self
             .factors
             .factors
             .iter()
             .iter()
             .map(|(f, m)| (f.clone(), n * m))
             .map(|(f, m)| (f.clone(), n * m))
             .collect();
             .collect();
-        DType::from_factors(&factors)
+        DType::from_factors(factors)
     }
     }
 
 
     pub fn inverse(&self) -> DType {
     pub fn inverse(&self) -> DType {
@@ -220,7 +227,7 @@ impl DType {
                 }
                 }
             }
             }
         }
         }
-        Self::from_factors(&factors)
+        Self::from_factors(factors)
     }
     }
 
 
     pub fn to_base_representation(&self) -> BaseRepresentation {
     pub fn to_base_representation(&self) -> BaseRepresentation {
@@ -259,11 +266,11 @@ impl std::fmt::Display for DType {
 
 
 impl From<BaseRepresentation> for DType {
 impl From<BaseRepresentation> for DType {
     fn from(base_representation: BaseRepresentation) -> Self {
     fn from(base_representation: BaseRepresentation) -> Self {
-        let factors: Vec<_> = base_representation
+        let factors = base_representation
             .into_iter()
             .into_iter()
             .map(|BaseRepresentationFactor(name, exp)| (DTypeFactor::BaseDimension(name), exp))
             .map(|BaseRepresentationFactor(name, exp)| (DTypeFactor::BaseDimension(name), exp))
             .collect();
             .collect();
-        DType::from_factors(&factors)
+        DType::from_factors(factors)
     }
     }
 }
 }
 
 
@@ -325,11 +332,11 @@ impl std::fmt::Display for Type {
 impl PrettyPrint for Type {
 impl PrettyPrint for Type {
     fn pretty_print(&self) -> Markup {
     fn pretty_print(&self) -> Markup {
         match self {
         match self {
-            Type::TVar(TypeVariable::Named(name)) => m::type_identifier(name),
+            Type::TVar(TypeVariable::Named(name)) => m::type_identifier(name.clone()),
             Type::TVar(TypeVariable::Quantified(_)) => {
             Type::TVar(TypeVariable::Quantified(_)) => {
                 unreachable!("Quantified types should not be printed")
                 unreachable!("Quantified types should not be printed")
             }
             }
-            Type::TPar(name) => m::type_identifier(name),
+            Type::TPar(name) => m::type_identifier(name.clone()),
             Type::Dimension(d) => d.pretty_print(),
             Type::Dimension(d) => d.pretty_print(),
             Type::Boolean => m::type_identifier("Bool"),
             Type::Boolean => m::type_identifier("Bool"),
             Type::String => m::type_identifier("String"),
             Type::String => m::type_identifier("String"),
@@ -349,7 +356,7 @@ impl PrettyPrint for Type {
                     + return_type.pretty_print()
                     + return_type.pretty_print()
                     + m::operator("]")
                     + m::operator("]")
             }
             }
-            Type::Struct(info) => m::type_identifier(&info.name),
+            Type::Struct(info) => m::type_identifier(info.name.clone()),
             Type::List(element_type) => {
             Type::List(element_type) => {
                 m::type_identifier("List")
                 m::type_identifier("List")
                     + m::operator("<")
                     + m::operator("<")
@@ -451,16 +458,16 @@ impl Type {
 }
 }
 
 
 #[derive(Debug, Clone, PartialEq)]
 #[derive(Debug, Clone, PartialEq)]
-pub enum StringPart {
+pub enum StringPart<'a> {
     Fixed(String),
     Fixed(String),
     Interpolation {
     Interpolation {
         span: Span,
         span: Span,
-        expr: Box<Expression>,
-        format_specifiers: Option<String>,
+        expr: Box<Expression<'a>>,
+        format_specifiers: Option<&'a str>,
     },
     },
 }
 }
 
 
-impl PrettyPrint for StringPart {
+impl PrettyPrint for StringPart<'_> {
     fn pretty_print(&self) -> Markup {
     fn pretty_print(&self) -> Markup {
         match self {
         match self {
             StringPart::Fixed(s) => m::string(escape_numbat_string(s)),
             StringPart::Fixed(s) => m::string(escape_numbat_string(s)),
@@ -472,7 +479,7 @@ impl PrettyPrint for StringPart {
                 let mut markup = m::operator("{") + expr.pretty_print();
                 let mut markup = m::operator("{") + expr.pretty_print();
 
 
                 if let Some(format_specifiers) = format_specifiers {
                 if let Some(format_specifiers) = format_specifiers {
-                    markup += m::text(format_specifiers);
+                    markup += m::text(format_specifiers.to_string());
                 }
                 }
 
 
                 markup += m::operator("}");
                 markup += m::operator("}");
@@ -483,23 +490,23 @@ impl PrettyPrint for StringPart {
     }
     }
 }
 }
 
 
-impl PrettyPrint for &Vec<StringPart> {
+impl PrettyPrint for &Vec<StringPart<'_>> {
     fn pretty_print(&self) -> Markup {
     fn pretty_print(&self) -> Markup {
         m::operator("\"") + self.iter().map(|p| p.pretty_print()).sum() + m::operator("\"")
         m::operator("\"") + self.iter().map(|p| p.pretty_print()).sum() + m::operator("\"")
     }
     }
 }
 }
 
 
 #[derive(Debug, Clone, PartialEq)]
 #[derive(Debug, Clone, PartialEq)]
-pub enum Expression {
+pub enum Expression<'a> {
     Scalar(Span, Number, TypeScheme),
     Scalar(Span, Number, TypeScheme),
-    Identifier(Span, String, TypeScheme),
+    Identifier(Span, &'a str, TypeScheme),
     UnitIdentifier(Span, Prefix, String, String, TypeScheme),
     UnitIdentifier(Span, Prefix, String, String, TypeScheme),
-    UnaryOperator(Span, UnaryOperator, Box<Expression>, TypeScheme),
+    UnaryOperator(Span, UnaryOperator, Box<Expression<'a>>, TypeScheme),
     BinaryOperator(
     BinaryOperator(
         Option<Span>,
         Option<Span>,
         BinaryOperator,
         BinaryOperator,
-        Box<Expression>,
-        Box<Expression>,
+        Box<Expression<'a>>,
+        Box<Expression<'a>>,
         TypeScheme,
         TypeScheme,
     ),
     ),
     /// A special binary operator that has a DateTime as one (or both) of the operands
     /// A special binary operator that has a DateTime as one (or both) of the operands
@@ -507,32 +514,37 @@ pub enum Expression {
         Option<Span>,
         Option<Span>,
         BinaryOperator,
         BinaryOperator,
         /// LHS must evaluate to a DateTime
         /// LHS must evaluate to a DateTime
-        Box<Expression>,
+        Box<Expression<'a>>,
         /// RHS can evaluate to a DateTime or a quantity of type Time
         /// RHS can evaluate to a DateTime or a quantity of type Time
-        Box<Expression>,
+        Box<Expression<'a>>,
         TypeScheme,
         TypeScheme,
     ),
     ),
     // A 'proper' function call
     // A 'proper' function call
-    FunctionCall(Span, Span, String, Vec<Expression>, TypeScheme),
+    FunctionCall(Span, Span, &'a str, Vec<Expression<'a>>, TypeScheme),
     // A call via a function object
     // A call via a function object
-    CallableCall(Span, Box<Expression>, Vec<Expression>, TypeScheme),
+    CallableCall(Span, Box<Expression<'a>>, Vec<Expression<'a>>, TypeScheme),
     Boolean(Span, bool),
     Boolean(Span, bool),
-    Condition(Span, Box<Expression>, Box<Expression>, Box<Expression>),
-    String(Span, Vec<StringPart>),
-    InstantiateStruct(Span, Vec<(String, Expression)>, StructInfo),
+    Condition(
+        Span,
+        Box<Expression<'a>>,
+        Box<Expression<'a>>,
+        Box<Expression<'a>>,
+    ),
+    String(Span, Vec<StringPart<'a>>),
+    InstantiateStruct(Span, Vec<(&'a str, Expression<'a>)>, StructInfo),
     AccessField(
     AccessField(
         Span,
         Span,
         Span,
         Span,
-        Box<Expression>,
-        String,     // field name
+        Box<Expression<'a>>,
+        &'a str,    // field name
         TypeScheme, // struct type
         TypeScheme, // struct type
         TypeScheme, // resulting field type
         TypeScheme, // resulting field type
     ),
     ),
-    List(Span, Vec<Expression>, TypeScheme),
+    List(Span, Vec<Expression<'a>>, TypeScheme),
     TypedHole(Span, TypeScheme),
     TypedHole(Span, TypeScheme),
 }
 }
 
 
-impl Expression {
+impl Expression<'_> {
     pub fn full_span(&self) -> Span {
     pub fn full_span(&self) -> Span {
         match self {
         match self {
             Expression::Scalar(span, ..) => *span,
             Expression::Scalar(span, ..) => *span,
@@ -570,9 +582,9 @@ impl Expression {
 
 
 #[derive(Debug, Clone, PartialEq)]
 #[derive(Debug, Clone, PartialEq)]
 pub struct DefineVariable<'a>(
 pub struct DefineVariable<'a>(
-    pub String,
+    pub &'a str,
     pub Vec<Decorator<'a>>,
     pub Vec<Decorator<'a>>,
-    pub Expression,
+    pub Expression<'a>,
     pub Option<TypeAnnotation>,
     pub Option<TypeAnnotation>,
     pub TypeScheme,
     pub TypeScheme,
     pub Markup,
     pub Markup,
@@ -580,41 +592,41 @@ pub struct DefineVariable<'a>(
 
 
 #[derive(Debug, Clone, PartialEq)]
 #[derive(Debug, Clone, PartialEq)]
 pub enum Statement<'a> {
 pub enum Statement<'a> {
-    Expression(Expression),
+    Expression(Expression<'a>),
     DefineVariable(DefineVariable<'a>),
     DefineVariable(DefineVariable<'a>),
     DefineFunction(
     DefineFunction(
-        String,
-        Vec<Decorator<'a>>,                        // decorators
-        Vec<(String, Option<TypeParameterBound>)>, // type parameters
+        &'a str,
+        Vec<Decorator<'a>>,                         // decorators
+        Vec<(&'a str, Option<TypeParameterBound>)>, // type parameters
         Vec<(
         Vec<(
             // parameters:
             // parameters:
             Span,                   // span of the parameter
             Span,                   // span of the parameter
-            String,                 // parameter name
+            &'a str,                // parameter name
             Option<TypeAnnotation>, // parameter type annotation
             Option<TypeAnnotation>, // parameter type annotation
             Markup,                 // readable parameter type
             Markup,                 // readable parameter type
         )>,
         )>,
-        Option<Expression>,      // function body
+        Option<Expression<'a>>,  // function body
         Vec<DefineVariable<'a>>, // local variables
         Vec<DefineVariable<'a>>, // local variables
         TypeScheme,              // function type
         TypeScheme,              // function type
         Option<TypeAnnotation>,  // return type annotation
         Option<TypeAnnotation>,  // return type annotation
         Markup,                  // readable return type
         Markup,                  // readable return type
     ),
     ),
-    DefineDimension(String, Vec<TypeExpression>),
+    DefineDimension(&'a str, Vec<TypeExpression>),
     DefineBaseUnit(
     DefineBaseUnit(
-        String,
+        &'a str,
         Vec<Decorator<'a>>,
         Vec<Decorator<'a>>,
         Option<TypeAnnotation>,
         Option<TypeAnnotation>,
         TypeScheme,
         TypeScheme,
     ),
     ),
     DefineDerivedUnit(
     DefineDerivedUnit(
-        String,
-        Expression,
+        &'a str,
+        Expression<'a>,
         Vec<Decorator<'a>>,
         Vec<Decorator<'a>>,
         Option<TypeAnnotation>,
         Option<TypeAnnotation>,
         TypeScheme,
         TypeScheme,
         Markup,
         Markup,
     ),
     ),
-    ProcedureCall(crate::ast::ProcedureKind, Vec<Expression>),
+    ProcedureCall(crate::ast::ProcedureKind, Vec<Expression<'a>>),
     DefineStruct(StructInfo),
     DefineStruct(StructInfo),
 }
 }
 
 
@@ -668,9 +680,8 @@ impl Statement<'_> {
                 return_type_annotation,
                 return_type_annotation,
                 readable_return_type,
                 readable_return_type,
             ) => {
             ) => {
-                let (fn_type, _) = fn_type.instantiate_for_printing(Some(
-                    type_parameters.iter().map(|(n, _)| n.as_str()),
-                ));
+                let (fn_type, _) =
+                    fn_type.instantiate_for_printing(Some(type_parameters.iter().map(|(n, _)| *n)));
 
 
                 for DefineVariable(_, _, _, type_annotation, type_, readable_type) in
                 for DefineVariable(_, _, _, type_annotation, type_, readable_type) in
                     local_variables
                     local_variables
@@ -751,7 +762,7 @@ impl Statement<'_> {
     }
     }
 }
 }
 
 
-impl Expression {
+impl Expression<'_> {
     pub fn get_type(&self) -> Type {
     pub fn get_type(&self) -> Type {
         match self {
         match self {
             Expression::Scalar(_, _, type_) => type_.unsafe_as_concrete(),
             Expression::Scalar(_, _, type_) => type_.unsafe_as_concrete(),
@@ -846,8 +857,8 @@ fn decorator_markup(decorators: &Vec<Decorator>) -> Markup {
                     m::decorator("@aliases")
                     m::decorator("@aliases")
                         + m::operator("(")
                         + m::operator("(")
                         + Itertools::intersperse(
                         + Itertools::intersperse(
-                            names.iter().map(|(name, accepts_prefix)| {
-                                m::unit(name) + accepts_prefix_markup(accepts_prefix)
+                            names.iter().map(|(name, accepts_prefix, _)| {
+                                m::unit(name.to_string()) + accepts_prefix_markup(accepts_prefix)
                             }),
                             }),
                             m::operator(", "),
                             m::operator(", "),
                         )
                         )
@@ -855,15 +866,32 @@ fn decorator_markup(decorators: &Vec<Decorator>) -> Markup {
                         + m::operator(")")
                         + m::operator(")")
                 }
                 }
                 Decorator::Url(url) => {
                 Decorator::Url(url) => {
-                    m::decorator("@url") + m::operator("(") + m::string(url) + m::operator(")")
+                    m::decorator("@url")
+                        + m::operator("(")
+                        + m::string(url.clone())
+                        + m::operator(")")
                 }
                 }
                 Decorator::Name(name) => {
                 Decorator::Name(name) => {
-                    m::decorator("@name") + m::operator("(") + m::string(name) + m::operator(")")
+                    m::decorator("@name")
+                        + m::operator("(")
+                        + m::string(name.clone())
+                        + m::operator(")")
                 }
                 }
                 Decorator::Description(description) => {
                 Decorator::Description(description) => {
                     m::decorator("@description")
                     m::decorator("@description")
                         + m::operator("(")
                         + m::operator("(")
-                        + m::string(description)
+                        + m::string(description.clone())
+                        + m::operator(")")
+                }
+                Decorator::Example(example_code, example_description) => {
+                    m::decorator("@example")
+                        + m::operator("(")
+                        + m::string(example_code.clone())
+                        + if let Some(example_description) = example_description {
+                            m::operator(", ") + m::string(example_description.clone())
+                        } else {
+                            m::empty()
+                        }
                         + m::operator(")")
                         + m::operator(")")
                 }
                 }
             }
             }
@@ -890,7 +918,7 @@ pub fn pretty_print_function_signature<'a>(
         m::operator("<")
         m::operator("<")
             + Itertools::intersperse(
             + Itertools::intersperse(
                 type_parameters.iter().map(|tv| {
                 type_parameters.iter().map(|tv| {
-                    m::type_identifier(tv.unsafe_name())
+                    m::type_identifier(tv.unsafe_name().to_string())
                         + if fn_type.bounds.is_dtype_bound(tv) {
                         + if fn_type.bounds.is_dtype_bound(tv) {
                             m::operator(":") + m::space() + m::type_identifier("Dim")
                             m::operator(":") + m::space() + m::type_identifier("Dim")
                         } else {
                         } else {
@@ -905,7 +933,7 @@ pub fn pretty_print_function_signature<'a>(
 
 
     let markup_parameters = Itertools::intersperse(
     let markup_parameters = Itertools::intersperse(
         parameters.map(|(name, parameter_type)| {
         parameters.map(|(name, parameter_type)| {
-            m::identifier(name) + m::operator(":") + m::space() + parameter_type.clone()
+            m::identifier(name.to_string()) + m::operator(":") + m::space() + parameter_type
         }),
         }),
         m::operator(", "),
         m::operator(", "),
     )
     )
@@ -916,7 +944,7 @@ pub fn pretty_print_function_signature<'a>(
 
 
     m::keyword("fn")
     m::keyword("fn")
         + m::space()
         + m::space()
-        + m::identifier(function_name)
+        + m::identifier(function_name.to_string())
         + markup_type_parameters
         + markup_type_parameters
         + m::operator("(")
         + m::operator("(")
         + markup_parameters
         + markup_parameters
@@ -937,7 +965,7 @@ impl PrettyPrint for Statement<'_> {
             )) => {
             )) => {
                 m::keyword("let")
                 m::keyword("let")
                     + m::space()
                     + m::space()
-                    + m::identifier(identifier)
+                    + m::identifier(identifier.to_string())
                     + m::operator(":")
                     + m::operator(":")
                     + m::space()
                     + m::space()
                     + readable_type.clone()
                     + readable_type.clone()
@@ -957,9 +985,8 @@ impl PrettyPrint for Statement<'_> {
                 _return_type_annotation,
                 _return_type_annotation,
                 readable_return_type,
                 readable_return_type,
             ) => {
             ) => {
-                let (fn_type, type_parameters) = fn_type.instantiate_for_printing(Some(
-                    type_parameters.iter().map(|(n, _)| n.as_str()),
-                ));
+                let (fn_type, type_parameters) =
+                    fn_type.instantiate_for_printing(Some(type_parameters.iter().map(|(n, _)| *n)));
 
 
                 let mut pretty_local_variables = None;
                 let mut pretty_local_variables = None;
                 let mut first = true;
                 let mut first = true;
@@ -984,7 +1011,7 @@ impl PrettyPrint for Statement<'_> {
                         plv += m::nl()
                         plv += m::nl()
                             + introducer_keyword
                             + introducer_keyword
                             + m::space()
                             + m::space()
-                            + m::identifier(identifier)
+                            + m::identifier(identifier.to_string())
                             + m::operator(":")
                             + m::operator(":")
                             + m::space()
                             + m::space()
                             + readable_type.clone()
                             + readable_type.clone()
@@ -1002,7 +1029,7 @@ impl PrettyPrint for Statement<'_> {
                     &type_parameters,
                     &type_parameters,
                     parameters
                     parameters
                         .iter()
                         .iter()
-                        .map(|(_, name, _, type_)| (name.as_str(), type_.clone())),
+                        .map(|(_, name, _, type_)| (*name, type_.clone())),
                     readable_return_type,
                     readable_return_type,
                 ) + body
                 ) + body
                     .as_ref()
                     .as_ref()
@@ -1012,12 +1039,12 @@ impl PrettyPrint for Statement<'_> {
             }
             }
             Statement::Expression(expr) => expr.pretty_print(),
             Statement::Expression(expr) => expr.pretty_print(),
             Statement::DefineDimension(identifier, dexprs) if dexprs.is_empty() => {
             Statement::DefineDimension(identifier, dexprs) if dexprs.is_empty() => {
-                m::keyword("dimension") + m::space() + m::type_identifier(identifier)
+                m::keyword("dimension") + m::space() + m::type_identifier(identifier.to_string())
             }
             }
             Statement::DefineDimension(identifier, dexprs) => {
             Statement::DefineDimension(identifier, dexprs) => {
                 m::keyword("dimension")
                 m::keyword("dimension")
                     + m::space()
                     + m::space()
-                    + m::type_identifier(identifier)
+                    + m::type_identifier(identifier.to_string())
                     + m::space()
                     + m::space()
                     + m::operator("=")
                     + m::operator("=")
                     + m::space()
                     + m::space()
@@ -1031,7 +1058,7 @@ impl PrettyPrint for Statement<'_> {
                 decorator_markup(decorators)
                 decorator_markup(decorators)
                     + m::keyword("unit")
                     + m::keyword("unit")
                     + m::space()
                     + m::space()
-                    + m::unit(identifier)
+                    + m::unit(identifier.to_string())
                     + m::operator(":")
                     + m::operator(":")
                     + m::space()
                     + m::space()
                     + annotation
                     + annotation
@@ -1050,7 +1077,7 @@ impl PrettyPrint for Statement<'_> {
                 decorator_markup(decorators)
                 decorator_markup(decorators)
                     + m::keyword("unit")
                     + m::keyword("unit")
                     + m::space()
                     + m::space()
-                    + m::unit(identifier)
+                    + m::unit(identifier.to_string())
                     + m::operator(":")
                     + m::operator(":")
                     + m::space()
                     + m::space()
                     + readable_type.clone()
                     + readable_type.clone()
@@ -1087,7 +1114,7 @@ impl PrettyPrint for Statement<'_> {
                         m::space()
                         m::space()
                             + Itertools::intersperse(
                             + Itertools::intersperse(
                                 fields.iter().map(|(n, (_, t))| {
                                 fields.iter().map(|(n, (_, t))| {
-                                    m::identifier(n)
+                                    m::identifier(n.clone())
                                         + m::operator(":")
                                         + m::operator(":")
                                         + m::space()
                                         + m::space()
                                         + t.pretty_print()
                                         + t.pretty_print()
@@ -1158,7 +1185,7 @@ fn pretty_print_binop(op: &BinaryOperator, lhs: &Expression, rhs: &Expression) -
             }
             }
             (Expression::Scalar(_, s, _), Expression::Identifier(_, name, _type)) => {
             (Expression::Scalar(_, s, _), Expression::Identifier(_, name, _type)) => {
                 // Fuse multiplication of a scalar and identifier
                 // Fuse multiplication of a scalar and identifier
-                pretty_scalar(*s) + m::space() + m::identifier(name)
+                pretty_scalar(*s) + m::space() + m::identifier(name.to_string())
             }
             }
             _ => {
             _ => {
                 let add_parens_if_needed = |expr: &Expression| {
                 let add_parens_if_needed = |expr: &Expression| {
@@ -1242,13 +1269,13 @@ fn pretty_print_binop(op: &BinaryOperator, lhs: &Expression, rhs: &Expression) -
     }
     }
 }
 }
 
 
-impl PrettyPrint for Expression {
+impl PrettyPrint for Expression<'_> {
     fn pretty_print(&self) -> Markup {
     fn pretty_print(&self) -> Markup {
         use Expression::*;
         use Expression::*;
 
 
         match self {
         match self {
             Scalar(_, n, _) => pretty_scalar(*n),
             Scalar(_, n, _) => pretty_scalar(*n),
-            Identifier(_, name, _type) => m::identifier(name),
+            Identifier(_, name, _type) => m::identifier(name.to_string()),
             UnitIdentifier(_, prefix, _name, full_name, _type) => {
             UnitIdentifier(_, prefix, _name, full_name, _type) => {
                 m::unit(format!("{}{}", prefix.as_string_long(), full_name))
                 m::unit(format!("{}{}", prefix.as_string_long(), full_name))
             }
             }
@@ -1264,7 +1291,7 @@ impl PrettyPrint for Expression {
             BinaryOperator(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs),
             BinaryOperator(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs),
             BinaryOperatorForDate(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs),
             BinaryOperatorForDate(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs),
             FunctionCall(_, _, name, args, _type) => {
             FunctionCall(_, _, name, args, _type) => {
-                m::identifier(name)
+                m::identifier(name.to_string())
                     + m::operator("(")
                     + m::operator("(")
                     + itertools::Itertools::intersperse(
                     + itertools::Itertools::intersperse(
                         args.iter().map(|e| e.pretty_print()),
                         args.iter().map(|e| e.pretty_print()),
@@ -1308,7 +1335,7 @@ impl PrettyPrint for Expression {
                         m::space()
                         m::space()
                             + itertools::Itertools::intersperse(
                             + itertools::Itertools::intersperse(
                                 exprs.iter().map(|(n, e)| {
                                 exprs.iter().map(|(n, e)| {
-                                    m::identifier(n)
+                                    m::identifier(n.to_string())
                                         + m::operator(":")
                                         + m::operator(":")
                                         + m::space()
                                         + m::space()
                                         + e.pretty_print()
                                         + e.pretty_print()
@@ -1321,7 +1348,7 @@ impl PrettyPrint for Expression {
                     + m::operator("}")
                     + m::operator("}")
             }
             }
             AccessField(_, _, expr, attr, _, _) => {
             AccessField(_, _, expr, attr, _, _) => {
-                expr.pretty_print() + m::operator(".") + m::identifier(attr)
+                expr.pretty_print() + m::operator(".") + m::identifier(attr.to_string())
             }
             }
             List(_, elements, _) => {
             List(_, elements, _) => {
                 m::operator("[")
                 m::operator("[")
@@ -1410,7 +1437,7 @@ mod tests {
         let transformed_statements = transformer.transform(statements).unwrap().replace_spans();
         let transformed_statements = transformer.transform(statements).unwrap().replace_spans();
 
 
         crate::typechecker::TypeChecker::default()
         crate::typechecker::TypeChecker::default()
-            .check(transformed_statements)
+            .check(&transformed_statements)
             .unwrap()
             .unwrap()
             .last()
             .last()
             .unwrap()
             .unwrap()

+ 2 - 2
numbat/src/unicode_input.rs

@@ -54,8 +54,8 @@ pub const UNICODE_INPUT: &[(&[&str], &str)] = &[
     (&["beta"], "β"),
     (&["beta"], "β"),
     (&["gamma"], "γ"),
     (&["gamma"], "γ"),
     (&["delta"], "δ"),
     (&["delta"], "δ"),
-    (&["epsilon"], "ε"),
-    (&["varepsilon"], "ϵ"),
+    (&["epsilon"], "ϵ"),
+    (&["varepsilon"], "ε"),
     (&["zeta"], "ζ"),
     (&["zeta"], "ζ"),
     (&["eta"], "η"),
     (&["eta"], "η"),
     (&["theta"], "θ"),
     (&["theta"], "θ"),

+ 19 - 27
numbat/src/unit.rs

@@ -269,33 +269,25 @@ impl Unit {
     }
     }
 
 
     pub fn to_base_unit_representation(&self) -> (Self, ConversionFactor) {
     pub fn to_base_unit_representation(&self) -> (Self, ConversionFactor) {
-        // TODO: reduce wrapping/unwrapping and duplication.
-
-        let base_unit_representation = self
-            .iter()
-            .map(
-                |UnitFactor {
-                     prefix: _,
-                     unit_id: base_unit,
-                     exponent,
-                 }| { base_unit.base_unit_and_factor().0.power(*exponent) },
-            )
-            .product::<Self>()
-            .canonicalized();
-
-        let factor = self
-            .iter()
-            .map(
-                |UnitFactor {
-                     prefix,
-                     unit_id: base_unit,
-                     exponent,
-                 }| {
-                    (prefix.factor() * base_unit.base_unit_and_factor().1)
-                        .pow(&Number::from_f64(exponent.to_f64().unwrap())) // TODO do we want to use exponent.to_f64?
-                },
-            )
-            .product();
+        // TODO: reduce wrapping/unwrapping
+        let mut base_unit_representation = Product::unity();
+        let mut factor = Number::from_f64(1.0);
+
+        for UnitFactor {
+            unit_id: base_unit,
+            prefix,
+            exponent,
+        } in self.iter()
+        {
+            base_unit_representation =
+                base_unit_representation * base_unit.base_unit_and_factor().0.power(*exponent);
+            factor = factor
+                * (prefix.factor() * base_unit.base_unit_and_factor().1)
+                    // TODO do we want to use exponent.to_f64?
+                    .pow(&Number::from_f64(exponent.to_f64().unwrap()));
+        }
+
+        base_unit_representation.canonicalize();
 
 
         (base_unit_representation, factor)
         (base_unit_representation, factor)
     }
     }

+ 2 - 2
numbat/src/value.rs

@@ -156,7 +156,7 @@ impl PrettyPrint for Value {
             Value::String(s) => s.pretty_print(),
             Value::String(s) => s.pretty_print(),
             Value::DateTime(dt) => crate::markup::string(crate::datetime::to_string(dt)),
             Value::DateTime(dt) => crate::markup::string(crate::datetime::to_string(dt)),
             Value::FunctionReference(r) => crate::markup::string(r.to_string()),
             Value::FunctionReference(r) => crate::markup::string(r.to_string()),
-            Value::FormatSpecifiers(Some(s)) => crate::markup::string(s),
+            Value::FormatSpecifiers(Some(s)) => crate::markup::string(s.clone()),
             Value::FormatSpecifiers(None) => crate::markup::empty(),
             Value::FormatSpecifiers(None) => crate::markup::empty(),
             Value::StructInstance(struct_info, values) => {
             Value::StructInstance(struct_info, values) => {
                 crate::markup::type_identifier(struct_info.name.clone())
                 crate::markup::type_identifier(struct_info.name.clone())
@@ -168,7 +168,7 @@ impl PrettyPrint for Value {
                         crate::markup::space()
                         crate::markup::space()
                             + itertools::Itertools::intersperse(
                             + itertools::Itertools::intersperse(
                                 struct_info.fields.keys().zip(values).map(|(name, val)| {
                                 struct_info.fields.keys().zip(values).map(|(name, val)| {
-                                    crate::markup::identifier(name)
+                                    crate::markup::identifier(name.clone())
                                         + crate::markup::operator(":")
                                         + crate::markup::operator(":")
                                         + crate::markup::space()
                                         + crate::markup::space()
                                         + val.pretty_print()
                                         + val.pretty_print()