Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions numbat-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion numbat-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)
Expand Down
145 changes: 104 additions & 41 deletions numbat/src/currency.rs
Original file line number Diff line number Diff line change
@@ -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<f64>;
fn is_known(&self, currency: &str) -> bool;
fn is_loaded(&self) -> bool;
fn load(&self) -> Result<Option<String>, CouldNotLoadCurrencyManager>;
}

static EXCHANGE_RATES: OnceLock<Mutex<Option<ExchangeRates>>> = 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<f64> {
None
}

pub fn get_rate(&self, currency: &str) -> Option<f64> {
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<Option<String>, CouldNotLoadCurrencyManager> {
Ok(None)
}
}

#[derive(Debug, Default)]
pub struct OnDemandCurrencyManager {
loaded: AtomicBool,
rates: OnceLock<Option<ExchangeRates>>,
}

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<f64> {
self.rates.get()?.as_ref()?.get(currency).copied()
}

#[cfg(feature = "fetch-exchangerates")]
pub fn fetch() -> MutexGuard<'static, Option<ExchangeRates>> {
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(&currency)
}

#[cfg(not(feature = "fetch-exchangerates"))]
pub fn fetch() -> MutexGuard<'static, Option<ExchangeRates>> {
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<Option<String>, 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<Mutex<Box<dyn CurrencyManager>>> =
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<f64> {
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<Option<String>, 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<ExchangeRates> {
None
}
5 changes: 1 addition & 4 deletions numbat/src/ffi/currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +14,5 @@ pub fn exchange_rate(
) -> Result<Value, Box<RuntimeErrorKind>> {
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))
}
79 changes: 21 additions & 58 deletions numbat/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -114,7 +114,6 @@ pub struct Context {
typechecker: TypeChecker,
interpreter: BytecodeInterpreter,
resolver: Resolver,
load_currency_module_on_demand: bool,
terminal_width: Option<usize>,
}

Expand All @@ -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,
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
}

Expand Down
29 changes: 27 additions & 2 deletions numbat/tests/common.rs
Original file line number Diff line number Diff line change
@@ -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<f64> {
Some(1.)
}

fn is_known(&self, _: &str) -> bool {
true
}

fn is_loaded(&self) -> bool {
true
}

fn load(&self) -> Result<Option<String>, CouldNotLoadCurrencyManager> {
Ok(None)
}
}

pub fn get_test_context_without_prelude() -> Context {
let module_path = Path::new(
&std::env::var_os("CARGO_MANIFEST_DIR")
Expand All @@ -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)
}

Expand Down
Loading