Browse Source

Updates for Rust 2024

David Peter 2 weeks ago
parent
commit
34170f8429
59 changed files with 515 additions and 493 deletions
  1. 0 2
      .github/workflows/ci.yml
  2. 2 2
      numbat-cli/Cargo.toml
  3. 9 11
      numbat-cli/src/completer.rs
  4. 2 2
      numbat-cli/src/highlighter.rs
  5. 6 4
      numbat-cli/src/main.rs
  6. 3 1
      numbat-cli/tests/integration.rs
  7. 2 2
      numbat-exchange-rates/Cargo.toml
  8. 2 2
      numbat-wasm/Cargo.toml
  9. 0 10
      numbat-wasm/deploy.sh
  10. 2 2
      numbat/Cargo.toml
  11. 2 2
      numbat/benches/prelude.rs
  12. 5 3
      numbat/examples/inspect.rs
  13. 1 1
      numbat/examples/unit_graph.rs
  14. 1 1
      numbat/src/arithmetic.rs
  15. 4 10
      numbat/src/ast.rs
  16. 1 1
      numbat/src/bytecode_interpreter.rs
  17. 1 1
      numbat/src/command.rs
  18. 10 11
      numbat/src/datetime.rs
  19. 4 4
      numbat/src/decorator.rs
  20. 126 98
      numbat/src/diagnostic.rs
  21. 1 1
      numbat/src/dimension.rs
  22. 1 1
      numbat/src/ffi/currency.rs
  23. 3 3
      numbat/src/ffi/datetime.rs
  24. 1 1
      numbat/src/ffi/functions.rs
  25. 1 1
      numbat/src/ffi/lookup.rs
  26. 1 1
      numbat/src/ffi/math.rs
  27. 1 1
      numbat/src/ffi/procedures.rs
  28. 1 1
      numbat/src/ffi/strings.rs
  29. 1 1
      numbat/src/gamma.rs
  30. 10 18
      numbat/src/help.rs
  31. 1 1
      numbat/src/html_formatter.rs
  32. 6 2
      numbat/src/interpreter/assert_eq.rs
  33. 3 1
      numbat/src/interpreter/mod.rs
  34. 121 128
      numbat/src/lib.rs
  35. 15 14
      numbat/src/module_importer.rs
  36. 1 1
      numbat/src/number.rs
  37. 19 10
      numbat/src/parser.rs
  38. 1 1
      numbat/src/plot.rs
  39. 1 1
      numbat/src/prefix.rs
  40. 4 4
      numbat/src/prefix_parser.rs
  41. 8 8
      numbat/src/quantity.rs
  42. 1 1
      numbat/src/registry.rs
  43. 1 1
      numbat/src/resolver.rs
  44. 4 3
      numbat/src/session_history.rs
  45. 3 3
      numbat/src/tokenizer.rs
  46. 4 4
      numbat/src/typechecker/const_evaluation.rs
  47. 8 10
      numbat/src/typechecker/constraints.rs
  48. 1 1
      numbat/src/typechecker/environment.rs
  49. 16 6
      numbat/src/typechecker/error.rs
  50. 2 2
      numbat/src/typechecker/incompatible_dimensions.rs
  51. 56 64
      numbat/src/typechecker/mod.rs
  52. 1 1
      numbat/src/typechecker/qualified_type.rs
  53. 1 1
      numbat/src/typechecker/substitutions.rs
  54. 2 2
      numbat/src/typechecker/tests/mod.rs
  55. 8 8
      numbat/src/typed_ast.rs
  56. 1 1
      numbat/src/unit.rs
  57. 9 6
      numbat/src/vm.rs
  58. 1 1
      numbat/tests/common.rs
  59. 12 8
      numbat/tests/interpreter.rs

+ 0 - 2
.github/workflows/ci.yml

@@ -67,8 +67,6 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - uses: dtolnay/rust-toolchain@stable
-      with:
-        toolchain: "1.76.0"
     - uses: actions/checkout@v3
     - name: Install wasm-pack
       run: |

+ 2 - 2
numbat-cli/Cargo.toml

@@ -7,10 +7,10 @@ keywords = ["language", "compiler", "physics", "units", "calculation"]
 homepage = "https://numbat.dev/"
 repository = "https://github.com/sharkdp/numbat"
 version = "1.16.0"
-edition = "2021"
+edition = "2024"
 license = "MIT OR Apache-2.0"
 readme = "../README.md"
-rust-version = "1.76"
+rust-version = "1.88"
 
 [dependencies]
 anyhow = "1"

+ 9 - 11
numbat-cli/src/completer.rs

@@ -1,7 +1,7 @@
 use std::sync::{Arc, Mutex};
 
-use numbat::{compact_str::CompactString, unicode_input::UNICODE_INPUT, Context};
-use rustyline::completion::{extract_word, Completer, Pair};
+use numbat::{Context, compact_str::CompactString, unicode_input::UNICODE_INPUT};
+use rustyline::completion::{Completer, Pair, extract_word};
 
 pub struct NumbatCompleter {
     pub context: Arc<Mutex<Context>>,
@@ -73,10 +73,11 @@ impl Completer for NumbatCompleter {
 
         // does it look like we're tab-completing a timezone?
         let complete_tz = line.find("tz(").and_then(|convert_pos| {
-            if let Some(quote_pos) = line.rfind('"') {
-                if quote_pos > convert_pos && pos > quote_pos {
-                    return Some(quote_pos + 1);
-                }
+            if let Some(quote_pos) = line.rfind('"')
+                && quote_pos > convert_pos
+                && pos > quote_pos
+            {
+                return Some(quote_pos + 1);
             }
             None
         });
@@ -120,11 +121,8 @@ impl Completer for NumbatCompleter {
             .iter()
             .any(|&s| line[..pos].contains(s));
 
-        let candidates = self
-            .context
-            .lock()
-            .unwrap()
-            .get_completions_for(word_part, add_paren);
+        let binding = self.context.lock().unwrap();
+        let candidates = binding.get_completions_for(word_part, add_paren);
 
         Ok((
             pos_word,

+ 2 - 2
numbat-cli/src/highlighter.rs

@@ -1,8 +1,8 @@
 use colored::Colorize;
 use numbat::compact_str::ToCompactString;
 use numbat::keywords::KEYWORDS;
-use numbat::{markup, Context};
-use rustyline::{highlight::Highlighter, CompletionType};
+use numbat::{Context, markup};
+use rustyline::{CompletionType, highlight::Highlighter};
 
 use std::{
     borrow::Cow,

+ 6 - 4
numbat-cli/src/main.rs

@@ -18,15 +18,15 @@ use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystem
 use numbat::pretty_print::PrettyPrint;
 use numbat::resolver::CodeSource;
 use numbat::session_history::{ParseEvaluationResult, SessionHistory};
-use numbat::{markup as m, RuntimeErrorKind};
 use numbat::{Context, NumbatError};
 use numbat::{InterpreterSettings, NameResolutionError};
+use numbat::{RuntimeErrorKind, markup as m};
 
-use anyhow::{bail, Context as AnyhowContext, Result};
+use anyhow::{Context as AnyhowContext, Result, bail};
 use clap::Parser;
 use rustyline::config::Configurer;
 use rustyline::{
-    error::ReadlineError, history::DefaultHistory, Completer, Editor, Helper, Hinter, Validator,
+    Completer, Editor, Helper, Hinter, Validator, error::ReadlineError, history::DefaultHistory,
 };
 use rustyline::{EventHandler, Highlighter, KeyCode, KeyEvent, Modifiers};
 
@@ -642,7 +642,9 @@ fn generate_config() -> Result<()> {
         "A default configuration has been written to '{}'.",
         config_file_path.to_string_lossy()
     );
-    println!("Open the file in a text editor. Modify whatever you want to change and remove the other fields");
+    println!(
+        "Open the file in a text editor. Modify whatever you want to change and remove the other fields"
+    );
 
     Ok(())
 }

+ 3 - 1
numbat-cli/tests/integration.rs

@@ -9,7 +9,9 @@ fn numbat() -> Command {
         .unwrap()
         .join("numbat")
         .join("modules");
-    std::env::set_var("NUMBAT_MODULES_PATH", module_path);
+    unsafe {
+        std::env::set_var("NUMBAT_MODULES_PATH", module_path);
+    }
 
     let mut cmd = Command::cargo_bin("numbat").unwrap();
     cmd.arg("--no-init");

+ 2 - 2
numbat-exchange-rates/Cargo.toml

@@ -5,9 +5,9 @@ authors = ["David Peter <[email protected]>"]
 homepage = "https://numbat.dev/"
 repository = "https://github.com/sharkdp/numbat"
 version = "0.5.0"
-edition = "2021"
+edition = "2024"
 license = "MIT OR Apache-2.0"
-rust-version = "1.76"
+rust-version = "1.88"
 
 [dependencies]
 attohttpc = { version = "0.27.0", default-features = false, features = [

+ 2 - 2
numbat-wasm/Cargo.toml

@@ -2,8 +2,8 @@
 name = "numbat-wasm"
 authors = ["David Peter <[email protected]>"]
 version = "0.1.0"
-edition = "2021"
-rust-version = "1.76"
+edition = "2024"
+rust-version = "1.88"
 
 [lib]
 crate-type = ["cdylib", "rlib"]

+ 0 - 10
numbat-wasm/deploy.sh

@@ -2,16 +2,6 @@
 
 set -euo pipefail
 
-# Make sure that Rust version is == 1.76.0 for now to avoid running into
-# https://github.com/rustwasm/wasm-pack/issues/1389
-# With newer versions, we get panics when (for example) running:
-# https://numbat.dev/doc/example-paper_size.html
-if ! rustc --version | grep -q "1.76.0"; then
-    echo "Please switch to Rust version 1.76.0."
-    echo "(rustup default 1.76.0)"
-    exit 1
-fi
-
 current_branch=$(git rev-parse --abbrev-ref HEAD)
 
 if [[ "$current_branch" != "master" ]]; then

+ 2 - 2
numbat/Cargo.toml

@@ -7,10 +7,10 @@ keywords = ["language", "compiler", "physics", "units", "calculation"]
 homepage = "https://numbat.dev/"
 repository = "https://github.com/sharkdp/numbat"
 version = "1.16.0"
-edition = "2021"
+edition = "2024"
 license = "MIT OR Apache-2.0"
 readme = "README.md"
-rust-version = "1.76"
+rust-version = "1.88"
 
 [dependencies]
 thiserror = "2"

+ 2 - 2
numbat/benches/prelude.rs

@@ -1,7 +1,7 @@
-use criterion::{criterion_group, criterion_main, Criterion};
+use criterion::{Criterion, criterion_group, criterion_main};
+use numbat::Context;
 use numbat::module_importer::BuiltinModuleImporter;
 use numbat::resolver::CodeSource;
-use numbat::Context;
 
 fn import_prelude(c: &mut Criterion) {
     let importer = BuiltinModuleImporter::default();

+ 5 - 3
numbat/examples/inspect.rs

@@ -1,4 +1,4 @@
-use compact_str::{format_compact, CompactString};
+use compact_str::{CompactString, format_compact};
 use itertools::Itertools;
 use numbat::markup::plain_text_format;
 use numbat::module_importer::FileSystemImporter;
@@ -139,8 +139,10 @@ fn inspect_functions_in_module(ctx: &Context, prelude_ctx: &Context, module: Str
 
                     print!("<pre>");
                     print!("<div class=\"buttons\">");
-                    print!("<button class=\"fa fa-play play-button\" title=\"Run this code\" aria-label=\"Run this code\"  onclick=\" window.open('{}')\"\"></button>",
-                        example_url);
+                    print!(
+                        "<button class=\"fa fa-play play-button\" title=\"Run this code\" aria-label=\"Run this code\"  onclick=\" window.open('{}')\"\"></button>",
+                        example_url
+                    );
                     print!("</div>");
                     print!("<code class=\"language-nbt hljs numbat\">");
                     for l in example_input.lines() {

+ 1 - 1
numbat/examples/unit_graph.rs

@@ -6,7 +6,7 @@
 
 use itertools::Itertools;
 use numbat::{
-    module_importer::FileSystemImporter, resolver::CodeSource, BaseRepresentationFactor, Context,
+    BaseRepresentationFactor, Context, module_importer::FileSystemImporter, resolver::CodeSource,
 };
 
 fn main() {

+ 1 - 1
numbat/src/arithmetic.rs

@@ -1,4 +1,4 @@
-use compact_str::{format_compact, CompactString};
+use compact_str::{CompactString, format_compact};
 use num_rational::Ratio;
 
 pub type Rational = Ratio<i128>;

+ 4 - 10
numbat/src/ast.rs

@@ -7,7 +7,7 @@ use crate::{
     arithmetic::Exponent, decorator::Decorator, markup::Markup, number::Number, prefix::Prefix,
     pretty_print::PrettyPrint,
 };
-use compact_str::{format_compact, CompactString, ToCompactString};
+use compact_str::{CompactString, ToCompactString, format_compact};
 use itertools::Itertools;
 use num_traits::Signed;
 
@@ -149,23 +149,17 @@ impl Expression<'_> {
 
 #[cfg(test)]
 macro_rules! scalar {
-    ( $num:expr ) => {{
-        crate::ast::Expression::Scalar(Span::dummy(), Number::from_f64($num))
-    }};
+    ( $num:expr ) => {{ crate::ast::Expression::Scalar(Span::dummy(), Number::from_f64($num)) }};
 }
 
 #[cfg(test)]
 macro_rules! identifier {
-    ( $name:expr ) => {{
-        crate::ast::Expression::Identifier(Span::dummy(), $name.into())
-    }};
+    ( $name:expr ) => {{ crate::ast::Expression::Identifier(Span::dummy(), $name.into()) }};
 }
 
 #[cfg(test)]
 macro_rules! boolean {
-    ( $name:expr ) => {{
-        crate::ast::Expression::Boolean(Span::dummy(), $name.into())
-    }};
+    ( $name:expr ) => {{ crate::ast::Expression::Boolean(Span::dummy(), $name.into()) }};
 }
 
 #[cfg(test)]

+ 1 - 1
numbat/src/bytecode_interpreter.rs

@@ -20,7 +20,7 @@ use crate::unit::{CanonicalName, Unit};
 use crate::unit_registry::{UnitMetadata, UnitRegistry};
 use crate::value::{FunctionReference, Value};
 use crate::vm::{Constant, ExecutionContext, Op, Vm};
-use crate::{decorator, Type};
+use crate::{Type, decorator};
 
 #[derive(Debug, Clone, Default)]
 pub struct LocalMetadata {

+ 1 - 1
numbat/src/command.rs

@@ -3,6 +3,7 @@ use std::str::{FromStr, SplitWhitespace};
 use compact_str::ToCompactString;
 
 use crate::{
+    Context, ParseError, RuntimeError,
     diagnostic::{ErrorDiagnostic, ResolverDiagnostic},
     help::help_markup,
     interpreter::RuntimeErrorKind,
@@ -11,7 +12,6 @@ use crate::{
     resolver::CodeSource,
     session_history::{SessionHistory, SessionHistoryOptions},
     span::{ByteIndex, Span},
-    Context, ParseError, RuntimeError,
 };
 
 #[derive(Debug, Clone, PartialEq)]

+ 10 - 11
numbat/src/datetime.rs

@@ -1,5 +1,5 @@
 use compact_str::{CompactString, ToCompactString};
-use jiff::{civil::DateTime, fmt::rfc2822, tz::TimeZone, Timestamp, Zoned};
+use jiff::{Timestamp, Zoned, civil::DateTime, fmt::rfc2822, tz::TimeZone};
 use std::str::FromStr;
 
 pub fn get_local_timezone_or_utc() -> TimeZone {
@@ -12,10 +12,10 @@ pub fn parse_datetime(input: &str) -> Result<Zoned, jiff::Error> {
     }
 
     // RFC 3339
-    if let Ok(timestamp) = DateTime::strptime("%Y-%m-%dT%H:%M:%S%.fZ", input) {
-        if let zoned @ Ok(_) = timestamp.to_zoned(TimeZone::UTC) {
-            return zoned;
-        }
+    if let Ok(timestamp) = DateTime::strptime("%Y-%m-%dT%H:%M:%S%.fZ", input)
+        && let zoned @ Ok(_) = timestamp.to_zoned(TimeZone::UTC)
+    {
+        return zoned;
     }
 
     // RFC 2822
@@ -52,12 +52,11 @@ pub fn parse_datetime(input: &str) -> Result<Zoned, jiff::Error> {
         // Get the last space-separated word in the input string, and try to parse it
         // as a timezone specifier, then try to match the rest of the string with the
         // given format.
-        if let Some((rest, potential_timezone_name)) = input.rsplit_once(' ') {
-            if let Ok(tz) = TimeZone::get(potential_timezone_name) {
-                if let Ok(datetime) = DateTime::strptime(format, rest) {
-                    return datetime.to_zoned(tz);
-                }
-            }
+        if let Some((rest, potential_timezone_name)) = input.rsplit_once(' ')
+            && let Ok(tz) = TimeZone::get(potential_timezone_name)
+            && let Ok(datetime) = DateTime::strptime(format, rest)
+        {
+            return datetime.to_zoned(tz);
         }
 
         // Without timezone/offset

+ 4 - 4
numbat/src/decorator.rs

@@ -132,10 +132,10 @@ pub fn examples(decorators: &[Decorator]) -> Vec<(CompactString, Option<CompactS
 
 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()) {
-                return true;
-            }
+        if let Decorator::Aliases(aliases) = decorator
+            && aliases.iter().any(|(_, prefixes, _)| prefixes.is_some())
+        {
+            return true;
         }
     }
 

+ 126 - 98
numbat/src/diagnostic.rs

@@ -1,13 +1,13 @@
 use codespan_reporting::diagnostic::LabelStyle;
 
 use crate::{
+    NameResolutionError,
     interpreter::{RuntimeError, RuntimeErrorKind},
     parser::ParseError,
     pretty_print::PrettyPrint,
     resolver::{Resolver, ResolverError},
     span::Span,
     typechecker::{IncompatibleDimensionsError, TypeCheckError},
-    NameResolutionError,
 };
 
 pub type Diagnostic = codespan_reporting::diagnostic::Diagnostic<usize>;
@@ -18,23 +18,29 @@ pub trait ErrorDiagnostic {
 
 impl ErrorDiagnostic for ParseError {
     fn diagnostics(&self) -> Vec<Diagnostic> {
-        vec![Diagnostic::error()
-            .with_message("while parsing")
-            .with_labels(vec![self
-                .span
-                .diagnostic_label(LabelStyle::Primary)
-                .with_message(self.kind.to_string())])]
+        vec![
+            Diagnostic::error()
+                .with_message("while parsing")
+                .with_labels(vec![
+                    self.span
+                        .diagnostic_label(LabelStyle::Primary)
+                        .with_message(self.kind.to_string()),
+                ]),
+        ]
     }
 }
 
 impl ErrorDiagnostic for ResolverError {
     fn diagnostics(&self) -> Vec<Diagnostic> {
         match self {
-            ResolverError::UnknownModule(span, _) => vec![Diagnostic::error()
-                .with_message("while resolving imports in")
-                .with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message("Unknown module")])],
+            ResolverError::UnknownModule(span, _) => vec![
+                Diagnostic::error()
+                    .with_message("while resolving imports in")
+                    .with_labels(vec![
+                        span.diagnostic_label(LabelStyle::Primary)
+                            .with_message("Unknown module"),
+                    ]),
+            ],
             ResolverError::ParseErrors(errors) => {
                 errors.iter().flat_map(|e| e.diagnostics()).collect()
             }
@@ -50,25 +56,30 @@ impl ErrorDiagnostic for NameResolutionError {
                 original_item_type,
                 conflict_span,
                 original_span,
-            } => vec![Diagnostic::error()
-                .with_message("identifier clash in definition")
-                .with_labels(vec![
-                    original_span
-                        .diagnostic_label(LabelStyle::Secondary)
-                        .with_message(if let Some(t) = original_item_type.as_ref() {
-                            format!("Previously defined {t} here")
-                        } else {
-                            "Previously defined here".to_owned()
-                        }),
-                    conflict_span
-                        .diagnostic_label(LabelStyle::Primary)
-                        .with_message("identifier is already in use"),
-                ])],
-            NameResolutionError::ReservedIdentifier(span) => vec![Diagnostic::error()
-                .with_message("reserved identifier may not be used")
-                .with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message("reserved identifier")])],
+            } => vec![
+                Diagnostic::error()
+                    .with_message("identifier clash in definition")
+                    .with_labels(vec![
+                        original_span
+                            .diagnostic_label(LabelStyle::Secondary)
+                            .with_message(if let Some(t) = original_item_type.as_ref() {
+                                format!("Previously defined {t} here")
+                            } else {
+                                "Previously defined here".to_owned()
+                            }),
+                        conflict_span
+                            .diagnostic_label(LabelStyle::Primary)
+                            .with_message("identifier is already in use"),
+                    ]),
+            ],
+            NameResolutionError::ReservedIdentifier(span) => vec![
+                Diagnostic::error()
+                    .with_message("reserved identifier may not be used")
+                    .with_labels(vec![
+                        span.diagnostic_label(LabelStyle::Primary)
+                            .with_message("reserved identifier"),
+                    ]),
+            ],
         }
     }
 }
@@ -85,10 +96,11 @@ impl ErrorDiagnostic for TypeCheckError {
                 } else {
                     vec![]
                 };
-                d.with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message("unknown identifier")])
-                    .with_notes(notes)
+                d.with_labels(vec![
+                    span.diagnostic_label(LabelStyle::Primary)
+                        .with_message("unknown identifier"),
+                ])
+                .with_notes(notes)
             }
             TypeCheckError::IncompatibleDimensions(IncompatibleDimensionsError {
                 operation,
@@ -127,16 +139,19 @@ impl ErrorDiagnostic for TypeCheckError {
             }
             TypeCheckError::NonScalarExponent(span, type_)
             | TypeCheckError::NonScalarFactorialArgument(span, type_) => d
-                .with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message(format!("{type_}"))])
+                .with_labels(vec![
+                    span.diagnostic_label(LabelStyle::Primary)
+                        .with_message(format!("{type_}")),
+                ])
                 .with_notes(vec![inner_error]),
-            TypeCheckError::UnsupportedConstEvalExpression(span, _) => d.with_labels(vec![span
-                .diagnostic_label(LabelStyle::Primary)
-                .with_message(inner_error)]),
-            TypeCheckError::DivisionByZeroInConstEvalExpression(span) => d.with_labels(vec![span
-                .diagnostic_label(LabelStyle::Primary)
-                .with_message(inner_error)]),
+            TypeCheckError::UnsupportedConstEvalExpression(span, _) => d.with_labels(vec![
+                span.diagnostic_label(LabelStyle::Primary)
+                    .with_message(inner_error),
+            ]),
+            TypeCheckError::DivisionByZeroInConstEvalExpression(span) => d.with_labels(vec![
+                span.diagnostic_label(LabelStyle::Primary)
+                    .with_message(inner_error),
+            ]),
             TypeCheckError::RegistryError(re) => match re {
                 crate::registry::RegistryError::EntryExists(_) => d.with_notes(vec![inner_error]),
                 crate::registry::RegistryError::UnknownEntry(name, suggestion) => {
@@ -173,26 +188,28 @@ impl ErrorDiagnostic for TypeCheckError {
                 arity,
                 num_args,
             } => {
-                let mut labels = vec![callable_span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message(format!(
-                        "{what}was called with {num}, but takes {range}",
-                        what = if callable_definition_span.is_some() {
-                            ""
-                        } else {
-                            "procedure or function object "
-                        },
-                        num = if *num_args == 1 {
-                            "one argument".into()
-                        } else {
-                            format!("{num_args} arguments")
-                        },
-                        range = if arity.start() == arity.end() {
-                            format!("{}", arity.start())
-                        } else {
-                            format!("{} to {}", arity.start(), arity.end())
-                        }
-                    ))];
+                let mut labels = vec![
+                    callable_span
+                        .diagnostic_label(LabelStyle::Primary)
+                        .with_message(format!(
+                            "{what}was called with {num}, but takes {range}",
+                            what = if callable_definition_span.is_some() {
+                                ""
+                            } else {
+                                "procedure or function object "
+                            },
+                            num = if *num_args == 1 {
+                                "one argument".into()
+                            } else {
+                                format!("{num_args} arguments")
+                            },
+                            range = if arity.start() == arity.end() {
+                                format!("{}", arity.start())
+                            } else {
+                                format!("{} to {}", arity.start(), arity.end())
+                            }
+                        )),
+                ];
                 if let Some(span) = callable_definition_span {
                     labels.insert(
                         0,
@@ -203,9 +220,10 @@ impl ErrorDiagnostic for TypeCheckError {
 
                 d.with_labels(labels)
             }
-            TypeCheckError::TypeParameterNameClash(span, _) => d.with_labels(vec![span
-                .diagnostic_label(LabelStyle::Primary)
-                .with_message(inner_error)]),
+            TypeCheckError::TypeParameterNameClash(span, _) => d.with_labels(vec![
+                span.diagnostic_label(LabelStyle::Primary)
+                    .with_message(inner_error),
+            ]),
             TypeCheckError::IncompatibleTypesInCondition(
                 if_span,
                 then_type,
@@ -301,10 +319,12 @@ impl ErrorDiagnostic for TypeCheckError {
                     ])
                     .with_notes(vec![inner_error])
                 } else {
-                    d.with_labels(vec![argument_span
-                        .diagnostic_label(LabelStyle::Primary)
-                        .with_message(argument_type.to_string())])
-                        .with_notes(vec![inner_error])
+                    d.with_labels(vec![
+                        argument_span
+                            .diagnostic_label(LabelStyle::Primary)
+                            .with_message(argument_type.to_string()),
+                    ])
+                    .with_notes(vec![inner_error])
                 }
             }
             TypeCheckError::IncompatibleTypesInList(
@@ -323,9 +343,10 @@ impl ErrorDiagnostic for TypeCheckError {
                 ])
                 .with_notes(vec![inner_error]),
             TypeCheckError::NoDimensionlessBaseUnit(span, unit_name) => d
-                .with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message(inner_error)])
+                .with_labels(vec![
+                    span.diagnostic_label(LabelStyle::Primary)
+                        .with_message(inner_error),
+                ])
                 .with_notes(vec![
                     format!("Use 'unit {unit_name}' for ad-hoc units."),
                     format!("Use 'unit {unit_name}: Scalar = …' for derived units."),
@@ -339,13 +360,15 @@ impl ErrorDiagnostic for TypeCheckError {
             | TypeCheckError::NoFunctionReferenceToGenericFunction(span)
             | TypeCheckError::OnlyFunctionsAndReferencesCanBeCalled(span)
             | TypeCheckError::DerivedUnitDefinitionMustNotBeGeneric(span)
-            | TypeCheckError::MultipleTypedHoles(span) => d.with_labels(vec![span
-                .diagnostic_label(LabelStyle::Primary)
-                .with_message(inner_error)]),
+            | TypeCheckError::MultipleTypedHoles(span) => d.with_labels(vec![
+                span.diagnostic_label(LabelStyle::Primary)
+                    .with_message(inner_error),
+            ]),
             TypeCheckError::MissingDimension(span, dim) => d
-                .with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message(format!("Missing dimension '{dim}'"))])
+                .with_labels(vec![
+                    span.diagnostic_label(LabelStyle::Primary)
+                        .with_message(format!("Missing dimension '{dim}'")),
+                ])
                 .with_notes(vec![format!(
                     "This operation requires the '{dim}' dimension to be defined"
                 )]),
@@ -412,9 +435,10 @@ impl ErrorDiagnostic for TypeCheckError {
                     .diagnostic_label(LabelStyle::Secondary)
                     .with_message("Defined here"),
             ]),
-            TypeCheckError::UnknownStruct(span, _name) => d.with_labels(vec![span
-                .diagnostic_label(LabelStyle::Primary)
-                .with_message(inner_error)]),
+            TypeCheckError::UnknownStruct(span, _name) => d.with_labels(vec![
+                span.diagnostic_label(LabelStyle::Primary)
+                    .with_message(inner_error),
+            ]),
             TypeCheckError::UnknownFieldInStructInstantiation(field_span, defn_span, _, _) => d
                 .with_labels(vec![
                     field_span
@@ -460,15 +484,17 @@ impl ErrorDiagnostic for TypeCheckError {
                 ])
             }
             TypeCheckError::MissingDimBound(span) => d
-                .with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message(inner_error)])
+                .with_labels(vec![
+                    span.diagnostic_label(LabelStyle::Primary)
+                        .with_message(inner_error),
+                ])
                 .with_notes(vec![
-                    "Consider adding `: Dim` after the type parameter".to_owned()
+                    "Consider adding `: Dim` after the type parameter".to_owned(),
                 ]),
-            TypeCheckError::ExponentiationNeedsTypeAnnotation(span) => d.with_labels(vec![span
-                .diagnostic_label(LabelStyle::Primary)
-                .with_message(inner_error)]),
+            TypeCheckError::ExponentiationNeedsTypeAnnotation(span) => d.with_labels(vec![
+                span.diagnostic_label(LabelStyle::Primary)
+                    .with_message(inner_error),
+            ]),
             TypeCheckError::TypedHoleInStatement(span, type_, statement, matches) => {
                 let mut notes = vec![
                     format!("Found a hole of type '{type_}' in the statement:"),
@@ -480,11 +506,12 @@ impl ErrorDiagnostic for TypeCheckError {
                     notes.push(format!("  {}", matches.join(", ")));
                 }
 
-                d.with_labels(vec![span
-                    .diagnostic_label(LabelStyle::Primary)
-                    .with_message(type_)])
-                    .with_message("Found typed hole")
-                    .with_notes(notes)
+                d.with_labels(vec![
+                    span.diagnostic_label(LabelStyle::Primary)
+                        .with_message(type_),
+                ])
+                .with_message("Found typed hole")
+                .with_notes(notes)
             }
         };
         vec![d]
@@ -507,9 +534,10 @@ impl ErrorDiagnostic for ResolverDiagnostic<'_, RuntimeError> {
             RuntimeErrorKind::AssertFailed(span) => diag.push(
                 Diagnostic::error()
                     .with_message("Assertion failed")
-                    .with_labels(vec![span
-                        .diagnostic_label(LabelStyle::Primary)
-                        .with_message("assertion failed")]),
+                    .with_labels(vec![
+                        span.diagnostic_label(LabelStyle::Primary)
+                            .with_message("assertion failed"),
+                    ]),
             ),
             RuntimeErrorKind::AssertEq2Failed(assert_eq2_error) => diag.push(
                 Diagnostic::error()

+ 1 - 1
numbat/src/dimension.rs

@@ -1,10 +1,10 @@
 use compact_str::{CompactString, ToCompactString};
 
+use crate::BaseRepresentationFactor;
 use crate::arithmetic::{Exponent, Power};
 use crate::ast::{TypeExpression, TypeParameterBound};
 use crate::registry::{BaseRepresentation, Registry, Result};
 use crate::span::Span;
-use crate::BaseRepresentationFactor;
 
 #[derive(Default, Clone)]
 pub struct DimensionRegistry {

+ 1 - 1
numbat/src/ffi/currency.rs

@@ -1,6 +1,6 @@
-use super::macros::*;
 use super::Args;
 use super::Result;
+use super::macros::*;
 use crate::currency::ExchangeRatesCache;
 use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;

+ 3 - 3
numbat/src/ffi/datetime.rs

@@ -1,14 +1,14 @@
 use compact_str::CompactString;
-use jiff::fmt::strtime::BrokenDownTime;
-use jiff::fmt::StdFmtWrite;
 use jiff::Span;
 use jiff::Timestamp;
 use jiff::Zoned;
+use jiff::fmt::StdFmtWrite;
+use jiff::fmt::strtime::BrokenDownTime;
 use num_traits::ToPrimitive;
 
-use super::macros::*;
 use super::Args;
 use super::Result;
+use super::macros::*;
 use crate::datetime;
 use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;

+ 1 - 1
numbat/src/ffi/functions.rs

@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 use std::sync::OnceLock;
 
-use super::{macros::*, Args};
+use super::{Args, macros::*};
 use crate::{interpreter::RuntimeErrorKind, quantity::Quantity, value::Value};
 
 use super::{Callable, ForeignFunction, Result};

+ 1 - 1
numbat/src/ffi/lookup.rs

@@ -1,8 +1,8 @@
 use compact_str::CompactString;
 
-use super::macros::*;
 use super::Args;
 use super::Result;
+use super::macros::*;
 use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::typed_ast::DType;

+ 1 - 1
numbat/src/ffi/math.rs

@@ -1,6 +1,6 @@
-use super::macros::*;
 use super::Args;
 use super::Result;
+use super::macros::*;
 
 use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;

+ 1 - 1
numbat/src/ffi/procedures.rs

@@ -7,8 +7,8 @@ use crate::{
     ast::ProcedureKind,
     ffi::ControlFlow,
     interpreter::{
-        assert_eq::{AssertEq2Error, AssertEq3Error},
         RuntimeErrorKind,
+        assert_eq::{AssertEq2Error, AssertEq3Error},
     },
     pretty_print::PrettyPrint,
     span::Span,

+ 1 - 1
numbat/src/ffi/strings.rs

@@ -1,6 +1,6 @@
-use super::macros::*;
 use super::Args;
 use super::Result;
+use super::macros::*;
 use crate::interpreter::RuntimeErrorKind;
 use crate::quantity::Quantity;
 use crate::value::Value;

+ 1 - 1
numbat/src/gamma.rs

@@ -1,6 +1,6 @@
 use std::ffi::c_double;
 
-extern "C" {
+unsafe extern "C" {
     fn tgamma(n: c_double) -> c_double;
 }
 

+ 10 - 18
numbat/src/help.rs

@@ -1,10 +1,10 @@
+use crate::Context;
+use crate::InterpreterSettings;
 /// Print a help, linking the documentation, and live-running some examples
 /// in an isolated context.
 use crate::markup as m;
 use crate::module_importer::BuiltinModuleImporter;
 use crate::resolver::CodeSource;
-use crate::Context;
-use crate::InterpreterSettings;
 
 use std::sync::{Arc, Mutex};
 
@@ -21,22 +21,14 @@ fn evaluate_example(context: &mut Context, input: &str) -> m::Markup {
         .interpret_with_settings(&mut settings, input, CodeSource::Internal)
         .expect("No error in 'help' examples");
 
-    let markup =
-        statement_output
-            .lock()
-            .unwrap()
-            .iter()
-            .fold(m::empty(), |accumulated_mk, single_line| {
-                accumulated_mk + m::nl() + m::whitespace("  ") + single_line.clone() + m::nl()
-            })
-            + interpreter_result.to_markup(
-                statements.last(),
-                context.dimension_registry(),
-                true,
-                true,
-            );
-
-    markup
+    statement_output
+        .lock()
+        .unwrap()
+        .iter()
+        .fold(m::empty(), |accumulated_mk, single_line| {
+            accumulated_mk + m::nl() + m::whitespace("  ") + single_line.clone() + m::nl()
+        })
+        + interpreter_result.to_markup(statements.last(), context.dimension_registry(), true, true)
 }
 
 pub fn help_markup() -> m::Markup {

+ 1 - 1
numbat/src/html_formatter.rs

@@ -1,7 +1,7 @@
 use crate::buffered_writer::BufferedWriter;
 use crate::markup::{FormatType, FormattedString, Formatter};
 
-use compact_str::{format_compact, CompactString};
+use compact_str::{CompactString, format_compact};
 use termcolor::{Color, WriteColor};
 
 pub struct HtmlFormatter;

+ 6 - 2
numbat/src/interpreter/assert_eq.rs

@@ -1,5 +1,5 @@
 use crate::{quantity::Quantity, span::Span, value::Value};
-use compact_str::{format_compact, CompactString};
+use compact_str::{CompactString, format_compact};
 use std::fmt::Display;
 use thiserror::Error;
 
@@ -100,7 +100,11 @@ impl Display for AssertEq3Error {
         let diff_abs = self.fmt_quantity(&self.diff_abs);
         let eps = self.fmt_quantity(&self.eps);
 
-        write!(f, "Assertion failed because the following two quantities differ by {}, which is more than {}:\n  {}\n  {}", diff_abs, eps, lhs, rhs)
+        write!(
+            f,
+            "Assertion failed because the following two quantities differ by {}, which is more than {}:\n  {}\n  {}",
+            diff_abs, eps, lhs, rhs
+        )
     }
 }
 

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

@@ -63,7 +63,9 @@ pub enum RuntimeErrorKind {
     DurationOutOfRange,
     #[error("DateTime out of range")]
     DateTimeOutOfRange,
-    #[error("{0}. See https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html#conversion-specifications for possible format specifiers.")]
+    #[error(
+        "{0}. See https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html#conversion-specifications for possible format specifiers."
+    )]
     DateFormattingError(String),
 
     #[error("Invalid format specifiers: {0}")]

+ 121 - 128
numbat/src/lib.rs

@@ -361,103 +361,99 @@ impl Context {
         // Check if it's a unit
         if let PrefixParserResult::UnitIdentifier(_span, prefix, _, full_name) =
             self.prefix_transformer.prefix_parser.parse(keyword)
-        {
-            if let Some(md) = reg
+            && let Some(md) = reg
                 .inner
                 .get_base_representation_for_name(&full_name)
                 .ok()
                 .map(|(_, md)| md)
-            {
-                let mut help = m::text("Unit: ")
-                    + m::unit(md.name.unwrap_or_else(|| keyword.to_compact_string()));
-                if let Some(url) = &md.url {
-                    help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
-                }
-                help += m::nl();
-                if md.aliases.len() > 1 {
-                    help += m::text("Aliases: ")
-                        + m::text(
-                            md.aliases
-                                .iter()
-                                .map(|(x, _)| x.as_str())
-                                .collect::<Vec<_>>()
-                                .join_compact(", "),
-                        )
+        {
+            let mut help =
+                m::text("Unit: ") + m::unit(md.name.unwrap_or_else(|| keyword.to_compact_string()));
+            if let Some(url) = &md.url {
+                help += m::text(" (") + m::string(url_encode(url)) + m::text(")");
+            }
+            help += m::nl();
+            if md.aliases.len() > 1 {
+                help += m::text("Aliases: ")
+                    + m::text(
+                        md.aliases
+                            .iter()
+                            .map(|(x, _)| x.as_str())
+                            .collect::<Vec<_>>()
+                            .join_compact(", "),
+                    )
+                    + m::nl();
+            }
+
+            if let Some(description) = &md.description {
+                let desc = "Description: ";
+                let mut lines = description.lines();
+                help += m::text(desc)
+                    + m::text(
+                        lines
+                            .by_ref()
+                            .next()
+                            .unwrap_or("")
+                            .trim()
+                            .to_compact_string(),
+                    )
+                    + m::nl();
+
+                for line in lines {
+                    help += m::whitespace(CompactString::const_new(" ").repeat(desc.len()))
+                        + m::text(line.trim().to_compact_string())
                         + m::nl();
                 }
+            }
 
-                if let Some(description) = &md.description {
-                    let desc = "Description: ";
-                    let mut lines = description.lines();
-                    help += m::text(desc)
-                        + m::text(
-                            lines
-                                .by_ref()
-                                .next()
-                                .unwrap_or("")
-                                .trim()
-                                .to_compact_string(),
-                        )
-                        + m::nl();
+            if matches!(md.type_, Type::Dimension(d) if d.is_scalar()) {
+                help +=
+                    m::text("A dimensionless unit ([") + md.readable_type + m::text("])") + m::nl();
+            } else {
+                help += m::text("A unit of: ") + md.readable_type + m::nl();
+            }
 
-                    for line in lines {
-                        help += m::whitespace(CompactString::const_new(" ").repeat(desc.len()))
-                            + m::text(line.trim().to_compact_string())
-                            + m::nl();
-                    }
+            if let Some(defining_info) = self.interpreter.get_defining_unit(&full_name) {
+                let x = defining_info
+                    .iter()
+                    .filter(|u| !u.unit_id.is_base())
+                    .map(|unit_factor| unit_factor.unit_id.unit_and_factor())
+                    .next();
+
+                if !prefix.is_none() {
+                    help += m::nl()
+                        + m::value("1 ")
+                        + m::unit(keyword.to_compact_string())
+                        + m::text(" = ")
+                        + m::value(prefix.factor().pretty_print())
+                        + m::space()
+                        + m::unit(full_name.to_compact_string());
                 }
 
-                if matches!(md.type_, Type::Dimension(d) if d.is_scalar()) {
-                    help += m::text("A dimensionless unit ([")
-                        + md.readable_type
-                        + m::text("])")
-                        + m::nl();
+                if let Some(BaseUnitAndFactor(prod, num)) = x {
+                    help += m::nl()
+                        + m::value("1 ")
+                        + m::unit(full_name.to_compact_string())
+                        + m::text(" = ")
+                        + m::value(num.pretty_print())
+                        + m::space()
+                        + prod.pretty_print_with(
+                            |f| f.exponent,
+                            'x',
+                            '/',
+                            true,
+                            Some(m::FormatType::Unit),
+                        );
                 } else {
-                    help += m::text("A unit of: ") + md.readable_type + m::nl();
+                    help += m::nl()
+                        + m::unit(full_name.to_compact_string())
+                        + m::text(" is a base unit");
                 }
+            };
 
-                if let Some(defining_info) = self.interpreter.get_defining_unit(&full_name) {
-                    let x = defining_info
-                        .iter()
-                        .filter(|u| !u.unit_id.is_base())
-                        .map(|unit_factor| unit_factor.unit_id.unit_and_factor())
-                        .next();
-
-                    if !prefix.is_none() {
-                        help += m::nl()
-                            + m::value("1 ")
-                            + m::unit(keyword.to_compact_string())
-                            + m::text(" = ")
-                            + m::value(prefix.factor().pretty_print())
-                            + m::space()
-                            + m::unit(full_name.to_compact_string());
-                    }
-
-                    if let Some(BaseUnitAndFactor(prod, num)) = x {
-                        help += m::nl()
-                            + m::value("1 ")
-                            + m::unit(full_name.to_compact_string())
-                            + m::text(" = ")
-                            + m::value(num.pretty_print())
-                            + m::space()
-                            + prod.pretty_print_with(
-                                |f| f.exponent,
-                                'x',
-                                '/',
-                                true,
-                                Some(m::FormatType::Unit),
-                            );
-                    } else {
-                        help += m::nl()
-                            + m::unit(full_name.to_compact_string())
-                            + m::text(" is a base unit");
-                    }
-                };
-
-                help += m::nl();
+            help += m::nl();
 
-                return help;
-            }
+            return help;
         };
 
         // Check if it's a dimension
@@ -504,12 +500,11 @@ impl Context {
                 let matching_units: Vec<CompactString> = self
                     .unit_representations()
                     .filter_map(|(_unit_name, (_unit_base_rep, unit_metadata))| {
-                        if let Type::Dimension(unit_dim) = &unit_metadata.type_ {
-                            if unit_dim.to_base_representation() == base_representation {
-                                if let Some((primary_alias, _)) = unit_metadata.aliases.first() {
-                                    return Some(primary_alias.clone());
-                                }
-                            }
+                        if let Type::Dimension(unit_dim) = &unit_metadata.type_
+                            && unit_dim.to_base_representation() == base_representation
+                            && let Some((primary_alias, _)) = unit_metadata.aliases.first()
+                        {
+                            return Some(primary_alias.clone());
                         }
                         None
                     })
@@ -739,51 +734,50 @@ impl Context {
             self.prefix_transformer = prefix_transformer_old.clone();
             self.typechecker = typechecker_old.clone();
 
-            if self.load_currency_module_on_demand {
-                if let Err(NumbatError::TypeCheckError(TypeCheckError::UnknownIdentifier(
+            if self.load_currency_module_on_demand
+                && let Err(NumbatError::TypeCheckError(TypeCheckError::UnknownIdentifier(
                     _,
                     identifier,
                     _,
                 ))) = &result
-                {
-                    const CURRENCY_IDENTIFIERS: &[&str] =
-                        &include!(concat!(env!("OUT_DIR"), "/currencies.rs"));
-                    if CURRENCY_IDENTIFIERS.contains(&identifier.as_str()) {
-                        let mut no_print_settings = InterpreterSettings {
-                            print_fn: Box::new(
-                                move |_: &m::Markup| { // ignore any print statements when loading this module asynchronously
-                                },
-                            ),
-                        };
-
-                        // We also call this from a thread at program startup, so if a user only starts
-                        // to use currencies later on, this will already be available and return immediately.
-                        // Otherwise, we fetch it now and make sure to block on this call.
-                        {
-                            let erc = ExchangeRatesCache::fetch();
-
-                            if erc.is_none() {
-                                return Err(Box::new(NumbatError::RuntimeError(
-                                    self.runtime_error(RuntimeErrorKind::CouldNotLoadExchangeRates),
-                                )));
-                            }
+            {
+                const CURRENCY_IDENTIFIERS: &[&str] =
+                    &include!(concat!(env!("OUT_DIR"), "/currencies.rs"));
+                if CURRENCY_IDENTIFIERS.contains(&identifier.as_str()) {
+                    let mut no_print_settings = InterpreterSettings {
+                        print_fn: Box::new(
+                            move |_: &m::Markup| { // ignore any print statements when loading this module asynchronously
+                            },
+                        ),
+                    };
+
+                    // We also call this from a thread at program startup, so if a user only starts
+                    // to use currencies later on, this will already be available and return immediately.
+                    // Otherwise, we fetch it now and make sure to block on this call.
+                    {
+                        let erc = ExchangeRatesCache::fetch();
+
+                        if erc.is_none() {
+                            return Err(Box::new(NumbatError::RuntimeError(
+                                self.runtime_error(RuntimeErrorKind::CouldNotLoadExchangeRates),
+                            )));
                         }
+                    }
 
-                        let _ = self.interpret_with_settings(
-                            &mut no_print_settings,
-                            "use units::currencies",
-                            CodeSource::Internal,
-                        )?;
+                    let _ = self.interpret_with_settings(
+                        &mut no_print_settings,
+                        "use units::currencies",
+                        CodeSource::Internal,
+                    )?;
 
-                        // Make sure we do not run into an infinite loop in case loading that
-                        // module did not bring in the required currency unit identifier. This
-                        // can happen if the list of currency identifiers is not in sync with
-                        // what the module actually defines.
-                        self.load_currency_module_on_demand = false;
+                    // Make sure we do not run into an infinite loop in case loading that
+                    // module did not bring in the required currency unit identifier. This
+                    // can happen if the list of currency identifiers is not in sync with
+                    // what the module actually defines.
+                    self.load_currency_module_on_demand = false;
 
-                        // Now we try to evaluate the user expression again:
-                        return self.interpret_with_settings(settings, code, code_source);
-                    }
+                    // Now we try to evaluate the user expression again:
+                    return self.interpret_with_settings(settings, code, code_source);
                 }
             }
         }
@@ -821,9 +815,8 @@ impl Context {
 
     pub fn print_diagnostic(&self, error: impl ErrorDiagnostic, colorize: bool) {
         use codespan_reporting::term::{
-            self,
+            self, Config,
             termcolor::{ColorChoice, StandardStream},
-            Config,
         };
 
         let color = if colorize {

+ 15 - 14
numbat/src/module_importer.rs

@@ -67,20 +67,21 @@ impl ModuleImporter for FileSystemImporter {
                 .flatten()
             {
                 let path = entry.path();
-                if path.is_file() && path.extension() == Some(OsStr::new("nbt")) {
-                    if let Ok(relative_path) = path.strip_prefix(root_path) {
-                        let components = relative_path
-                            .components()
-                            .map(|c| {
-                                c.as_os_str()
-                                    .to_string_lossy()
-                                    .trim_end_matches(".nbt")
-                                    .to_compact_string()
-                            })
-                            .collect();
-
-                        modules.push(ModulePath(components));
-                    }
+                if path.is_file()
+                    && path.extension() == Some(OsStr::new("nbt"))
+                    && let Ok(relative_path) = path.strip_prefix(root_path)
+                {
+                    let components = relative_path
+                        .components()
+                        .map(|c| {
+                            c.as_os_str()
+                                .to_string_lossy()
+                                .trim_end_matches(".nbt")
+                                .to_compact_string()
+                        })
+                        .collect();
+
+                    modules.push(ModulePath(components));
                 }
             }
         }

+ 1 - 1
numbat/src/number.rs

@@ -1,6 +1,6 @@
 use std::fmt::Display;
 
-use compact_str::{format_compact, CompactString, ToCompactString};
+use compact_str::{CompactString, ToCompactString, format_compact};
 use num_traits::{Pow, ToPrimitive};
 use pretty_dtoa::FmtFloatConfig;
 

+ 19 - 10
numbat/src/parser.rs

@@ -109,7 +109,9 @@ pub enum ParseErrorKind {
     #[error("Expected '=' or ':' after identifier (and type annotation) in 'let' assignment")]
     ExpectedEqualOrColonAfterLetIdentifier,
 
-    #[error("Expected identifier after 'fn' keyword. Note that some reserved words can not be used as function names.")]
+    #[error(
+        "Expected identifier after 'fn' keyword. Note that some reserved words can not be used as function names."
+    )]
     ExpectedIdentifierAfterFn,
 
     #[error("Expected identifier")]
@@ -382,10 +384,10 @@ impl<'a> Parser<'a> {
             } else if self.match_exact(tokens, TokenKind::None).is_some() {
                 Ok(Some(AcceptsPrefix::none()))
             } else {
-                return Err(ParseError::new(
+                Err(ParseError::new(
                     ParseErrorKind::UnknownAliasAnnotation,
                     self.peek(tokens).span,
-                ));
+                ))
             }
         } else {
             Ok(None)
@@ -1083,7 +1085,7 @@ impl<'a> Parser<'a> {
                     return Err(ParseError::new(
                         ParseErrorKind::ExpectedIdentifierOrCallAfterPostfixApply,
                         full_span,
-                    ))
+                    ));
                 }
             }
         }
@@ -1424,7 +1426,7 @@ impl<'a> Parser<'a> {
                         return Err(ParseError::new(
                             ParseErrorKind::MissingClosingParen,
                             self.peek(tokens).span,
-                        ))
+                        ));
                     }
                 }
             } else if self.match_exact(tokens, TokenKind::RightParen).is_some() {
@@ -2097,8 +2099,8 @@ mod tests {
     use super::*;
     use crate::{
         ast::{
-            binop, boolean, conditional, factorial, identifier, list, logical_neg, negate, scalar,
-            struct_, ReplaceSpans,
+            ReplaceSpans, binop, boolean, conditional, factorial, identifier, list, logical_neg,
+            negate, scalar, struct_,
         },
         span::ByteIndex,
     };
@@ -2907,7 +2909,9 @@ mod tests {
         );
 
         parse_as(
-            &["@name(\"Some function\") @description(\"This is a description of some_function.\") fn some_function(x) = 1"],
+            &[
+                "@name(\"Some function\") @description(\"This is a description of some_function.\") fn some_function(x) = 1",
+            ],
             Statement::DefineFunction {
                 function_name_span: Span::dummy(),
                 function_name: "some_function",
@@ -2926,7 +2930,9 @@ 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"],
+            &[
+                "@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",
@@ -2937,7 +2943,10 @@ mod tests {
                 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(
+                        "some_function(2)".into(),
+                        Some("Use this function:".into()),
+                    ),
                     decorator::Decorator::Example("let some_var = some_function(0)".into(), None),
                 ],
             },

+ 1 - 1
numbat/src/plot.rs

@@ -2,10 +2,10 @@ use compact_str::CompactString;
 
 #[cfg(feature = "plotting")]
 use plotly::{
+    Bar, Configuration, Layout, Plot, Scatter,
     color::Rgb,
     common::{Font, Line},
     layout::Axis,
-    Bar, Configuration, Layout, Plot, Scatter,
 };
 
 #[cfg(feature = "plotting")]

+ 1 - 1
numbat/src/prefix.rs

@@ -1,4 +1,4 @@
-use compact_str::{format_compact, CompactString};
+use compact_str::{CompactString, format_compact};
 
 use crate::number::Number;
 

+ 4 - 4
numbat/src/prefix_parser.rs

@@ -174,10 +174,10 @@ impl PrefixParser {
             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, definition_span, *original_span));
-            }
+        if clash_with_other_identifiers
+            && let Some(original_span) = self.other_identifiers.get(name)
+        {
+            return Err(self.identifier_clash_error(name, definition_span, *original_span));
         }
 
         match self.parse(name) {

+ 8 - 8
numbat/src/quantity.rs

@@ -1,9 +1,9 @@
 use crate::arithmetic::{Exponent, Power, Rational};
 use crate::number::Number;
 use crate::pretty_print::PrettyPrint;
-use crate::unit::{is_multiple_of, Unit, UnitFactor};
+use crate::unit::{Unit, UnitFactor, is_multiple_of};
 
-use compact_str::{format_compact, CompactString, ToCompactString};
+use compact_str::{CompactString, ToCompactString, format_compact};
 use itertools::Itertools;
 use num_rational::Ratio;
 use num_traits::{FromPrimitive, Zero};
@@ -159,12 +159,12 @@ impl Quantity {
                 factor.exponent = Exponent::from_integer(1);
                 let factor_unit = Unit::from_factor(factor);
 
-                if let Some(alpha) = is_multiple_of(&unit, &factor_unit) {
-                    if alpha.is_integer() {
-                        let simplified_unit = factor_unit.power(alpha);
-                        if let Ok(q) = self.convert_to(&simplified_unit) {
-                            return q;
-                        }
+                if let Some(alpha) = is_multiple_of(&unit, &factor_unit)
+                    && alpha.is_integer()
+                {
+                    let simplified_unit = factor_unit.power(alpha);
+                    if let Ok(q) = self.convert_to(&simplified_unit) {
+                        return q;
                     }
                 }
             }

+ 1 - 1
numbat/src/registry.rs

@@ -6,7 +6,7 @@ use num_traits::Zero;
 use thiserror::Error;
 
 use crate::{
-    arithmetic::{pretty_exponent, Exponent, Power, Rational},
+    arithmetic::{Exponent, Power, Rational, pretty_exponent},
     pretty_print::PrettyPrint,
     product::{Canonicalize, Product},
     suggestion,

+ 1 - 1
numbat/src/resolver.rs

@@ -1,7 +1,7 @@
 use std::{collections::HashMap, path::PathBuf, sync::Arc};
 
 use crate::{
-    ast::Statement, module_importer::ModuleImporter, parser::parse, span::Span, ParseError,
+    ParseError, ast::Statement, module_importer::ModuleImporter, parser::parse, span::Span,
 };
 
 use codespan_reporting::files::SimpleFiles;

+ 4 - 3
numbat/src/session_history.rs

@@ -127,14 +127,15 @@ mod test {
     #[test]
     fn test_error() {
         let sh = SessionHistory::new();
-        assert!(sh
-            .save(
+        assert!(
+            sh.save(
                 ".", // one place we know writing will fail
                 SessionHistoryOptions {
                     include_err_lines: false,
                     trim_lines: false
                 }
             )
-            .is_err())
+            .is_err()
+        )
     }
 }

+ 3 - 3
numbat/src/tokenizer.rs

@@ -417,7 +417,7 @@ impl Tokenizer {
             let scope = self.scopes.pop().unwrap();
             Ok(scope)
         } else {
-            return Err(TokenizerError {
+            Err(TokenizerError {
                 kind: TokenizerErrorKind::UnexpectedScopeClosing {
                     current_scope: self.scopes.last().copied(),
                     closing_scope_type: scope_type,
@@ -427,7 +427,7 @@ impl Tokenizer {
                     end: self.current,
                     code_source_id: self.code_source_id,
                 },
-            });
+            })
         }
     }
 
@@ -450,7 +450,7 @@ impl Tokenizer {
         self.scopes
             .iter()
             .filter(|scope| scope.scope_type == scope_type)
-            .last()
+            .next_back()
             .map(|scope| scope.scope_start)
     }
 

+ 4 - 4
numbat/src/typechecker/const_evaluation.rs

@@ -3,7 +3,7 @@ use crate::{ast, typed_ast};
 
 use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, FromPrimitive, Zero};
 
-use super::{error::Result, TypeCheckError};
+use super::{TypeCheckError, error::Result};
 
 fn to_rational_exponent(exponent_f64: f64) -> Option<Exponent> {
     Rational::from_f64(exponent_f64)
@@ -16,10 +16,10 @@ pub fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result<Exponent> {
     let name = match expr {
         typed_ast::Expression::Scalar(span, n, _type) => {
             return Ok(to_rational_exponent(n.to_f64())
-                .ok_or(TypeCheckError::NonRationalExponent(*span))?)
+                .ok_or(TypeCheckError::NonRationalExponent(*span))?);
         }
-        typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::Negate, ref expr, _) => {
-            return Ok(-evaluate_const_expr(expr)?)
+        typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::Negate, expr, _) => {
+            return Ok(-evaluate_const_expr(expr)?);
         }
         typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::Factorial(_order), _, _) => {
             "factorial"

+ 8 - 10
numbat/src/typechecker/constraints.rs

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use compact_str::{format_compact, CompactString};
+use compact_str::{CompactString, format_compact};
 
 use super::substitutions::{ApplySubstitution, Substitution, SubstitutionError};
 use crate::type_variable::TypeVariable;
@@ -321,15 +321,13 @@ impl Constraint {
             Constraint::HasField(struct_type, field_name, field_type)
                 if struct_type.is_closed() =>
             {
-                if let Type::Struct(info) = struct_type {
-                    if let Some((_, actual_field_type)) = info.fields.get(field_name) {
-                        Some(Satisfied::with_new_constraints(vec![Constraint::Equal(
-                            actual_field_type.clone(),
-                            field_type.clone(),
-                        )]))
-                    } else {
-                        None
-                    }
+                if let Type::Struct(info) = struct_type
+                    && let Some((_, actual_field_type)) = info.fields.get(field_name)
+                {
+                    Some(Satisfied::with_new_constraints(vec![Constraint::Equal(
+                        actual_field_type.clone(),
+                        field_type.clone(),
+                    )]))
                 } else {
                     None
                 }

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

@@ -1,12 +1,12 @@
 use compact_str::CompactString;
 
+use crate::Type;
 use crate::ast::{TypeAnnotation, TypeParameterBound};
 use crate::dimension::DimensionRegistry;
 use crate::pretty_print::PrettyPrint;
 use crate::span::Span;
 use crate::type_variable::TypeVariable;
 use crate::typed_ast::pretty_print_function_signature;
-use crate::Type;
 
 use super::map_stack::MapStack;
 use super::substitutions::{ApplySubstitution, Substitution, SubstitutionError};

+ 16 - 6
numbat/src/typechecker/error.rs

@@ -7,8 +7,8 @@ use crate::{BaseRepresentation, NameResolutionError, Type};
 use compact_str::CompactString;
 use thiserror::Error;
 
-use super::substitutions::SubstitutionError;
 use super::IncompatibleDimensionsError;
+use super::substitutions::SubstitutionError;
 
 #[derive(Debug, Clone, Error, PartialEq, Eq)]
 pub enum TypeCheckError {
@@ -51,10 +51,14 @@ pub enum TypeCheckError {
         num_args: usize,
     },
 
-    #[error("'{1}' can not be used as a type parameter because it is also an existing dimension identifier.")]
+    #[error(
+        "'{1}' can not be used as a type parameter because it is also an existing dimension identifier."
+    )]
     TypeParameterNameClash(Span, String),
 
-    #[error("Foreign function definition (without body) '{1}' needs parameter and return type annotations.")]
+    #[error(
+        "Foreign function definition (without body) '{1}' needs parameter and return type annotations."
+    )]
     ForeignFunctionNeedsTypeAnnotations(Span, String),
 
     #[error("Unknown foreign function (without body) '{1}'")]
@@ -135,16 +139,22 @@ pub enum TypeCheckError {
     #[error(transparent)]
     NameResolutionError(#[from] NameResolutionError),
 
-    #[error("Could not solve the following constraints:\n{0}\n.. while trying to infer types in the (elaborated) statement:\n  {1}\n")]
+    #[error(
+        "Could not solve the following constraints:\n{0}\n.. while trying to infer types in the (elaborated) statement:\n  {1}\n"
+    )]
     ConstraintSolverError(String, String),
 
-    #[error("{1}\nThis error occured while trying to infer types in the (elaborated) statement:\n  {0}\n")]
+    #[error(
+        "{1}\nThis error occured while trying to infer types in the (elaborated) statement:\n  {0}\n"
+    )]
     SubstitutionError(String, SubstitutionError),
 
     #[error("Missing dimension bound for type parameter")]
     MissingDimBound(Span),
 
-    #[error("Type for exponentiation operation can not be inferred for this case, consider adding a type annotation for the base")]
+    #[error(
+        "Type for exponentiation operation can not be inferred for this case, consider adding a type annotation for the base"
+    )]
     ExponentiationNeedsTypeAnnotation(Span),
 
     #[error("Derived unit definitions may not contain generic types. Use a variable instead")]

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

@@ -1,10 +1,10 @@
 use std::{collections::HashMap, error::Error, fmt};
 
-use crate::arithmetic::{pretty_exponent, Exponent, Rational};
+use crate::arithmetic::{Exponent, Rational, pretty_exponent};
 use crate::registry::{BaseRepresentation, BaseRepresentationFactor};
 use crate::span::Span;
 
-use compact_str::{format_compact, CompactString, ToCompactString};
+use compact_str::{CompactString, ToCompactString, format_compact};
 use itertools::Itertools;
 use num_traits::Zero;
 use unicode_width::UnicodeWidthStr;

+ 56 - 64
numbat/src/typechecker/mod.rs

@@ -22,15 +22,15 @@ use crate::ast::{
     TypeExpression, TypeParameterBound,
 };
 use crate::dimension::DimensionRegistry;
-use crate::name_resolution::Namespace;
 use crate::name_resolution::LAST_RESULT_IDENTIFIERS;
+use crate::name_resolution::Namespace;
 use crate::pretty_print::PrettyPrint;
 use crate::span::Span;
 use crate::type_variable::TypeVariable;
 use crate::typed_ast::{self, DType, DTypeFactor, Expression, StructInfo, Type};
 use crate::{decorator, ffi, suggestion};
 
-use compact_str::{format_compact, CompactString, ToCompactString};
+use compact_str::{CompactString, ToCompactString, format_compact};
 use const_evaluation::evaluate_const_expr;
 use constraints::{Constraint, ConstraintSet, ConstraintSolverError, TrivialResolution};
 use environment::{Environment, FunctionMetadata, FunctionSignature};
@@ -235,13 +235,13 @@ impl TypeChecker {
     fn type_from_annotation(&self, annotation: &TypeAnnotation) -> Result<Type> {
         match annotation {
             TypeAnnotation::TypeExpression(dexpr) => {
-                if let TypeExpression::TypeIdentifier(_, name) = dexpr {
-                    if let Some(info) = self.structs.get(name) {
-                        // if we see a struct name here, it's safe to assume it
-                        // isn't accidentally clashing with a dimension, we
-                        // check that earlier.
-                        return Ok(Type::Struct(Box::new(info.clone())));
-                    }
+                if let TypeExpression::TypeIdentifier(_, name) = dexpr
+                    && let Some(info) = self.structs.get(name)
+                {
+                    // if we see a struct name here, it's safe to assume it
+                    // isn't accidentally clashing with a dimension, we
+                    // check that earlier.
+                    return Ok(Type::Struct(Box::new(info.clone())));
                 }
 
                 let mut factors = self
@@ -254,7 +254,7 @@ impl TypeChecker {
                 // Replace BaseDimension("D") with TVar("D") for all type parameters
                 for (factor, _) in Arc::make_mut(&mut factors) {
                     *factor = match factor {
-                        DTypeFactor::BaseDimension(ref n)
+                        &mut DTypeFactor::BaseDimension(ref n)
                             if self
                                 .registry
                                 .introduced_type_parameters
@@ -446,7 +446,7 @@ impl TypeChecker {
 
                     if *op == BinaryOperator::Sub && rhs_is_datetime {
                         let time = DType::base_dimension("Time"); // TODO: error handling
-                                                                  // TODO make sure the "second" unit exists
+                        // TODO make sure the "second" unit exists
 
                         typed_ast::Expression::BinaryOperatorForDate(
                             *span_op,
@@ -1509,55 +1509,48 @@ impl TypeChecker {
                     if self
                         .add_equal_constraint(&return_type_inferred, &return_type)
                         .is_trivially_violated()
+                        && let Some(annotated_return_type) = annotated_return_type
                     {
-                        if let Some(annotated_return_type) = annotated_return_type {
-                            match (&return_type_inferred, annotated_return_type) {
-                                (
-                                    Type::Dimension(dtype_deduced),
-                                    Type::Dimension(dtype_specified),
-                                ) => {
-                                    return Err(Box::new(TypeCheckError::IncompatibleDimensions(
-                                        IncompatibleDimensionsError {
-                                            span_operation: *function_name_span,
-                                            operation: "function return type".into(),
-                                            span_expected: return_type_annotation
-                                                .as_ref()
-                                                .unwrap()
-                                                .full_span(),
-                                            expected_name: "specified return type",
-                                            expected_dimensions: self
-                                                .registry
-                                                .get_derived_entry_names_for(
-                                                    &dtype_specified.to_base_representation(),
-                                                ),
-                                            expected_type: dtype_specified.to_base_representation(),
-                                            span_actual: body
-                                                .as_ref()
-                                                .map(|b| b.full_span())
-                                                .unwrap(),
-                                            actual_name: "   actual return type",
-                                            actual_name_for_fix: "expression in the function body",
-                                            actual_dimensions: self
-                                                .registry
-                                                .get_derived_entry_names_for(
-                                                    &dtype_deduced.to_base_representation(),
-                                                ),
-                                            actual_type: dtype_deduced.to_base_representation(),
-                                        },
-                                    )));
-                                }
-                                (return_type_inferred, type_specified) => {
-                                    return Err(Box::new(
-                                        TypeCheckError::IncompatibleTypesInAnnotation(
-                                            "function definition".into(),
-                                            *function_name_span,
-                                            type_specified,
-                                            return_type_annotation.as_ref().unwrap().full_span(),
-                                            return_type_inferred.clone(),
-                                            body.as_ref().map(|b| b.full_span()).unwrap(),
-                                        ),
-                                    ));
-                                }
+                        match (&return_type_inferred, annotated_return_type) {
+                            (Type::Dimension(dtype_deduced), Type::Dimension(dtype_specified)) => {
+                                return Err(Box::new(TypeCheckError::IncompatibleDimensions(
+                                    IncompatibleDimensionsError {
+                                        span_operation: *function_name_span,
+                                        operation: "function return type".into(),
+                                        span_expected: return_type_annotation
+                                            .as_ref()
+                                            .unwrap()
+                                            .full_span(),
+                                        expected_name: "specified return type",
+                                        expected_dimensions: self
+                                            .registry
+                                            .get_derived_entry_names_for(
+                                                &dtype_specified.to_base_representation(),
+                                            ),
+                                        expected_type: dtype_specified.to_base_representation(),
+                                        span_actual: body.as_ref().map(|b| b.full_span()).unwrap(),
+                                        actual_name: "   actual return type",
+                                        actual_name_for_fix: "expression in the function body",
+                                        actual_dimensions: self
+                                            .registry
+                                            .get_derived_entry_names_for(
+                                                &dtype_deduced.to_base_representation(),
+                                            ),
+                                        actual_type: dtype_deduced.to_base_representation(),
+                                    },
+                                )));
+                            }
+                            (return_type_inferred, type_specified) => {
+                                return Err(Box::new(
+                                    TypeCheckError::IncompatibleTypesInAnnotation(
+                                        "function definition".into(),
+                                        *function_name_span,
+                                        type_specified,
+                                        return_type_annotation.as_ref().unwrap().full_span(),
+                                        return_type_inferred.clone(),
+                                        body.as_ref().map(|b| b.full_span()).unwrap(),
+                                    ),
+                                ));
                             }
                         }
                     }
@@ -1851,12 +1844,11 @@ impl TypeChecker {
 
         if let typed_ast::Statement::DefineDerivedUnit(_, _, expr, _, _annotation, type_, _) =
             &elaborated_statement
+            && !type_.unsafe_as_concrete().is_closed()
         {
-            if !type_.unsafe_as_concrete().is_closed() {
-                return Err(Box::new(
-                    TypeCheckError::DerivedUnitDefinitionMustNotBeGeneric(expr.full_span()),
-                ));
-            }
+            return Err(Box::new(
+                TypeCheckError::DerivedUnitDefinitionMustNotBeGeneric(expr.full_span()),
+            ));
         }
 
         // Make sure that the user-specified type parameter bounds are properly reflected:

+ 1 - 1
numbat/src/typechecker/qualified_type.rs

@@ -1,4 +1,4 @@
-use crate::{type_variable::TypeVariable, Type};
+use crate::{Type, type_variable::TypeVariable};
 
 use super::{
     substitutions::{ApplySubstitution, Substitution, SubstitutionError},

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

@@ -1,8 +1,8 @@
 use thiserror::Error;
 
+use crate::Statement;
 use crate::type_variable::TypeVariable;
 use crate::typed_ast::{DType, DTypeFactor, DefineVariable, Expression, StructInfo, Type};
-use crate::Statement;
 
 #[derive(Debug, Clone)]
 pub struct Substitution(pub Vec<(TypeVariable, Type)>);

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

@@ -1,14 +1,14 @@
 mod type_checking;
 mod type_inference;
 
+use crate::Statement;
 use crate::parser::parse;
 use crate::prefix_transformer::Transformer;
 use crate::typechecker::{Result, TypeCheckError};
 use crate::typed_ast::{self, DType};
-use crate::Statement;
 
-use super::type_scheme::TypeScheme;
 use super::TypeChecker;
+use super::type_scheme::TypeScheme;
 
 const TEST_PRELUDE: &str = "
     dimension Scalar = 1

+ 8 - 8
numbat/src/typed_ast.rs

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use compact_str::{format_compact, CompactString, ToCompactString};
+use compact_str::{CompactString, ToCompactString, format_compact};
 use indexmap::IndexMap;
 use itertools::Itertools;
 
@@ -11,14 +11,14 @@ use crate::dimension::DimensionRegistry;
 use crate::pretty_print::escape_numbat_string;
 use crate::traversal::{ForAllExpressions, ForAllTypeSchemes};
 use crate::type_variable::TypeVariable;
+use crate::typechecker::TypeCheckError;
 use crate::typechecker::qualified_type::QualifiedType;
 use crate::typechecker::type_scheme::TypeScheme;
-use crate::typechecker::TypeCheckError;
+use crate::{BaseRepresentation, BaseRepresentationFactor, markup as m};
 use crate::{
     decorator::Decorator, markup::Markup, number::Number, prefix::Prefix,
     prefix_parser::AcceptsPrefix, pretty_print::PrettyPrint, span::Span,
 };
-use crate::{markup as m, BaseRepresentation, BaseRepresentationFactor};
 
 /// Dimension type
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -155,11 +155,11 @@ impl DType {
         // Merge powers of equal factors:
         let mut new_factors = Vec::new();
         for (f, n) in self.factors.iter() {
-            if let Some((last_f, last_n)) = new_factors.last_mut() {
-                if f == last_f {
-                    *last_n += n;
-                    continue;
-                }
+            if let Some((last_f, last_n)) = new_factors.last_mut()
+                && f == last_f
+            {
+                *last_n += n;
+                continue;
             }
             new_factors.push((f.clone(), *n));
         }

+ 1 - 1
numbat/src/unit.rs

@@ -5,7 +5,7 @@ use itertools::Itertools;
 use num_traits::{ToPrimitive, Zero};
 
 use crate::{
-    arithmetic::{pretty_exponent, Exponent, Power, Rational},
+    arithmetic::{Exponent, Power, Rational, pretty_exponent},
     number::Number,
     prefix::Prefix,
     prefix_parser::AcceptsPrefix,

+ 9 - 6
numbat/src/vm.rs

@@ -391,7 +391,7 @@ impl Vm {
         let (bytecode, spans) = self.current_chunk_mut();
         bytecode.push(op as u8);
         Self::push_u16(bytecode, arg);
-        spans.extend(std::iter::repeat(span).take(3));
+        spans.extend(std::iter::repeat_n(span, 3));
     }
 
     pub(crate) fn add_op2(&mut self, op: Op, arg1: u16, arg2: u16, span: Span) {
@@ -399,7 +399,7 @@ impl Vm {
         bytecode.push(op as u8);
         Self::push_u16(bytecode, arg1);
         Self::push_u16(bytecode, arg2);
-        spans.extend(std::iter::repeat(span).take(5));
+        spans.extend(std::iter::repeat_n(span, 5));
     }
 
     pub(crate) fn add_op3(&mut self, op: Op, arg1: u16, arg2: u16, arg3: u16, span: Span) {
@@ -408,7 +408,7 @@ impl Vm {
         Self::push_u16(bytecode, arg1);
         Self::push_u16(bytecode, arg2);
         Self::push_u16(bytecode, arg3);
-        spans.extend(std::iter::repeat(span).take(7));
+        spans.extend(std::iter::repeat_n(span, 7));
     }
 
     pub fn current_offset(&self) -> u16 {
@@ -793,7 +793,7 @@ impl Vm {
                                     lhs.unit().clone(),
                                     rhs.unit().clone(),
                                 )),
-                            )))
+                            )));
                         }
                         QuantityOrdering::NanOperand => false,
                         QuantityOrdering::Ok(Ordering::Less) => {
@@ -939,10 +939,13 @@ impl Vm {
 
                             match &self.ffi_callables[function_idx].callable {
                                 Callable::Function(function) => {
-                                    let result = (function)(args).map_err(|e| self.runtime_error(*e))?;
+                                    let result =
+                                        (function)(args).map_err(|e| self.runtime_error(*e))?;
                                     self.push(result);
                                 }
-                                Callable::Procedure(..) => unreachable!("Foreign procedures can not be targeted by a function reference"),
+                                Callable::Procedure(..) => unreachable!(
+                                    "Foreign procedures can not be targeted by a function reference"
+                                ),
                             }
                         }
                         FunctionReference::TzConversion(tz_name) => {

+ 1 - 1
numbat/tests/common.rs

@@ -1,6 +1,6 @@
 use std::path::Path;
 
-use numbat::{module_importer::FileSystemImporter, resolver::CodeSource, Context, NumbatError};
+use numbat::{Context, NumbatError, module_importer::FileSystemImporter, resolver::CodeSource};
 use once_cell::sync::Lazy;
 
 pub fn get_test_context_without_prelude() -> Context {

+ 12 - 8
numbat/tests/interpreter.rs

@@ -4,10 +4,10 @@ use common::get_test_context;
 
 use compact_str::CompactString;
 use insta::assert_snapshot;
+use numbat::NumbatError;
 use numbat::markup::{Formatter, PlainTextFormatter};
 use numbat::resolver::CodeSource;
-use numbat::NumbatError;
-use numbat::{pretty_print::PrettyPrint, Context, InterpreterResult};
+use numbat::{Context, InterpreterResult, pretty_print::PrettyPrint};
 
 #[track_caller]
 fn expect_output_with_context(ctx: &mut Context, code: &str, expected_output: impl AsRef<str>) {
@@ -802,7 +802,10 @@ fn test_full_simplify_for_function_calls() {
 
 #[test]
 fn test_datetime_runtime_errors() {
-    expect_failure("datetime(\"2000-01-99\")", "Unrecognized datetime format: failed to parse day in date \"2000-01-99\": day is not valid: parameter 'day' with value 99 is not in the required range of 1..=31");
+    expect_failure(
+        "datetime(\"2000-01-99\")",
+        "Unrecognized datetime format: failed to parse day in date \"2000-01-99\": day is not valid: parameter 'day' with value 99 is not in the required range of 1..=31",
+    );
     expect_failure("now() -> tz(\"Europe/NonExisting\")", "Unknown timezone");
     expect_failure(
         "date(\"2000-01-01\") + 1e100 years",
@@ -849,11 +852,12 @@ fn test_recovery_after_runtime_error() {
         let mut ctx = get_test_context();
 
         expect_failure_with_context(&mut ctx, "let x = 1/0", "Division by zero");
-        assert!(ctx
-            .interpret("let x = 1", CodeSource::Internal)
-            .unwrap()
-            .1
-            .is_continue());
+        assert!(
+            ctx.interpret("let x = 1", CodeSource::Internal)
+                .unwrap()
+                .1
+                .is_continue()
+        );
         expect_output_with_context(&mut ctx, "x", "1");
     }
 }