浏览代码

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

David Peter 1 年之前
父节点
当前提交
ac53ad595e
共有 66 个文件被更改,包括 2218 次插入550 次删除
  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",
  "numbat-exchange-rates",
  "once_cell",
+ "percent-encoding",
  "plotly",
  "pretty_dtoa",
  "rand",

+ 8 - 4
book/build.py

@@ -1,6 +1,7 @@
 import subprocess
 from pathlib import Path
 import urllib.parse
+import os
 
 
 SCRIPT_DIR = Path(__file__).parent.resolve()
@@ -15,14 +16,14 @@ def generate_example(
     print(path_out)
 
     code = []
-    with open(path_in, "r") as fin:
+    with open(path_in, "r", encoding="utf-8") as fin:
         for line in fin:
             if not (strip_asserts and "assert_eq" in line):
                 code.append(line)
 
     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("\n")
         fout.write(f"# {title}\n")
@@ -80,7 +81,7 @@ generate_example(
 )
 
 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)
     subprocess.run(
         ["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):
     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)
 
         if introduction := document.get("introduction"):
@@ -119,6 +120,8 @@ def list_of_functions(file_name, document):
                 print(
                     f"Generating list of functions for module '{module}'...", flush=True
                 )
+                env = os.environ.copy()
+                env["TZ"] = "UTC"
                 subprocess.run(
                     [
                         "cargo",
@@ -132,6 +135,7 @@ def list_of_functions(file_name, document):
                     ],
                     stdout=f,
                     text=True,
+                    env=env,
                 )
 
 

+ 5 - 1
book/src/SUMMARY.md

@@ -21,7 +21,6 @@
     - [XKCD 687](./example-xkcd_687.md)
     - [XKCD 2585](./example-xkcd_2585.md)
     - [XKCD 2812](./example-xkcd_2812.md)
-- [IDE / editor integration](./editor-integration.md)
 
 # Numbat language reference
 
@@ -69,6 +68,11 @@
 
 - [Type system](./type-system.md)
 
+# Other topics
+
+- [IDE / editor integration](./editor-integration.md)
+- [Comparison with other tools](./comparison.md)
+
 # Support
 
 - [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?
 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
 ```
 

+ 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
 ```
 
+<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`
 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
 ```
 
+<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`
 Returns the users local timezone.
 
@@ -32,6 +62,16 @@ Returns the users local timezone.
 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`
 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]
 ```
 
+<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`
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 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
 ```
 
+<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)
 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).
@@ -103,6 +218,16 @@ More information [here](https://en.wikipedia.org/wiki/Julian_day).
 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)
 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).
@@ -111,3 +236,14 @@ More information [here](https://numbat.dev/doc/date-and-time.html).
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 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>
 ```
 
+<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`
 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>
 ```
 
+<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`
 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>
 ```
 
+<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`
 Check if a list is empty.
 
@@ -44,6 +94,21 @@ Check if a list is empty.
 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`
 Concatenate two lists.
 
@@ -51,6 +116,16 @@ Concatenate two lists.
 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`
 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>
 ```
 
+<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`
 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>
 ```
 
+<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`
 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
 ```
 
+<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`
 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>
 ```
 
+<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 the order of a list.
 
@@ -86,6 +201,16 @@ Reverse the order of a list.
 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`
 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>
 ```
 
+<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 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>
 ```
 
+<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`
 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
 ```
 
+<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 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>
 ```
 
+<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 a list of quantities.
 
@@ -121,6 +290,16 @@ Sort a list of quantities.
 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`
 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>
 ```
 
+<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 all elements of a list.
 
@@ -135,6 +324,16 @@ Sum all elements of a list.
 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`
 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>
 ```
 
+<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`
 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
 ```
 
+<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 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>
 ```
 
+<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
 ```
 
+<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)
 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).
@@ -21,6 +31,16 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 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)
 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).
@@ -29,6 +49,16 @@ More information [here](https://en.wikipedia.org/wiki/Square_root).
 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)
 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).
@@ -37,6 +67,16 @@ More information [here](https://en.wikipedia.org/wiki/Cube_root).
 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)
 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
 ```
 
+<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 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).
@@ -52,13 +102,45 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 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 to the nearest multiple of `base`. For example: `round_in(m, 5.3 m) == 5 m`.
+Round to the nearest multiple of `base`.
 
 ```nbt
 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)
 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).
@@ -67,13 +149,40 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 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)
-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
 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)
 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).
@@ -82,13 +191,40 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 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)
-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
 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)
 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).
@@ -97,13 +233,45 @@ More information [here](https://doc.rust-lang.org/std/primitive.f64.html#method.
 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)
-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
 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)
 Calculates the least nonnegative remainder of \\( a (\mod b) \\).
 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
 ```
 
+<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
 
 Defined in: `math::transcendental`
@@ -124,6 +302,16 @@ More information [here](https://en.wikipedia.org/wiki/Exponential_function).
 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)
 The natural logarithm with base \\( e \\).
 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
 ```
 
+<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)
 The natural logarithm with base \\( e \\).
 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
 ```
 
+<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)
 The common logarithm with base \\( 10 \\).
 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
 ```
 
+<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)
 The binary logarithm with base \\( 2 \\).
 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
 ```
 
+<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)
 The gamma function, \\( \Gamma(x) \\).
 More information [here](https://en.wikipedia.org/wiki/Gamma_function).
@@ -264,27 +492,57 @@ fn atanh(x: Scalar) -> Scalar
 Defined in: `math::statistics`
 
 ### `maximum` (Maxmimum)
-Get the largest element of a list: `maximum([30 cm, 2 m]) = 2 m`.
+Get the largest element of a list.
 
 ```nbt
 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)
-Get the smallest element of a list: `minimum([30 cm, 2 m]) = 30 cm`.
+Get the smallest element of a list.
 
 ```nbt
 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)
-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).
 
 ```nbt
 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)
 Calculate the population variance of a list of quantities.
 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
 ```
 
+<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)
 Calculate the population standard deviation of a list of quantities.
 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
 ```
 
+<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)
 Calculate the median of a list of quantities.
 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
 ```
 
+<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
 
 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
 ```
 
+<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)
 The smallest positive integer that is divisible by both \\( a \\) and \\( b \\).
 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
 ```
 
+<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
 
 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
 ```
 
+<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)
 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).
@@ -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
 ```
 
+<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)
 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).
@@ -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
 ```
 
+<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)
 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).
@@ -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
 ```
 
+<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
 
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 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>
 ```
 
+<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)
 
 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
 ```
 
+<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`
 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).
@@ -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
 ```
 
+<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`
 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
 ```
 
+<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
 
 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
 ```
 
+<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`
 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
 ```
 
+<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
 
 Defined in: `chemistry::elements`
 
 ### `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
 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
 
 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)
 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).
 
 ```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)
 Convert an angle to a mixed degrees and decimal minutes representation.
 More information [here](https://en.wikipedia.org/wiki/Decimal_degrees).
 
 ```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)
 Convert a length to a mixed feet and inches representation.
 More information [here](https://en.wikipedia.org/wiki/Foot_(unit)).
 
 ```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)
 Convert a mass to a mixed pounds and ounces representation.
 More information [here](https://en.wikipedia.org/wiki/Pound_(mass)).
 
 ```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
 
 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
 ```
 
+<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`
 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).
@@ -125,6 +275,17 @@ More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_te
 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`
 Converts from degree Fahrenheit (°F) to Kelvin.
 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
 ```
 
+<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`
 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).
@@ -141,6 +313,17 @@ More information [here](https://en.wikipedia.org/wiki/Conversion_of_scales_of_te
 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
 
 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
 ```
 
+<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`
-Create a `Color` from a (hexadecimal) value, e.g. `color(0xff7700)`.
+Create a `Color` from a (hexadecimal) value.
 
 ```nbt
 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`
-Convert a color to its RGB representation, e.g. `cyan -> color_rgb`.
+Convert a color to its RGB representation.
 
 ```nbt
 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`
-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
 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`
-Convert a color to its hexadecimal representation, e.g. `rgb(225, 36, 143) -> color_hex`.
+Convert a color to its hexadecimal representation.
 
 ```nbt
 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
 ```
 
+<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`
 Subslice of a string.
 
@@ -16,20 +26,50 @@ Subslice of a 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`
-Get a single-character string from a Unicode code point. Example: `0x2764 -> chr`.
+Get a single-character string from a Unicode code point.
 
 ```nbt
 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`
-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
 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`
 Convert a string to lowercase.
 
@@ -37,6 +77,16 @@ Convert a string to lowercase.
 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`
 Convert a string to uppercase.
 
@@ -44,6 +94,16 @@ Convert a string to uppercase.
 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`
 Concatenate two strings.
 
@@ -51,6 +111,16 @@ Concatenate two strings.
 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`
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 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
 ```
 
+<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`
 Repeat the input string `n` times.
 
@@ -79,27 +179,67 @@ Repeat the input string `n` times.
 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`
-Convert a number to the given base. Example: `42 |> base(16)`.
+Convert a number to the given base.
 
 ```nbt
 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`
-Get a binary representation of a number. Example: `42 -> bin`.
+Get a binary representation of a number.
 
 ```nbt
 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`
-Get an octal representation of a number. Example: `42 -> oct`.
+Get an octal representation of a number.
 
 ```nbt
 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`
 Get a decimal representation of a number.
 
@@ -107,10 +247,30 @@ Get a decimal representation of a number.
 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`
-Get a hexadecimal representation of a number. Example: `2^31-1 -> hex`.
+Get a hexadecimal representation of a number.
 
 ```nbt
 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(-(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
 assert_eq(48° + 46′ + 32″, 48.7756°, 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)
 
-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
 
-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
 
-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
                 .map(|w| Pair {
                     display: w.to_string(),
-                    replacement: w.to_string(),
+                    replacement: w,
                 })
                 .collect(),
         ))

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

@@ -40,17 +40,23 @@ impl Highlighter for NumbatHighlighter {
         if ctx.variable_names().any(|n| 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
             .unit_names()
             .iter()
             .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) {
-            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) {
-            Cow::Owned(ansi_format(&markup::keyword(candidate), false))
+            Cow::Owned(ansi_format(&markup::keyword(candidate.to_string()), false))
         } else {
             Cow::Borrowed(candidate)
         }

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

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

+ 1 - 0
numbat/Cargo.toml

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

+ 113 - 20
numbat/examples/inspect.rs

@@ -1,5 +1,9 @@
 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;
 
 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 {
             unreachable!();
         };
@@ -57,19 +61,7 @@ fn inspect_functions_in_module(ctx: &Context, module: String) {
         }
 
         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('.') {
                 println!("{description}");
@@ -86,17 +78,118 @@ fn inspect_functions_in_module(ctx: &Context, module: String) {
         println!("{signature}");
         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();
     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 mut example_ctx = prepare_context();
+    let _result = example_ctx
+        .interpret("use prelude", CodeSource::Internal)
+        .unwrap();
+
     let mut args = std::env::args();
     args.next();
     if let Some(arg) = args.next() {
@@ -104,7 +197,7 @@ fn main() {
             "units" => inspect_units(&ctx),
             "functions" => {
                 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>]"),
         }

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

@@ -49,6 +49,8 @@ fn _convert_from_raw(raw: _ChemicalElementRaw) -> ChemicalElement =
     }
 
 @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 =
     _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")
 @description("Return the input value.")
+@example("id(8 kg)")
 fn id<A>(x: A) -> A = x
 
 @name("Absolute value")
 @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")
+@example("abs(-22.2 m)")
 fn abs<T: Dim>(x: T) -> T
 
 @name("Square root")
 @description("Return the square root $\\sqrt\{x\}$ of the input: `sqrt(121 m^2) = 11 m`.")
 @url("https://en.wikipedia.org/wiki/Square_root")
+@example("sqrt(4 are) -> m")
 fn sqrt<D: Dim>(x: D^2) -> D = x^(1/2)
 
 @name("Cube root")
 @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")
+@example("cbrt(8 L) -> cm")
 fn cbrt<D: Dim>(x: D^3) -> D = x^(1/3)
 
 @name("Square function")
 @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
 
 @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`.")
 @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
 
 @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
 
 @name("Floor function")
 @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")
+@example("floor(5.5)")
 fn floor(x: Scalar) -> Scalar
 
 @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
 
 @name("Ceil function")
 @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")
+@example("ceil(5.5)")
 fn ceil(x: Scalar) -> Scalar
 
 @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
 
 @name("Truncation")
 @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")
+@example("trunc(5.5)")
+@example("trunc(-5.5)")
 fn trunc(x: Scalar) -> Scalar
 
 @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
 
 @name("Modulo")
 @description("Calculates the least nonnegative remainder of $a (\\mod b)$.")
 @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

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

@@ -3,48 +3,60 @@ use core::error
 use core::strings
 
 @description("Get the length of a list")
+@example("len([3, 2, 1])")
 fn len<A>(xs: List<A>) -> Scalar
 
 @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
 
 @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>
 
 @description("Prepend an element to a list")
+@example("cons(77, [3, 2, 1])")
 fn cons<A>(x: A, xs: List<A>) -> List<A>
 
 @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>
 
 @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 == []
 
 @description("Concatenate two lists")
+@example("concat([3, 2, 1], [10, 11])")
 fn concat<A>(xs1: List<A>, xs2: List<A>) -> List<A> =
   if is_empty(xs1)
     then xs2
     else cons(head(xs1), concat(tail(xs1), xs2))
 
 @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> =
   if n == 0 || is_empty(xs)
     then []
     else cons(head(xs), take(n - 1, tail(xs)))
 
 @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> =
   if n == 0 || is_empty(xs)
     then xs
     else drop(n - 1, tail(xs))
 
 @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 =
   if i == 0
     then head(xs)
     else element_at(i - 1, tail(xs))
 
 @description("Generate a range of integer numbers from `start` to `end` (inclusive)")
+@example("range(2, 12)")
 fn range(start: Scalar, end: Scalar) -> List<Scalar> =
   if start > end
     then []
@@ -52,18 +64,21 @@ fn range(start: Scalar, end: Scalar) -> List<Scalar> =
 
 
 @description("Reverse the order of a list")
+@example("reverse([3, 2, 1])")
 fn reverse<A>(xs: List<A>) -> List<A> =
   if is_empty(xs)
     then []
     else cons_end(head(xs), reverse(tail(xs)))
 
 @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> =
   if is_empty(xs)
     then []
     else cons(f(head(xs)), map(f, tail(xs)))
 
 @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> =
   if is_empty(xs)
     then []
@@ -72,6 +87,7 @@ fn filter<A>(p: Fn[(A) -> Bool], xs: List<A>) -> List<A> =
       else filter(p, tail(xs))
 
 @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 =
   if is_empty(xs)
     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")
+@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> =
   if is_empty(xs)
     then []
@@ -98,9 +115,11 @@ fn sort_by_key<A, D: Dim>(key: Fn[(A) -> D], xs: List<A>) -> List<A> =
                   key)
 
 @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)
 
 @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> =
   if is_empty(xs)
     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
 @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)
 
 # 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))
 
 @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> =
   if n_steps <= 1
     then error("Number of steps must be larger than 1")
     else _linspace_helper(start, end, n_steps, 0)
 
 @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) =
   if is_empty(xs)
     then ""
@@ -133,6 +155,7 @@ fn join(xs: List<String>, sep: String) =
       else "{head(xs)}{sep}{join(tail(xs), sep)}"
 
 @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> =
   if input == ""
     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.
 
-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`.")
 @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
 
 @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")
+@example("is_infinite(37)")
+@example("is_infinite(-inf)")
 fn is_infinite<T: Dim>(n: T) -> Bool
 
 @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)

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

@@ -1,7 +1,9 @@
 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.")
+@example("unit_of(20 km/h)")
 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.")
+@example("value_of(20 km/h)")
 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
 
 @description("The length of a string")
+@example("str_length(\"Numbat\")")
 fn str_length(s: String) -> Scalar
 
 @description("Subslice of a string")
+@example("str_slice(\"Numbat\", 3, 6)")
 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
 
-@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
 
 @description("Convert a string to lowercase")
+@example("lowercase(\"Numbat\")")
 fn lowercase(s: String) -> String
 
 @description("Convert a string to uppercase")
+@example("uppercase(\"Numbat\")")
 fn uppercase(s: String) -> String
 
 @description("Concatenate two strings")
+@example("str_append(\"Numbat\", \"!\")")
 fn str_append(a: String, b: String) -> String = "{a}{b}"
 
 @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 =
   if len_haystack == 0
     then -1
@@ -36,10 +44,12 @@ fn str_find(haystack: String, needle: String) -> Scalar =
     and tail_haystack = str_slice(haystack, 1, len_haystack)
 
 @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 =
   str_find(haystack, needle) != -1
 
 @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 =
   if pattern == ""
     then s
@@ -50,6 +60,7 @@ fn str_replace(s: String, pattern: String, replacement: String) -> String =
            else s
 
 @description("Repeat the input string `n` times")
+@example("str_repeat(\"abc\", 4)")
 fn str_repeat(a: String, n: Scalar) -> String =
   if n > 0
     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
 # that it returns a partially-applied version of '_number_in_base'. This would allow
 # 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 =
   if x < 0
     then "-{base(b, -x)}"
@@ -84,14 +96,18 @@ fn base(b: Scalar, x: Scalar) -> String =
       then _digit_in_base(b, x)
       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)}"
 
-@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)}"
 
 @description("Get a decimal representation of a number.")
+@example("0b111 -> dec")
 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)}"

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

@@ -7,15 +7,22 @@ use units::time
 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.")
+@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
 
 @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
 
 @description("Returns the users local timezone.")
+@example("get_local_timezone()")
 fn get_local_timezone() -> String
 
 @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]
 
 @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")
 
 @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
 
 @description("Converts a UNIX timestamp to a `DateTime` object.")
+@example("from_unixtime(2^31)")
 fn from_unixtime(input: Scalar) -> DateTime
 
 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")
 
 @description("Parses a string (only date) into a `DateTime` object.")
+@example("date(\"2022-07-20\")")
 fn date(input: String) -> DateTime =
   if str_contains(input, " ")
     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
 
 @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 =
    if span_unit == days
      then _add_days(dt, span / days)
@@ -65,14 +76,17 @@ fn calendar_add(dt: DateTime, span: Time) -> DateTime =
     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.")
+@example("calendar_sub(datetime(\"2022-07-20 21:52 +0200\"), 3 years)")
 fn calendar_sub(dt: DateTime, span: Time) -> DateTime =
   calendar_add(dt, -span)
 
 @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)
 
 @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).")
 @url("https://en.wikipedia.org/wiki/Julian_day")
+@example("julian_date(datetime(\"2022-07-20 21:52 +0200\"))")
 fn julian_date(dt: DateTime) -> Time =
   (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")
 @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.")
+@example("century/1e6 -> human", "How long is a microcentury?")
 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")
 @url("https://en.wikipedia.org/wiki/Quadratic_equation")
 @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> =
   if a == 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)$.")
+@example("rgb(125, 128, 218)")
 fn rgb(red: Scalar, green: Scalar, blue: Scalar) -> Color =
   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 =
   rgb(
     floor(rgb_hex / 256^2),
@@ -22,15 +24,18 @@ fn color(rgb_hex: Scalar) -> Color =
 fn _color_to_scalar(color: Color) -> Scalar =
   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 =
   "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 =
   "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 =
   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")
 @url("https://en.wikipedia.org/wiki/Turn_(angle)#Tau_proposals")
+@aliases(tau)
 let τ = 2 π
 
 @name("Euler's number")

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

@@ -2,9 +2,11 @@ use core::functions
 use math::constants
 
 @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)
 
 @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)
 
 # 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")
 @description("The largest positive integer that divides each of the integers $a$ and $b$.")
 @url("https://en.wikipedia.org/wiki/Greatest_common_divisor")
+@example("gcd(60, 42)")
 fn gcd(a: Scalar, b: Scalar) -> Scalar =
   if b == 0
     then abs(a)
@@ -12,4 +13,5 @@ fn gcd(a: Scalar, b: Scalar) -> Scalar =
 @name("Least common multiple")
 @description("The smallest positive integer that is divisible by both $a$ and $b$.")
 @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)

+ 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
 
 @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 =
   if len(xs) == 1
     then head(xs)
     else _max(head(xs), maximum(tail(xs)))
 
 @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 =
   if len(xs) == 1
     then head(xs)
     else _min(head(xs), minimum(tail(xs)))
 
 @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")
 fn mean<D: Dim>(xs: List<D>) -> D = if is_empty(xs) then 0 else sum(xs) / len(xs)
 
 @name("Variance")
 @url("https://en.wikipedia.org/wiki/Variance")
 @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 =
   mean(map(sqr, xs)) - sqr(mean(xs))
 
 @name("Standard deviation")
 @url("https://en.wikipedia.org/wiki/Standard_deviation")
 @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))
 
 @name("Median")
 @url("https://en.wikipedia.org/wiki/Median")
 @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
   if mod(n, 2) == 1
     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")
 @description("The exponential function, $e^x$.")
 @url("https://en.wikipedia.org/wiki/Exponential_function")
+@example("exp(4)")
 fn exp(x: Scalar) -> Scalar
 
 @name("Natural logarithm")
 @description("The natural logarithm with base $e$.")
 @url("https://en.wikipedia.org/wiki/Natural_logarithm")
+@example("ln(20)")
 fn ln(x: Scalar) -> Scalar
 
 @name("Natural logarithm")
 @description("The natural logarithm with base $e$.")
 @url("https://en.wikipedia.org/wiki/Natural_logarithm")
+@example("log(20)")
 fn log(x: Scalar) -> Scalar = ln(x)
 
 @name("Common logarithm")
 @description("The common logarithm with base $10$.")
 @url("https://en.wikipedia.org/wiki/Common_logarithm")
+@example("log10(100)")
 fn log10(x: Scalar) -> Scalar
 
 @name("Binary logarithm")
 @description("The binary logarithm with base $2$.")
 @url("https://en.wikipedia.org/wiki/Binary_logarithm")
+@example("log2(256)")
 fn log2(x: Scalar) -> Scalar
 
 @name("Gamma function")

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

@@ -3,6 +3,8 @@ use core::quantities
 @name("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.")
+@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 =
   (f(x + Δx) - f(x - Δx)) / 2 Δx
   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")
 @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| < ε$.")
+@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) =
   _fixed_point(f, x0, ε, 100)

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

@@ -4,6 +4,7 @@ use core::error
 @name("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$.")
+@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 =
   if abs(x1 - x2) < x_tol
     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")
 @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.")
+@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 =
   _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
 
 @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")
 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`.")
+@example("300K -> celsius", "300 K in degree Celsius.")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 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
 
 @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")
 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`.")
+@example("300K -> fahrenheit", "300 K in degree Fahrenheit.")
 @url("https://en.wikipedia.org/wiki/Conversion_of_scales_of_temperature")
 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::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")
 @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")
-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")
 @description("Convert an angle to a mixed degrees and decimal minutes representation.")
 @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")
 @description("Convert a length to a mixed feet and inches representation.")
 @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")
 @description("Convert a mass to a mixed pounds and ounces representation.")
 @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")
 @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc")
-@aliases(arcminutes, arcmin, ′)
+@aliases(arcminutes, arcmin, ′: short)
 unit arcminute: Angle = 1 / 60 × degree
 
 @name("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
 
 @name("Are")

+ 23 - 18
numbat/src/ast.rs

@@ -36,21 +36,26 @@ impl PrettyPrint for BinaryOperator {
     fn pretty_print(&self) -> Markup {
         use BinaryOperator::*;
 
+        let operator = m::operator(match self {
+            Add => "+",
+            Sub => "-",
+            Mul => "×",
+            Div => "/",
+            Power => "^",
+            ConvertTo => "➞",
+            LessThan => "<",
+            GreaterThan => ">",
+            LessOrEqual => "≤",
+            GreaterOrEqual => "≥",
+            Equal => "==",
+            NotEqual => "≠",
+            LogicalAnd => "&&",
+            LogicalOr => "||",
+        });
+
         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 {
         span: Span,
         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> {
     Scalar(Span, Number),
     Identifier(Span, &'a str),
-    UnitIdentifier(Span, Prefix, String, String),
+    UnitIdentifier(Span, Prefix, String, String), // can't easily be made &'a str
     TypedHole(Span),
     UnaryOperator {
         op: UnaryOperator,
@@ -365,7 +370,7 @@ impl PrettyPrint for TypeExpression {
     fn pretty_print(&self) -> Markup {
         match self {
             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) => {
                 lhs.pretty_print() + m::space() + m::operator("×") + m::space() + rhs.pretty_print()
             }
@@ -507,7 +512,7 @@ impl ReplaceSpans for StringPart<'_> {
             } => StringPart::Interpolation {
                 span: Span::dummy(),
                 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)
                 {
                     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);
-                } else if let Some(is_foreign) = self.functions.get(identifier) {
+                } else if let Some(is_foreign) = self.functions.get(*identifier) {
                     let index = self
                         .vm
                         .add_constant(Constant::FunctionReference(if *is_foreign {
-                            FunctionReference::Foreign(identifier.clone())
+                            FunctionReference::Foreign(identifier.to_string())
                         } else {
-                            FunctionReference::Normal(identifier.clone())
+                            FunctionReference::Normal(identifier.to_string())
                         }));
                     self.vm.add_op1(Op::LoadConstant, index);
                 } else {
@@ -178,7 +178,7 @@ impl BytecodeInterpreter {
 
                 let sorted_exprs = exprs
                     .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() {
                     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);
             }
@@ -221,7 +221,7 @@ impl BytecodeInterpreter {
                 for part in string_parts {
                     match part {
                         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)
                         }
                         StringPart::Interpolation {
@@ -231,7 +231,7 @@ impl BytecodeInterpreter {
                         } => {
                             self.compile_expression(expr)?;
                             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)
                         }
@@ -335,7 +335,7 @@ impl BytecodeInterpreter {
                 let current_depth = self.current_depth();
                 for parameter in parameters {
                     self.locals[current_depth].push(Local {
-                        identifier: parameter.1.clone(),
+                        identifier: parameter.1.to_string(),
                         depth: current_depth,
                         metadata: LocalMetadata::default(),
                     });
@@ -352,7 +352,7 @@ impl BytecodeInterpreter {
 
                 self.vm.end_function();
 
-                self.functions.insert(name.clone(), false);
+                self.functions.insert(name.to_string(), false);
             }
             Statement::DefineFunction(
                 name,
@@ -371,7 +371,7 @@ impl BytecodeInterpreter {
                 self.vm
                     .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) => {
                 // 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(
                     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) {
                     self.unit_name_to_constant_index
@@ -436,11 +436,7 @@ impl BytecodeInterpreter {
                 let unit_information_idx = self.vm.add_unit_information(
                     unit_name,
                     Some(
-                        &crate::decorator::get_canonical_unit_name(
-                            unit_name.as_str(),
-                            &decorators[..],
-                        )
-                        .name,
+                        &crate::decorator::get_canonical_unit_name(unit_name, &decorators[..]).name,
                     ),
                     UnitMetadata {
                         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 {
                 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;
             }
 
             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];
                 for (idx, entry) in entries.iter().enumerate() {
@@ -81,7 +80,7 @@ impl ColumnFormatter {
                             result += Markup::from(FormattedString(
                                 OutputType::Normal,
                                 format,
-                                (*entry).into(),
+                                entry.to_string().into(),
                             ));
                             result += m::whitespace(" ".repeat(whitespace_length));
                         } 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 {
         // 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);
         }
 

+ 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)]
 pub enum Decorator<'a> {
     MetricPrefixes,
     BinaryPrefixes,
-    Aliases(Vec<(&'a str, Option<AcceptsPrefix>)>),
+    Aliases(Vec<(&'a str, Option<AcceptsPrefix>, Span)>),
     Url(String),
     Name(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,
-    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 {
     for decorator in decorators {
         if let Decorator::Aliases(aliases) = decorator {
-            for (alias, accepts_prefix) in aliases {
+            for (alias, accepts_prefix, _) in aliases {
                 match accepts_prefix {
                     &Some(ap) if ap.short => {
                         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 {
     for decorator in decorates {
         if let Decorator::Aliases(aliases) = decorator {
-            if aliases.iter().any(|(_, prefixes)| prefixes.is_some()) {
+            if aliases.iter().any(|(_, prefixes, _)| prefixes.is_some()) {
                 return true;
             }
         }
@@ -110,3 +149,13 @@ pub fn contains_aliases(decorators: &[Decorator]) -> bool {
 
     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 _use_prelude_output = evaluate_example(&mut example_context, "use prelude");
     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

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

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

+ 71 - 60
numbat/src/lib.rs

@@ -47,6 +47,8 @@ mod unit_registry;
 pub mod value;
 mod vm;
 
+use std::borrow::Cow;
+
 use bytecode_interpreter::BytecodeInterpreter;
 use column_formatter::ColumnFormatter;
 use currency::ExchangeRatesCache;
@@ -168,6 +170,7 @@ impl Context {
             String,
             Option<String>,
             Option<String>,
+            Vec<(String, Option<String>)>,
             CodeSource,
         ),
     > + '_ {
@@ -185,6 +188,7 @@ impl Context {
                         .to_string(),
                     meta.description.clone(),
                     meta.url.clone(),
+                    meta.examples.clone(),
                     self.resolver
                         .get_code_source(signature.definition_span.code_source_id),
                 )
@@ -200,15 +204,6 @@ impl Context {
     }
 
     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();
 
         output += m::emphasized("List of functions:") + m::nl();
@@ -253,11 +248,11 @@ impl Context {
     /// 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
-    pub fn get_completions_for<'a>(
+    pub fn get_completions_for(
         &self,
-        word_part: &'a str,
+        word_part: &str,
         add_paren: bool,
-    ) -> impl Iterator<Item = String> + 'a {
+    ) -> impl Iterator<Item = String> {
         const COMMON_METRIC_PREFIXES: &[&str] = &[
             "pico", "nano", "micro", "milli", "centi", "kilo", "mega", "giga", "tera",
         ];
@@ -270,53 +265,60 @@ impl Context {
             })
             .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 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.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 {
@@ -336,7 +338,8 @@ impl Context {
                 .ok()
                 .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 {
                     help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
                 }
@@ -357,12 +360,13 @@ impl Context {
                     let desc = "Description: ";
                     let mut lines = description.lines();
                     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();
 
                     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() {
                         help += m::nl()
                             + m::value("1 ")
-                            + m::unit(keyword)
+                            + m::unit(keyword.to_string())
                             + m::text(" = ")
                             + m::value(prefix.factor().pretty_print())
                             + m::space()
-                            + m::unit(&full_name);
+                            + m::unit(full_name.to_string());
                     }
 
                     if let Some(BaseUnitAndFactor(prod, num)) = x {
                         help += m::nl()
                             + m::value("1 ")
-                            + m::unit(&full_name)
+                            + m::unit(full_name.to_string())
                             + m::text(" = ")
                             + m::value(num.pretty_print())
                             + m::space()
@@ -407,7 +411,8 @@ impl Context {
                                 Some(m::FormatType::Unit),
                             );
                     } 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) {
             let mut help = m::text("Variable: ");
             if let Some(name) = &l.metadata.name {
-                help += m::text(name);
+                help += m::text(name.clone());
             } else {
-                help += m::identifier(keyword);
+                help += m::identifier(keyword.to_string());
             }
             if let Some(url) = &l.metadata.url {
                 help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
@@ -432,11 +437,14 @@ impl Context {
             if let Some(description) = &l.metadata.description {
                 let desc = "Description: ";
                 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 {
-                    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:    ");
             if let Some(name) = &metadata.name {
-                help += m::text(name);
+                help += m::text(name.to_string());
             } else {
-                help += m::identifier(keyword);
+                help += m::identifier(keyword.to_string());
             }
             if let Some(url) = &metadata.url {
                 help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
@@ -482,11 +490,14 @@ impl Context {
             if let Some(description) = &metadata.description {
                 let desc = "Description: ";
                 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 {
-                    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
             .typechecker
-            .check(transformed_statements)
+            .check(&transformed_statements)
             .map_err(|err| NumbatError::TypeCheckError(*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)]
 pub enum FormatType {
@@ -23,7 +23,7 @@ pub enum OutputType {
 }
 
 #[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)]
 pub struct Markup(pub Vec<FormattedString>);
@@ -43,10 +43,9 @@ impl Display for Markup {
 impl std::ops::Add for 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(
         OutputType::Normal,
         FormatType::Whitespace,
-        " ".to_string(),
+        " ".into(),
     ))
 }
 
@@ -74,99 +73,99 @@ pub fn empty() -> Markup {
     Markup::default()
 }
 
-pub fn whitespace(text: impl AsRef<str>) -> Markup {
+pub fn whitespace(text: impl Into<Cow<'static, str>>) -> Markup {
     Markup::from(FormattedString(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         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(
         OutputType::Normal,
         FormatType::Decorator,
-        text.as_ref().to_string(),
+        text.into(),
     ))
 }
 
@@ -206,6 +205,10 @@ pub struct PlainTextFormatter;
 
 impl Formatter for PlainTextFormatter {
     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.")]
     TrailingEqualSign(String),
 
+    #[error("Trailing '=' sign. Use `fn {0} = …` if you intended to define a function.")]
+    TrailingEqualSignFunction(String),
+
     #[error("Expected identifier after 'let' keyword")]
     ExpectedIdentifierAfterLet,
 
@@ -198,6 +201,9 @@ pub enum ParseErrorKind {
     #[error("Aliases cannot be used on functions.")]
     AliasUsedOnFunction,
 
+    #[error("Example decorators can only be used on functions.")]
+    ExampleUsedOnUnsuitableKind,
+
     #[error("Numerical overflow in dimension exponent")]
     OverflowInDimensionExponent,
 
@@ -310,12 +316,22 @@ impl<'a> Parser<'a> {
                     break;
                 }
                 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 {
-                        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,
                     });
+
                     self.recover_from_error(tokens);
                 }
                 _ => {
@@ -369,14 +385,17 @@ impl<'a> Parser<'a> {
     fn list_of_aliases(
         &mut self,
         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() {
             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() {
-            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() {
@@ -455,6 +474,14 @@ impl<'a> Parser<'a> {
                             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);
                 }
 
@@ -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 {
                         kind: ParseErrorKind::UnknownDecorator,
@@ -768,6 +844,13 @@ impl<'a> Parser<'a> {
 
             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![];
             std::mem::swap(&mut decorators, &mut self.decorator_stack);
 
@@ -1578,7 +1661,7 @@ impl<'a> Parser<'a> {
 
         let format_specifiers = self
             .match_exact(tokens, TokenKind::StringInterpolationSpecifiers)
-            .map(|token| token.lexeme.to_owned());
+            .map(|token| token.lexeme);
 
         parts.push(StringPart::Interpolation {
             span: expr.full_span(),
@@ -1987,9 +2070,12 @@ mod tests {
     use std::fmt::Write;
 
     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]
@@ -2438,7 +2524,26 @@ mod tests {
                 )),
                 decorators: vec![
                     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(
             &["fn double_kef(x) = y where y = x * 2"],
             Statement::DefineFunction {
@@ -3279,7 +3402,7 @@ mod tests {
                     StringPart::Interpolation {
                         span: Span::dummy(),
                         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::sync::OnceLock;
 
 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)]
 struct UnitInfo {
     definition_span: Span,
@@ -63,10 +82,7 @@ struct UnitInfo {
 
 #[derive(Debug, Clone)]
 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>,
 
@@ -76,8 +92,7 @@ pub struct PrefixParser {
 impl PrefixParser {
     pub fn new() -> Self {
         Self {
-            units: HashMap::new(),
-            units_vec: Vec::new(),
+            units: IndexMap::new(),
             other_identifiers: HashMap::new(),
             reserved_identifiers: &["_", "ans"],
         }
@@ -152,23 +167,23 @@ impl PrefixParser {
     fn ensure_name_is_available(
         &self,
         name: &str,
-        conflict_span: Span,
+        definition_span: Span,
         clash_with_other_identifiers: bool,
     ) -> Result<()> {
         if self.reserved_identifiers.contains(&name) {
-            return Err(NameResolutionError::ReservedIdentifier(conflict_span));
+            return Err(NameResolutionError::ReservedIdentifier(definition_span));
         }
 
         if clash_with_other_identifiers {
             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) {
             PrefixParserResult::Identifier(_) => Ok(()),
             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,
         binary: bool,
         full_name: &str,
-        definition_span: Span,
+        AliasSpanInfo {
+            name_span,
+            alias_span,
+        }: AliasSpanInfo,
     ) -> 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() {
             if !(prefix.is_metric() && metric || prefix.is_binary() && binary) {
@@ -192,7 +210,7 @@ impl PrefixParser {
             if accepts_prefix.long {
                 self.ensure_name_is_available(
                     &format!("{prefix_long}{unit_name}"),
-                    definition_span,
+                    alias_span,
                     true,
                 )?;
             }
@@ -200,7 +218,7 @@ impl PrefixParser {
                 for prefix_short in *prefixes_short {
                     self.ensure_name_is_available(
                         &format!("{prefix_short}{unit_name}"),
-                        definition_span,
+                        alias_span,
                         true,
                     )?;
                 }
@@ -208,14 +226,13 @@ impl PrefixParser {
         }
 
         let unit_info = UnitInfo {
-            definition_span,
+            definition_span: name_span,
             accepts_prefix,
             metric_prefixes: metric,
             binary_prefixes: binary,
             full_name: full_name.into(),
         };
         self.units.insert(unit_name.into(), unit_info.clone());
-        self.units_vec.push((unit_name.into(), unit_info));
 
         Ok(())
     }
@@ -233,12 +250,12 @@ impl PrefixParser {
             return PrefixParserResult::UnitIdentifier(
                 info.definition_span,
                 Prefix::none(),
-                input.into(),
+                input.to_string(),
                 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()) {
                 continue;
             }
@@ -294,7 +311,7 @@ mod tests {
                 true,
                 false,
                 "meter",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             .unwrap();
         prefix_parser
@@ -304,7 +321,7 @@ mod tests {
                 true,
                 false,
                 "meter",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             .unwrap();
 
@@ -315,7 +332,7 @@ mod tests {
                 true,
                 true,
                 "byte",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             .unwrap();
         prefix_parser
@@ -325,7 +342,7 @@ mod tests {
                 true,
                 true,
                 "byte",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             .unwrap();
 
@@ -336,7 +353,7 @@ mod tests {
                 false,
                 false,
                 "me",
-                Span::dummy(),
+                AliasSpanInfo::dummy(),
             )
             .unwrap();
 

+ 12 - 6
numbat/src/prefix_transformer.rs

@@ -2,7 +2,7 @@ use crate::{
     ast::{DefineVariable, Expression, Statement, StringPart},
     decorator::{self, Decorator},
     name_resolution::NameResolutionError,
-    prefix_parser::{PrefixParser, PrefixParserResult},
+    prefix_parser::{AliasSpanInfo, PrefixParser, PrefixParserResult},
     span::Span,
 };
 
@@ -135,20 +135,26 @@ impl Transformer {
     pub(crate) fn register_name_and_aliases(
         &mut self,
         name: &str,
+        name_span: Span,
         decorators: &[Decorator],
-        conflict_span: Span,
     ) -> Result<()> {
         let mut unit_names = vec![];
         let metric_prefixes = Self::has_decorator(decorators, Decorator::MetricPrefixes);
         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(
                 alias,
                 accepts_prefix,
                 metric_prefixes,
                 binary_prefixes,
                 name,
-                conflict_span,
+                AliasSpanInfo {
+                    name_span,
+                    alias_span,
+                },
             )?;
             unit_names.push(alias.to_string());
         }
@@ -189,7 +195,7 @@ impl Transformer {
         Ok(match statement {
             Statement::Expression(expr) => Statement::Expression(self.transform_expression(expr)),
             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::DefineDerivedUnit {
@@ -200,7 +206,7 @@ impl Transformer {
                 type_annotation,
                 decorators,
             } => {
-                self.register_name_and_aliases(identifier, &decorators, identifier_span)?;
+                self.register_name_and_aliases(identifier, identifier_span, &decorators)?;
                 Statement::DefineDerivedUnit {
                     identifier_span,
                     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::OutputType::Normal,
                         format_type,
-                        factor.to_string(),
+                        factor.to_string().into(),
                     ))
                     + if i == num_factors - 1 {
                         m::empty()
@@ -145,10 +145,8 @@ impl<Factor: Clone + Ord + Canonicalize, const CANONICALIZE: bool> Product<Facto
         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)]
@@ -245,13 +243,11 @@ impl<Factor: Clone + Ord + Canonicalize + Eq, const CANONICALIZE: bool> Eq
 }
 
 impl<Factor, const CANONICALIZE: bool> IntoIterator for Product<Factor, CANONICALIZE> {
-    type IntoIter = ProductIntoIter<Factor>;
+    type IntoIter = <Vec<Factor> as IntoIterator>::IntoIter;
     type Item = Factor;
 
     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)]
 mod tests {
     use super::*;

+ 2 - 1
numbat/src/quantity.rs

@@ -192,7 +192,8 @@ impl Quantity {
             let group_representative = group_as_unit
                 .iter()
                 .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()))
                         .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;
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct BaseIndex(isize);
-
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
 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>,
     text_code_source_count: usize,
     internal_code_source_count: usize,
-    imported_modules: Vec<ModulePath>,
+    pub imported_modules: Vec<ModulePath>,
     codesources: HashMap<usize, CodeSource>,
 }
 

+ 22 - 14
numbat/src/tokenizer.rs

@@ -22,10 +22,7 @@ pub enum TokenizerErrorKind {
     ExpectedDigit { character: Option<char> },
 
     #[error("Expected base-{base} digit")]
-    ExpectedDigitInBase {
-        base: usize,
-        character: Option<char>,
-    },
+    ExpectedDigitInBase { base: u8, character: Option<char> },
 
     #[error("Unterminated string")]
     UnterminatedString,
@@ -125,7 +122,7 @@ pub enum TokenKind {
 
     // Variable-length tokens
     Number,
-    IntegerWithBase(usize),
+    IntegerWithBase(u8),
     Identifier,
 
     // 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 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();
         let keywords = KEYWORDS.get_or_init(|| {
             let mut m = HashMap::new();
@@ -463,18 +472,17 @@ impl Tokenizer {
                 .map(|c| c == 'x' || c == 'o' || c == 'b')
                 .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
 
-                // 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(
                         self.current,
                         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)) {
         match self {
             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)) {
         f(self);
         match self {

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

@@ -160,6 +160,7 @@ pub enum TrivialResolution {
 }
 
 impl TrivialResolution {
+    #[allow(clippy::wrong_self_convention)]
     pub fn is_trivially_violated(self) -> bool {
         matches!(self, TrivialResolution::Violated)
     }
@@ -296,10 +297,7 @@ impl Constraint {
             Constraint::EqualScalar(dtype) => match dtype.split_first_factor() {
                 Some(((DTypeFactor::TVar(tv), k), rest)) => {
                     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(
                         tv.clone(),

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

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

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

@@ -65,16 +65,16 @@ pub struct TypeChecker {
     constraints: ConstraintSet,
 }
 
-struct ElaborationDefinitionArgs<'a> {
+struct ElaborationDefinitionArgs<'a, 'b> {
     identifier_span: Span,
-    expr: &'a ast::Expression<'a>,
+    expr: &'b ast::Expression<'a>,
     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,
     actual_name: &'static str,
     actual_name_for_fix: &'static str,
-    elaboration_kind: &'a str,
+    elaboration_kind: &'b str,
 }
 
 impl TypeChecker {
@@ -118,14 +118,15 @@ impl TypeChecker {
                     }
                 }
 
-                let mut dtype: DType = self
+                let mut factors = self
                     .registry
                     .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
-                for (factor, _) in dtype.factors.iter_mut() {
+                for (factor, _) in &mut factors {
                     *factor = match factor {
                         DTypeFactor::BaseDimension(ref n)
                             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::String(_) => Ok(Type::String),
@@ -172,28 +173,28 @@ impl TypeChecker {
         })?)
     }
 
-    fn get_proper_function_reference(
+    fn get_proper_function_reference<'a>(
         &self,
-        expr: &ast::Expression,
-    ) -> Option<(String, &FunctionSignature)> {
+        expr: &ast::Expression<'a>,
+    ) -> Option<(&'a str, &FunctionSignature)> {
         match expr {
             ast::Expression::Identifier(_, name) => self
                 .env
                 .get_function_info(name)
-                .map(|(signature, _)| (name.to_string(), signature)),
+                .map(|(signature, _)| (*name, signature)),
             _ => None,
         }
     }
 
-    fn proper_function_call(
+    fn proper_function_call<'a>(
         &mut self,
         span: &Span,
         full_span: &Span,
-        function_name: &str,
+        function_name: &'a str,
         signature: &FunctionSignature,
-        arguments: Vec<typed_ast::Expression>,
+        arguments: Vec<typed_ast::Expression<'a>>,
         argument_types: Vec<Type>,
-    ) -> Result<typed_ast::Expression> {
+    ) -> Result<typed_ast::Expression<'a>> {
         let FunctionSignature {
             name: _,
             definition_span,
@@ -288,13 +289,16 @@ impl TypeChecker {
         Ok(typed_ast::Expression::FunctionCall(
             *span,
             *full_span,
-            function_name.into(),
+            function_name,
             arguments,
             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 {
             ast::Expression::Scalar(span, n)
                 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) => {
                 let type_scheme = self.identifier_type(*span, name)?.clone();
@@ -780,7 +784,7 @@ impl TypeChecker {
                     self.proper_function_call(
                         span,
                         full_span,
-                        &name,
+                        name,
                         &signature,
                         arguments_checked,
                         argument_types,
@@ -880,7 +884,7 @@ impl TypeChecker {
                             format_specifiers,
                         } => Ok(typed_ast::StringPart::Interpolation {
                             span: *span,
-                            format_specifiers: format_specifiers.clone(),
+                            format_specifiers: format_specifiers.as_ref().copied(),
                             expr: Box::new(self.elaborate_expression(expr)?),
                         }),
                     })
@@ -933,7 +937,7 @@ impl TypeChecker {
                 let name = *name;
                 let fields_checked = fields
                     .iter()
-                    .map(|(_, n, v)| Ok((n.to_string(), self.elaborate_expression(v)?)))
+                    .map(|(_, n, v)| Ok((*n, self.elaborate_expression(v)?)))
                     .collect::<Result<Vec<_>>>()?;
 
                 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 {
                         return Err(Box::new(TypeCheckError::UnknownFieldInStructInstantiation(
                             *span,
                             struct_info.definition_span,
-                            field.clone(),
+                            field.to_string(),
                             struct_info.name.clone(),
                         )));
                     };
@@ -986,7 +990,7 @@ impl TypeChecker {
 
                 let missing_fields = {
                     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()
                 };
 
@@ -1050,7 +1054,7 @@ impl TypeChecker {
                     *ident_span,
                     *full_span,
                     Box::new(expr_checked),
-                    field_name.to_owned(),
+                    field_name,
                     TypeScheme::concrete(type_),
                     TypeScheme::concrete(field_type),
                 )
@@ -1105,10 +1109,10 @@ impl TypeChecker {
         })
     }
 
-    fn _elaborate_inner(
+    fn _elaborate_inner<'a>(
         &mut self,
-        definition: ElaborationDefinitionArgs,
-    ) -> Result<(typed_ast::Expression, typed_ast::Type)> {
+        definition: ElaborationDefinitionArgs<'a, '_>,
+    ) -> Result<(typed_ast::Expression<'a>, typed_ast::Type)> {
         let ElaborationDefinitionArgs {
             identifier_span,
             expr,
@@ -1219,8 +1223,8 @@ impl TypeChecker {
         }
 
         Ok(typed_ast::DefineVariable(
-            identifier.to_string(),
-            decorators.to_owned(),
+            identifier,
+            decorators.clone(),
             expr_checked,
             type_annotation.clone(),
             TypeScheme::concrete(type_deduced),
@@ -1284,7 +1288,7 @@ impl TypeChecker {
                 }
 
                 typed_ast::Statement::DefineBaseUnit(
-                    unit_name.to_string(),
+                    unit_name,
                     decorators.clone(),
                     type_annotation.clone().map(TypeAnnotation::TypeExpression),
                     TypeScheme::concrete(Type::Dimension(type_specified)),
@@ -1320,7 +1324,7 @@ impl TypeChecker {
                     );
                 }
                 typed_ast::Statement::DefineDerivedUnit(
-                    identifier.to_string(),
+                    identifier,
                     expr_checked,
                     decorators.clone(),
                     type_annotation.clone(),
@@ -1424,7 +1428,7 @@ impl TypeChecker {
                     );
                     typed_parameters.push((
                         *parameter_span,
-                        parameter.to_string(),
+                        *parameter,
                         parameter_type,
                         type_annotation,
                     ));
@@ -1444,7 +1448,7 @@ impl TypeChecker {
 
                 let parameters: Vec<_> = typed_parameters
                     .iter()
-                    .map(|(span, name, _, annotation)| (*span, name.clone(), (*annotation).clone()))
+                    .map(|(span, name, _, annotation)| (*span, name, (*annotation).clone()))
                     .collect();
                 let parameter_types = typed_parameters
                     .iter()
@@ -1463,7 +1467,10 @@ impl TypeChecker {
                             .iter()
                             .map(|(span, name, tpb)| (*span, name.to_string(), tpb.clone()).clone())
                             .collect(),
-                        parameters,
+                        parameters: parameters
+                            .into_iter()
+                            .map(|(span, s, o)| (span, s.to_string(), o))
+                            .collect(),
                         return_type_annotation: return_type_annotation.clone(),
                         fn_type: fn_type.clone(),
                     },
@@ -1471,6 +1478,7 @@ impl TypeChecker {
                         name: crate::decorator::name(decorators).map(ToOwned::to_owned),
                         url: crate::decorator::url(decorators).map(ToOwned::to_owned),
                         description: crate::decorator::description(decorators),
+                        examples: crate::decorator::examples(decorators),
                     },
                 );
 
@@ -1579,18 +1587,18 @@ impl TypeChecker {
                 );
 
                 typed_ast::Statement::DefineFunction(
-                    function_name.to_string(),
+                    function_name,
                     decorators.clone(),
                     type_parameters
                         .iter()
-                        .map(|(_, name, bound)| (name.to_string(), bound.clone()))
+                        .map(|(_, name, bound)| (*name, bound.clone()))
                         .collect(),
                     typed_parameters
                         .iter()
                         .map(|(span, name, _, type_annotation)| {
                             (
                                 *span,
-                                name.clone(),
+                                *name,
                                 (*type_annotation).clone(),
                                 crate::markup::empty(),
                             )
@@ -1640,7 +1648,7 @@ impl TypeChecker {
                         .add_base_dimension(name)
                         .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) => {
                 if args.len() != 1 {
@@ -1894,12 +1902,12 @@ impl TypeChecker {
 
     pub fn check<'a>(
         &mut self,
-        statements: impl IntoIterator<Item = ast::Statement<'a>>,
+        statements: &[ast::Statement<'a>],
     ) -> Result<Vec<typed_ast::Statement<'a>>> {
         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)

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

@@ -92,7 +92,7 @@ impl ApplySubstitution for Type {
 impl ApplySubstitution for DType {
     fn apply(&mut self, substitution: &Substitution) -> Result<(), SubstitutionError> {
         let mut new_dtype = self.clone();
-        for (f, power) in &self.factors {
+        for (f, power) in self.factors() {
             match f {
                 DTypeFactor::TVar(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> {
         match self {
             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()))?;
 
     TypeChecker::default()
-        .check(transformed_statements)
+        .check(&transformed_statements)
         .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 {
                 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) {
                     markup += m::operator(":");
@@ -219,7 +219,7 @@ impl PrettyPrint for TypeScheme {
                 for type_parameter in &type_parameters {
                     markup += m::keyword("forall");
                     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) {
                         markup += m::operator(":");

+ 116 - 89
numbat/src/typed_ast.rs

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

+ 2 - 2
numbat/src/unicode_input.rs

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

+ 19 - 27
numbat/src/unit.rs

@@ -269,33 +269,25 @@ impl Unit {
     }
 
     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)
     }

+ 2 - 2
numbat/src/value.rs

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