Browse Source

Add currencies and exchange rates, closes #47

David Peter 2 years ago
parent
commit
d23ec725fd

+ 0 - 3
modules/units/currency.nbt

@@ -2,6 +2,3 @@ dimension Money
 
 @aliases(euros, EUR, €: short)
 unit euro: Money
-
-@aliases(dollars, USD, $: short)
-unit dollar: Money

+ 33 - 0
modules/units/non_euro_currencies.nbt

@@ -0,0 +1,33 @@
+# This module is currently not part of the prelude, because the 'exchange_rate_XXX' calls
+# are blocking. In the CLI application, we do however load this module asynchronously after
+# prefetching the exchange rates.
+
+# TODO: if we ever support strings in the language, use 'exchange_rate("USD")', … instead
+fn exchange_rate_USD() -> Scalar
+fn exchange_rate_JPY() -> Scalar
+fn exchange_rate_GBP() -> Scalar
+fn exchange_rate_CNY() -> Scalar
+fn exchange_rate_AUD() -> Scalar
+fn exchange_rate_CAD() -> Scalar
+fn exchange_rate_CHF() -> Scalar
+
+@aliases(dollars, USD, $: short)
+unit dollar: Money = EUR / exchange_rate_USD()
+
+@aliases(yens, JPY, ¥: short, 円)
+unit yen: Money = EUR / exchange_rate_JPY()
+
+@aliases(pound_sterling, GBP, £: short)
+unit british_pound: Money = EUR / exchange_rate_GBP()
+
+@aliases(CNY: short, 元)
+unit renminbi: Money = EUR / exchange_rate_CNY()
+
+@aliases(australian_dollars, AUD: short, A$)
+unit australian_dollar: Money = EUR / exchange_rate_AUD()
+
+@aliases(canadian_dollars, CAD: short, C$)
+unit canadian_dollar: Money = EUR / exchange_rate_CAD()
+
+@aliases(swiss_francs, CHF: short)
+unit swiss_franc: Money = EUR / exchange_rate_CHF()

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

@@ -22,9 +22,9 @@ use rustyline::{
 };
 use rustyline::{EventHandler, Highlighter, KeyCode, KeyEvent, Modifiers};
 
-use std::fs;
 use std::path::PathBuf;
 use std::sync::{Arc, Mutex};
+use std::{fs, thread};
 
 use crate::ansi_formatter::ansi_format;
 
@@ -144,6 +144,20 @@ impl Cli {
             }
         }
 
+        let ctx = self.context.clone();
+        thread::spawn(move || {
+            numbat::Context::fetch_exchange_rates();
+
+            // After pre-fetching the exchange rates, we can load the 'non_euro_currencies'
+            // module without blocking the context for long. This allows us to have fast
+            // startup times of the CLI application, but still have currency units available
+            // after a short delay (the limiting factor is the HTTP request).
+            ctx.lock()
+                .unwrap()
+                .interpret("use units::non_euro_currencies", CodeSource::Text)
+                .ok();
+        });
+
         let (code, code_source): (Option<String>, CodeSource) =
             if let Some(ref path) = self.args.file {
                 (

+ 1 - 0
numbat/Cargo.toml

@@ -12,6 +12,7 @@ num-rational = "0.4"
 num-traits = "0.2"
 codespan-reporting = "0.11"
 strsim = "0.10.0"
+numbat-exchange-rates = { path = "../numbat-exchange-rates" }
 
 [dev-dependencies]
 approx = "0.5"

+ 25 - 0
numbat/src/currency.rs

@@ -0,0 +1,25 @@
+use std::sync::{Mutex, MutexGuard, OnceLock};
+
+use numbat_exchange_rates::{fetch_exchange_rates, ExchangeRates};
+
+static EXCHANGE_RATES: OnceLock<Mutex<ExchangeRates>> = OnceLock::new();
+
+pub struct ExchangeRatesCache {}
+
+impl ExchangeRatesCache {
+    pub fn new() -> Self {
+        Self {}
+    }
+
+    pub fn get_rate(&self, currency: &str) -> Option<f64> {
+        let rates = self.fetch();
+        rates.get(currency).cloned()
+    }
+
+    pub fn fetch(&self) -> MutexGuard<ExchangeRates> {
+        EXCHANGE_RATES
+            .get_or_init(|| Mutex::new(fetch_exchange_rates().unwrap_or_default()))
+            .lock()
+            .unwrap()
+    }
+}

+ 72 - 55
numbat/src/ffi.rs

@@ -2,6 +2,7 @@ use std::collections::HashMap;
 
 use std::sync::OnceLock;
 
+use crate::currency::ExchangeRatesCache;
 use crate::interpreter::RuntimeError;
 use crate::{ast::ProcedureKind, quantity::Quantity};
 
@@ -9,13 +10,11 @@ type ControlFlow = std::ops::ControlFlow<RuntimeError>;
 
 pub(crate) type ArityRange = std::ops::RangeInclusive<usize>;
 
-#[derive(Clone)]
 pub(crate) enum Callable {
-    Function(fn(&[Quantity]) -> Quantity),
+    Function(Box<dyn Fn(&[Quantity]) -> Quantity + Send + Sync>),
     Procedure(fn(&[Quantity]) -> ControlFlow),
 }
 
-#[derive(Clone)]
 pub(crate) struct ForeignFunction {
     pub(crate) name: String,
     pub(crate) arity: ArityRange,
@@ -23,7 +22,7 @@ pub(crate) struct ForeignFunction {
 }
 
 static FFI_PROCEDURES: OnceLock<HashMap<ProcedureKind, ForeignFunction>> = OnceLock::new();
-static FFI_FUNCTIONS: OnceLock<HashMap<&'static str, ForeignFunction>> = OnceLock::new();
+static FFI_FUNCTIONS: OnceLock<HashMap<String, ForeignFunction>> = OnceLock::new();
 
 pub(crate) fn procedures() -> &'static HashMap<ProcedureKind, ForeignFunction> {
     FFI_PROCEDURES.get_or_init(|| {
@@ -50,215 +49,226 @@ pub(crate) fn procedures() -> &'static HashMap<ProcedureKind, ForeignFunction> {
     })
 }
 
-pub(crate) fn functions() -> &'static HashMap<&'static str, ForeignFunction> {
+pub(crate) fn functions() -> &'static HashMap<String, ForeignFunction> {
     FFI_FUNCTIONS.get_or_init(|| {
         let mut m = HashMap::new();
 
         m.insert(
-            "abs",
+            "abs".to_string(),
             ForeignFunction {
                 name: "abs".into(),
                 arity: 1..=1,
-                callable: Callable::Function(abs),
+                callable: Callable::Function(Box::new(abs)),
             },
         );
         m.insert(
-            "round",
+            "round".to_string(),
             ForeignFunction {
                 name: "round".into(),
                 arity: 1..=1,
-                callable: Callable::Function(round),
+                callable: Callable::Function(Box::new(round)),
             },
         );
         m.insert(
-            "floor",
+            "floor".to_string(),
             ForeignFunction {
                 name: "floor".into(),
                 arity: 1..=1,
-                callable: Callable::Function(floor),
+                callable: Callable::Function(Box::new(floor)),
             },
         );
         m.insert(
-            "ceil",
+            "ceil".to_string(),
             ForeignFunction {
                 name: "ceil".into(),
                 arity: 1..=1,
-                callable: Callable::Function(ceil),
+                callable: Callable::Function(Box::new(ceil)),
             },
         );
 
         m.insert(
-            "sin",
+            "sin".to_string(),
             ForeignFunction {
                 name: "sin".into(),
                 arity: 1..=1,
-                callable: Callable::Function(sin),
+                callable: Callable::Function(Box::new(sin)),
             },
         );
         m.insert(
-            "cos",
+            "cos".to_string(),
             ForeignFunction {
                 name: "cos".into(),
                 arity: 1..=1,
-                callable: Callable::Function(cos),
+                callable: Callable::Function(Box::new(cos)),
             },
         );
         m.insert(
-            "tan",
+            "tan".to_string(),
             ForeignFunction {
                 name: "tan".into(),
                 arity: 1..=1,
-                callable: Callable::Function(tan),
+                callable: Callable::Function(Box::new(tan)),
             },
         );
         m.insert(
-            "asin",
+            "asin".to_string(),
             ForeignFunction {
                 name: "asin".into(),
                 arity: 1..=1,
-                callable: Callable::Function(asin),
+                callable: Callable::Function(Box::new(asin)),
             },
         );
         m.insert(
-            "acos",
+            "acos".to_string(),
             ForeignFunction {
                 name: "acos".into(),
                 arity: 1..=1,
-                callable: Callable::Function(acos),
+                callable: Callable::Function(Box::new(acos)),
             },
         );
         m.insert(
-            "atan",
+            "atan".to_string(),
             ForeignFunction {
                 name: "atan".into(),
                 arity: 1..=1,
-                callable: Callable::Function(atan),
+                callable: Callable::Function(Box::new(atan)),
             },
         );
         m.insert(
-            "atan2",
+            "atan2".to_string(),
             ForeignFunction {
                 name: "atan2".into(),
                 arity: 2..=2,
-                callable: Callable::Function(atan2),
+                callable: Callable::Function(Box::new(atan2)),
             },
         );
 
         m.insert(
-            "sinh",
+            "sinh".to_string(),
             ForeignFunction {
                 name: "sinh".into(),
                 arity: 1..=1,
-                callable: Callable::Function(sinh),
+                callable: Callable::Function(Box::new(sinh)),
             },
         );
         m.insert(
-            "cosh",
+            "cosh".to_string(),
             ForeignFunction {
                 name: "cosh".into(),
                 arity: 1..=1,
-                callable: Callable::Function(cosh),
+                callable: Callable::Function(Box::new(cosh)),
             },
         );
         m.insert(
-            "tanh",
+            "tanh".to_string(),
             ForeignFunction {
                 name: "tanh".into(),
                 arity: 1..=1,
-                callable: Callable::Function(tanh),
+                callable: Callable::Function(Box::new(tanh)),
             },
         );
         m.insert(
-            "asinh",
+            "asinh".to_string(),
             ForeignFunction {
                 name: "asinh".into(),
                 arity: 1..=1,
-                callable: Callable::Function(asinh),
+                callable: Callable::Function(Box::new(asinh)),
             },
         );
         m.insert(
-            "acosh",
+            "acosh".to_string(),
             ForeignFunction {
                 name: "acosh".into(),
                 arity: 1..=1,
-                callable: Callable::Function(acosh),
+                callable: Callable::Function(Box::new(acosh)),
             },
         );
         m.insert(
-            "atanh",
+            "atanh".to_string(),
             ForeignFunction {
                 name: "atanh".into(),
                 arity: 1..=1,
-                callable: Callable::Function(atanh),
+                callable: Callable::Function(Box::new(atanh)),
             },
         );
 
         m.insert(
-            "mod",
+            "mod".to_string(),
             ForeignFunction {
                 name: "mod".into(),
                 arity: 2..=2,
-                callable: Callable::Function(mod_),
+                callable: Callable::Function(Box::new(mod_)),
             },
         );
         m.insert(
-            "exp",
+            "exp".to_string(),
             ForeignFunction {
                 name: "exp".into(),
                 arity: 1..=1,
-                callable: Callable::Function(exp),
+                callable: Callable::Function(Box::new(exp)),
             },
         );
         m.insert(
-            "ln",
+            "ln".to_string(),
             ForeignFunction {
                 name: "ln".into(),
                 arity: 1..=1,
-                callable: Callable::Function(ln),
+                callable: Callable::Function(Box::new(ln)),
             },
         );
         m.insert(
-            "log10",
+            "log10".to_string(),
             ForeignFunction {
                 name: "log10".into(),
                 arity: 1..=1,
-                callable: Callable::Function(log10),
+                callable: Callable::Function(Box::new(log10)),
             },
         );
         m.insert(
-            "log2",
+            "log2".to_string(),
             ForeignFunction {
                 name: "log2".into(),
                 arity: 1..=1,
-                callable: Callable::Function(log2),
+                callable: Callable::Function(Box::new(log2)),
             },
         );
 
         m.insert(
-            "mean",
+            "mean".to_string(),
             ForeignFunction {
                 name: "mean".into(),
                 arity: 1..=usize::MAX,
-                callable: Callable::Function(mean),
+                callable: Callable::Function(Box::new(mean)),
             },
         );
         m.insert(
-            "maximum",
+            "maximum".to_string(),
             ForeignFunction {
                 name: "maximum".into(),
                 arity: 1..=usize::MAX,
-                callable: Callable::Function(maximum),
+                callable: Callable::Function(Box::new(maximum)),
             },
         );
         m.insert(
-            "minimum",
+            "minimum".to_string(),
             ForeignFunction {
                 name: "minimum".into(),
                 arity: 1..=usize::MAX,
-                callable: Callable::Function(minimum),
+                callable: Callable::Function(Box::new(minimum)),
             },
         );
 
+        for currency in ["USD", "JPY", "GBP", "CNY", "AUD", "CAD", "CHF"] {
+            m.insert(
+                format!("exchange_rate_{currency}"),
+                ForeignFunction {
+                    name: format!("exchange_rate_{currency}"),
+                    arity: 0..=0,
+                    callable: Callable::Function(exchange_rate(currency)),
+                },
+            );
+        }
+
         m
     })
 }
@@ -515,3 +525,10 @@ fn minimum(args: &[Quantity]) -> Quantity {
         output_unit.clone(),
     )
 }
+
+fn exchange_rate(rate: &'static str) -> Box<dyn Fn(&[Quantity]) -> Quantity + Send + Sync> {
+    Box::new(|_args: &[Quantity]| -> Quantity {
+        let exchange_rates = ExchangeRatesCache::new();
+        Quantity::from_scalar(exchange_rates.get_rate(rate).unwrap_or(f64::NAN))
+    })
+}

+ 9 - 1
numbat/src/lib.rs

@@ -1,6 +1,7 @@
 mod arithmetic;
 mod ast;
 mod bytecode_interpreter;
+mod currency;
 mod decorator;
 pub mod diagnostic;
 mod dimension;
@@ -27,6 +28,7 @@ mod unit_registry;
 mod vm;
 
 use bytecode_interpreter::BytecodeInterpreter;
+use currency::ExchangeRatesCache;
 use diagnostic::ErrorDiagnostic;
 use interpreter::{Interpreter, RuntimeError};
 use prefix_transformer::Transformer;
@@ -67,7 +69,7 @@ pub struct Context {
 }
 
 impl Context {
-    pub fn new(module_importer: impl ModuleImporter + 'static) -> Self {
+    pub fn new(module_importer: impl ModuleImporter + Send + 'static) -> Self {
         Context {
             prefix_transformer: Transformer::new(),
             typechecker: TypeChecker::default(),
@@ -84,6 +86,12 @@ impl Context {
         self.interpreter.set_debug(activate);
     }
 
+    /// Fill the currency exchange rate cache. This call is blocking.
+    pub fn fetch_exchange_rates() {
+        let cache = ExchangeRatesCache::new();
+        let _unused = cache.fetch();
+    }
+
     pub fn variable_names(&self) -> &[String] {
         &self.prefix_transformer.variable_names
     }

+ 2 - 2
numbat/src/resolver.rs

@@ -41,13 +41,13 @@ pub enum ResolverError {
 type Result<T> = std::result::Result<T, ResolverError>;
 
 pub(crate) struct Resolver {
-    importer: Box<dyn ModuleImporter>,
+    importer: Box<dyn ModuleImporter + Send>,
     code_sources: Vec<CodeSource>,
     pub files: SimpleFiles<String, String>,
 }
 
 impl Resolver {
-    pub(crate) fn new(importer: impl ModuleImporter + 'static) -> Self {
+    pub(crate) fn new(importer: impl ModuleImporter + Send + 'static) -> Self {
         Self {
             importer: Box::new(importer),
             code_sources: vec![],

+ 3 - 3
numbat/src/vm.rs

@@ -167,7 +167,7 @@ pub struct Vm {
     globals: HashMap<String, Quantity>,
 
     /// List of registered native/foreign functions
-    ffi_callables: Vec<ForeignFunction>,
+    ffi_callables: Vec<&'static ForeignFunction>,
 
     /// The call stack
     frames: Vec<CallFrame>,
@@ -188,7 +188,7 @@ impl Vm {
             constants: vec![],
             global_identifiers: vec![],
             globals: HashMap::new(),
-            ffi_callables: ffi::procedures().iter().map(|(_, ff)| ff.clone()).collect(),
+            ffi_callables: ffi::procedures().iter().map(|(_, ff)| ff).collect(),
             frames: vec![CallFrame::root()],
             stack: vec![],
             debug: false,
@@ -503,7 +503,7 @@ impl Vm {
                     }
                     args.reverse(); // TODO: use a deque?
 
-                    match self.ffi_callables[function_idx].callable {
+                    match &self.ffi_callables[function_idx].callable {
                         Callable::Function(function) => {
                             let result = (function)(&args[..]);
                             self.push(result);