diff --git a/numbat-cli/src/main.rs b/numbat-cli/src/main.rs index 1d380daf..7ec558c4 100644 --- a/numbat-cli/src/main.rs +++ b/numbat-cli/src/main.rs @@ -13,6 +13,7 @@ use highlighter::NumbatHighlighter; use itertools::Itertools; use numbat::command::{CommandControlFlow, CommandRunner}; +use numbat::currency::OnDemandCurrencyManager; use numbat::diagnostic::{ErrorDiagnostic, ResolverDiagnostic}; use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystemImporter}; use numbat::pretty_print::PrettyPrint; @@ -195,10 +196,7 @@ impl Cli { if self.config.load_prelude && self.config.exchange_rates.fetching_policy != ExchangeRateFetchingPolicy::Never { - self.context - .lock() - .unwrap() - .load_currency_module_on_demand(true); + Context::set_currency_manager(OnDemandCurrencyManager::default()); } Ok(()) @@ -296,7 +294,7 @@ impl Cli { == ExchangeRateFetchingPolicy::OnStartup { Some(thread::spawn(move || { - numbat::Context::prefetch_exchange_rates(); + Context::set_currency_manager(OnDemandCurrencyManager::preloaded()); })) } else { None diff --git a/numbat-wasm/src/lib.rs b/numbat-wasm/src/lib.rs index 2ea0715d..61943b63 100644 --- a/numbat-wasm/src/lib.rs +++ b/numbat-wasm/src/lib.rs @@ -6,6 +6,7 @@ use wasm_bindgen::prelude::*; use numbat::buffered_writer::BufferedWriter; use numbat::command::{CommandControlFlow, CommandRunner}; +use numbat::currency::OnDemandCurrencyManager; use numbat::diagnostic::{ErrorDiagnostic, ResolverDiagnostic}; use numbat::help::basic_help_markup; use numbat::html_formatter::{HtmlFormatter, HtmlWriter}; @@ -86,7 +87,7 @@ impl Numbat { } pub fn set_exchange_rates(&mut self, xml_content: &str) { - Context::set_exchange_rates(xml_content); + Context::set_currency_manager(OnDemandCurrencyManager::from_xml(xml_content)); let _ = self .ctx .interpret("use units::currencies", CodeSource::Internal) diff --git a/numbat/src/currency.rs b/numbat/src/currency.rs index 35b2d130..31c60389 100644 --- a/numbat/src/currency.rs +++ b/numbat/src/currency.rs @@ -1,60 +1,123 @@ -use std::sync::{Mutex, MutexGuard, OnceLock}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{LazyLock, Mutex, OnceLock}; -use numbat_exchange_rates::parse_exchange_rates; +use numbat_exchange_rates::{ExchangeRates, parse_exchange_rates}; -#[derive(Debug)] -pub(crate) enum ExchangeRates { - Real(numbat_exchange_rates::ExchangeRates), - TestRates, +pub trait CurrencyManager: Send + Sync { + fn get_rate(&self, currency: &str) -> Option; + fn is_known(&self, currency: &str) -> bool; + fn is_loaded(&self) -> bool; + fn load(&self) -> Result, CouldNotLoadCurrencyManager>; } -static EXCHANGE_RATES: OnceLock>> = OnceLock::new(); +pub struct CouldNotLoadCurrencyManager; -pub struct ExchangeRatesCache {} +#[derive(Debug, Clone, Default)] +pub struct NullCurrencyManager {} -impl ExchangeRatesCache { - pub fn new() -> Self { - Self {} +impl CurrencyManager for NullCurrencyManager { + fn get_rate(&self, _: &str) -> Option { + None } - pub fn get_rate(&self, currency: &str) -> Option { - let rates = Self::fetch(); - rates - .as_ref() - .and_then(|er| match er { - ExchangeRates::Real(er) => er.get(currency), - ExchangeRates::TestRates => Some(&1.0), - }) - .cloned() + fn is_known(&self, _: &str) -> bool { + false } - pub fn set_from_xml(xml_content: &str) { - EXCHANGE_RATES - .set(Mutex::new( - parse_exchange_rates(xml_content).map(ExchangeRates::Real), - )) + fn is_loaded(&self) -> bool { + true + } + + fn load(&self) -> Result, CouldNotLoadCurrencyManager> { + Ok(None) + } +} + +#[derive(Debug, Default)] +pub struct OnDemandCurrencyManager { + loaded: AtomicBool, + rates: OnceLock>, +} + +impl OnDemandCurrencyManager { + const CURRENCY_IDENTIFIERS: &[&str] = &include!(concat!(env!("OUT_DIR"), "/currencies.rs")); + + pub fn preloaded() -> Self { + let manager = Self::default(); + manager.load_rates(); + manager + } + + pub fn from_xml(xml_content: &str) -> Self { + let manager = Self::default(); + manager + .rates + .set(parse_exchange_rates(xml_content)) .unwrap(); + manager + } + + fn load_rates(&self) { + self.rates.get_or_init(fetch_exchange_rates); + } +} + +impl CurrencyManager for OnDemandCurrencyManager { + fn get_rate(&self, currency: &str) -> Option { + self.rates.get()?.as_ref()?.get(currency).copied() } - #[cfg(feature = "fetch-exchangerates")] - pub fn fetch() -> MutexGuard<'static, Option> { - EXCHANGE_RATES - .get_or_init(|| { - Mutex::new(numbat_exchange_rates::fetch_exchange_rates().map(ExchangeRates::Real)) - }) - .lock() - .unwrap() + fn is_known(&self, currency: &str) -> bool { + Self::CURRENCY_IDENTIFIERS.contains(¤cy) } - #[cfg(not(feature = "fetch-exchangerates"))] - pub fn fetch() -> MutexGuard<'static, Option> { - EXCHANGE_RATES - .get_or_init(|| Mutex::new(None)) - .lock() - .unwrap() + fn is_loaded(&self) -> bool { + self.loaded.load(Ordering::Acquire) } - pub fn use_test_rates() { - EXCHANGE_RATES.get_or_init(|| Mutex::new(Some(ExchangeRates::TestRates))); + fn load(&self) -> Result, CouldNotLoadCurrencyManager> { + self.load_rates(); + + if self.rates.get().is_none() { + return Err(CouldNotLoadCurrencyManager); + } + + self.loaded.store(true, Ordering::Release); + + Ok(Some("use units::currencies".into())) } } + +static MANAGER: LazyLock>> = + LazyLock::new(|| Mutex::new(Box::new(NullCurrencyManager {}))); + +pub(crate) fn set_manager(currency_manager: impl CurrencyManager + 'static) { + *MANAGER.lock().unwrap() = Box::new(currency_manager); +} + +pub(crate) fn get_rate(currency: &str) -> Option { + MANAGER.try_lock().ok()?.get_rate(currency) +} + +pub(crate) fn is_known(currency: &str) -> bool { + MANAGER.try_lock().is_ok_and(|m| m.is_known(currency)) +} + +pub(crate) fn is_loaded() -> bool { + MANAGER.try_lock().map_or(true, |m| m.is_loaded()) +} + +pub(crate) fn load_manager() -> Result, CouldNotLoadCurrencyManager> { + MANAGER + .try_lock() + .map_err(|_| CouldNotLoadCurrencyManager) + .and_then(|m| m.load()) +} + +#[cfg(feature = "fetch-exchangerates")] +use numbat_exchange_rates::fetch_exchange_rates; + +#[cfg(not(feature = "fetch-exchangerates"))] +fn fetch_exchange_rates() -> Option { + None +} diff --git a/numbat/src/ffi/currency.rs b/numbat/src/ffi/currency.rs index e446bd10..db64a20e 100644 --- a/numbat/src/ffi/currency.rs +++ b/numbat/src/ffi/currency.rs @@ -2,7 +2,6 @@ use super::Args; use super::FfiContext; use super::Result; use super::macros::*; -use crate::currency::ExchangeRatesCache; use crate::interpreter::RuntimeErrorKind; use crate::quantity::Quantity; use crate::typechecker::type_scheme::TypeScheme; @@ -15,7 +14,5 @@ pub fn exchange_rate( ) -> Result> { let rate = string_arg!(args); - let exchange_rates = ExchangeRatesCache::new(); - - return_scalar!(exchange_rates.get_rate(&rate).unwrap_or(f64::NAN)) + return_scalar!(crate::currency::get_rate(&rate).unwrap_or(f64::NAN)) } diff --git a/numbat/src/lib.rs b/numbat/src/lib.rs index 8a000703..f3629ea1 100644 --- a/numbat/src/lib.rs +++ b/numbat/src/lib.rs @@ -5,7 +5,7 @@ pub mod buffered_writer; mod bytecode_interpreter; mod column_formatter; pub mod command; -mod currency; +pub mod currency; pub mod datetime; mod decorator; pub mod diagnostic; @@ -56,7 +56,7 @@ use column_formatter::ColumnFormatter; use compact_str::CompactString; use compact_str::CompactStringExt; use compact_str::ToCompactString; -use currency::ExchangeRatesCache; +use currency::CurrencyManager; use diagnostic::ErrorDiagnostic; use dimension::DimensionRegistry; use interpreter::Interpreter; @@ -114,7 +114,6 @@ pub struct Context { typechecker: TypeChecker, interpreter: BytecodeInterpreter, resolver: Resolver, - load_currency_module_on_demand: bool, terminal_width: Option, } @@ -135,7 +134,6 @@ impl Context { typechecker: TypeChecker::default(), interpreter: BytecodeInterpreter::new(), resolver: Resolver::new(module_importer), - load_currency_module_on_demand: false, terminal_width: None, } } @@ -148,21 +146,8 @@ impl Context { self.interpreter.set_debug(activate); } - pub fn load_currency_module_on_demand(&mut self, yes: bool) { - self.load_currency_module_on_demand = yes; - } - - /// Fill the currency exchange rate cache. This call is blocking. - pub fn prefetch_exchange_rates() { - let _unused = ExchangeRatesCache::fetch(); - } - - pub fn set_exchange_rates(xml_content: &str) { - ExchangeRatesCache::set_from_xml(xml_content); - } - - pub fn use_test_exchange_rates() { - ExchangeRatesCache::use_test_rates(); + pub fn set_currency_manager(currency_manager: impl CurrencyManager + 'static) { + currency::set_manager(currency_manager); } pub fn runtime_error(&self, kind: RuntimeErrorKind) -> RuntimeError { @@ -755,51 +740,29 @@ impl Context { self.prefix_transformer = prefix_transformer_old.clone(); self.typechecker = typechecker_old.clone(); - if self.load_currency_module_on_demand + if !currency::is_loaded() && let Err(NumbatError::TypeCheckError(TypeCheckError::UnknownIdentifier( _, identifier, _, ))) = &result + && currency::is_known(identifier) + && let Some(loader_code) = currency::load_manager().map_err(|_| { + 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, - )?; - - // 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); - } + let _ = self.interpret_with_settings( + &mut InterpreterSettings { + print_fn: Box::new(|_| {}), + }, + &loader_code, + CodeSource::Internal, + )?; + + // Now we try to evaluate the user expression again: + return self.interpret_with_settings(settings, code, code_source); } } diff --git a/numbat/tests/common.rs b/numbat/tests/common.rs index d2b34a14..5a49104a 100644 --- a/numbat/tests/common.rs +++ b/numbat/tests/common.rs @@ -1,8 +1,33 @@ use std::path::Path; -use numbat::{Context, NumbatError, module_importer::FileSystemImporter, resolver::CodeSource}; +use numbat::{ + Context, NumbatError, + currency::{CouldNotLoadCurrencyManager, CurrencyManager}, + module_importer::FileSystemImporter, + resolver::CodeSource, +}; use once_cell::sync::Lazy; +pub struct TestCurrencyManager {} + +impl CurrencyManager for TestCurrencyManager { + fn get_rate(&self, _: &str) -> Option { + Some(1.) + } + + fn is_known(&self, _: &str) -> bool { + true + } + + fn is_loaded(&self) -> bool { + true + } + + fn load(&self) -> Result, CouldNotLoadCurrencyManager> { + Ok(None) + } +} + pub fn get_test_context_without_prelude() -> Context { let module_path = Path::new( &std::env::var_os("CARGO_MANIFEST_DIR") @@ -13,7 +38,7 @@ pub fn get_test_context_without_prelude() -> Context { let mut importer = FileSystemImporter::default(); importer.add_path(module_path); - Context::use_test_exchange_rates(); + Context::set_currency_manager(TestCurrencyManager {}); Context::new(importer) }