1 #![forbid(unsafe_code)]
2 #![deny(clippy::all)]
3 #![deny(clippy::pedantic)]
4 #![deny(clippy::use_self)]
5 #![forbid(clippy::needless_borrow)]
6 #![forbid(unreachable_pub)]
7 #![forbid(elided_lifetimes_in_paths)]
8 #![allow(clippy::tabs_in_doc_comments)]
9
10 //! This library implements most of the features of [fend](https://github.com/printfn/fend).
11 //!
12 //! ## Example
13 //!
14 //! ```rust
15 //! extern crate fend_core;
16 //!
17 //! fn main() {
18 //! let mut context = fend_core::Context::new();
19 //! let result = fend_core::evaluate("1 + 1", &mut context).unwrap();
20 //! assert_eq!(result.get_main_result(), "2");
21 //! }
22 //! ```
23
24 mod ast;
25 mod date;
26 mod error;
27 mod eval;
28 mod format;
29 mod ident;
30 mod inline_substitutions;
31 mod interrupt;
32 /// This module is not meant to be used by other crates. It may change or be removed at any point.
33 pub mod json;
34 mod lexer;
35 mod num;
36 mod parser;
37 mod result;
38 mod scope;
39 mod serialize;
40 mod units;
41 mod value;
42
43 use std::sync::Arc;
44 use std::{collections::HashMap, fmt, io};
45
46 use error::FendError;
47 pub(crate) use eval::Attrs;
48 pub use interrupt::Interrupt;
49 use result::FResult;
50 use serialize::{Deserialize, Serialize};
51
52 /// This contains the result of a computation.
53 #[derive(PartialEq, Eq, Debug)]
54 pub struct FendResult {
55 plain_result: String,
56 span_result: Vec<Span>,
57 is_unit: bool, // is this the () type
58 attrs: eval::Attrs,
59 }
60
61 #[derive(Debug, Clone, Copy, Eq, PartialEq)]
62 #[non_exhaustive]
63 pub enum SpanKind {
64 Number,
65 BuiltInFunction,
66 Keyword,
67 String,
68 Date,
69 Whitespace,
70 Ident,
71 Boolean,
72 Other,
73 }
74
75 #[derive(Clone, Debug, PartialEq, Eq)]
76 struct Span {
77 string: String,
78 kind: SpanKind,
79 }
80
81 impl Span {
from_string(s: String) -> Self82 fn from_string(s: String) -> Self {
83 Self {
84 string: s,
85 kind: SpanKind::Other,
86 }
87 }
88 }
89
90 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
91 pub struct SpanRef<'a> {
92 string: &'a str,
93 kind: SpanKind,
94 }
95
96 impl<'a> SpanRef<'a> {
97 #[must_use]
kind(self) -> SpanKind98 pub fn kind(self) -> SpanKind {
99 self.kind
100 }
101
102 #[must_use]
string(self) -> &'a str103 pub fn string(self) -> &'a str {
104 self.string
105 }
106 }
107
108 impl FendResult {
109 /// This retrieves the main result of the computation.
110 #[must_use]
get_main_result(&self) -> &str111 pub fn get_main_result(&self) -> &str {
112 self.plain_result.as_str()
113 }
114
115 /// This retrieves the main result as a list of spans, which is useful
116 /// for colored output.
get_main_result_spans(&self) -> impl Iterator<Item = SpanRef<'_>>117 pub fn get_main_result_spans(&self) -> impl Iterator<Item = SpanRef<'_>> {
118 self.span_result.iter().map(|span| SpanRef {
119 string: &span.string,
120 kind: span.kind,
121 })
122 }
123
124 /// Returns whether or not the result is the `()` type. It can sometimes
125 /// be useful to hide these values.
126 #[must_use]
is_unit_type(&self) -> bool127 pub fn is_unit_type(&self) -> bool {
128 self.is_unit
129 }
130
empty() -> Self131 fn empty() -> Self {
132 Self {
133 plain_result: String::new(),
134 span_result: vec![],
135 is_unit: true,
136 attrs: Attrs::default(),
137 }
138 }
139
140 /// Returns whether or not the result should be outputted with a
141 /// trailing newline. This is controlled by the `@no_trailing_newline`
142 /// attribute.
143 #[must_use]
has_trailing_newline(&self) -> bool144 pub fn has_trailing_newline(&self) -> bool {
145 self.attrs.trailing_newline
146 }
147 }
148
149 #[derive(Clone, Debug)]
150 struct CurrentTimeInfo {
151 elapsed_unix_time_ms: u64,
152 timezone_offset_secs: i64,
153 }
154
155 #[derive(Clone, Debug, PartialEq, Eq)]
156 enum FCMode {
157 CelsiusFahrenheit,
158 CoulombFarad,
159 }
160
161 #[derive(Clone, Debug, PartialEq, Eq)]
162 enum OutputMode {
163 SimpleText,
164 TerminalFixedWidth,
165 }
166
167 /// An exchange rate handler.
168 pub trait ExchangeRateFn {
169 /// Returns the value of a currency relative to the base currency.
170 /// The base currency depends on your implementation. fend-core can work
171 /// with any base currency as long as it is consistent.
172 ///
173 /// # Errors
174 /// This function errors out if the currency was not found or the
175 /// conversion is impossible for any reason (HTTP request failed, etc.)
relative_to_base_currency( &self, currency: &str, ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>176 fn relative_to_base_currency(
177 &self,
178 currency: &str,
179 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>;
180 }
181
182 impl<T> ExchangeRateFn for T
183 where
184 T: Fn(&str) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>,
185 {
relative_to_base_currency( &self, currency: &str, ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>186 fn relative_to_base_currency(
187 &self,
188 currency: &str,
189 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>> {
190 self(currency)
191 }
192 }
193
194 /// This struct contains fend's current context, including some settings
195 /// as well as stored variables.
196 ///
197 /// If you're writing an interpreter it's recommended to only
198 /// instantiate this struct once so that variables and settings are
199 /// preserved, but you can also manually serialise all variables
200 /// and recreate the context for every calculation, depending on
201 /// which is easier.
202 #[derive(Clone)]
203 pub struct Context {
204 current_time: Option<CurrentTimeInfo>,
205 variables: HashMap<String, value::Value>,
206 fc_mode: FCMode,
207 random_u32: Option<fn() -> u32>,
208 output_mode: OutputMode,
209 get_exchange_rate: Option<Arc<dyn ExchangeRateFn + Send + Sync>>,
210 custom_units: Vec<(String, String, String)>,
211 }
212
213 impl fmt::Debug for Context {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 f.debug_struct("Context")
216 .field("current_time", &self.current_time)
217 .field("variables", &self.variables)
218 .field("fc_mode", &self.fc_mode)
219 .field("random_u32", &self.random_u32)
220 .field("output_mode", &self.output_mode)
221 .finish_non_exhaustive()
222 }
223 }
224
225 impl Default for Context {
default() -> Self226 fn default() -> Self {
227 Self::new()
228 }
229 }
230
231 impl Context {
232 /// Create a new context instance.
233 #[must_use]
new() -> Self234 pub fn new() -> Self {
235 Self {
236 current_time: None,
237 variables: HashMap::new(),
238 fc_mode: FCMode::CelsiusFahrenheit,
239 random_u32: None,
240 output_mode: OutputMode::SimpleText,
241 get_exchange_rate: None,
242 custom_units: vec![],
243 }
244 }
245
246 /// This method currently has no effect!
247 ///
248 /// Set the current time. This API will likely change in the future!
249 ///
250 /// The first argument (`ms_since_1970`) must be the number of elapsed milliseconds
251 /// since January 1, 1970 at midnight UTC, ignoring leap seconds in the same way
252 /// as unix time.
253 ///
254 /// The second argument (`tz_offset_secs`) is the current time zone
255 /// offset to UTC, in seconds.
set_current_time_v1(&mut self, _ms_since_1970: u64, _tz_offset_secs: i64)256 pub fn set_current_time_v1(&mut self, _ms_since_1970: u64, _tz_offset_secs: i64) {
257 // self.current_time = Some(CurrentTimeInfo {
258 // elapsed_unix_time_ms: ms_since_1970,
259 // timezone_offset_secs: tz_offset_secs,
260 // });
261 self.current_time = None;
262 }
263
264 /// Define the units `C` and `F` as coulomb and farad instead of degrees
265 /// celsius and degrees fahrenheit.
use_coulomb_and_farad(&mut self)266 pub fn use_coulomb_and_farad(&mut self) {
267 self.fc_mode = FCMode::CoulombFarad;
268 }
269
270 /// Set a random number generator
set_random_u32_fn(&mut self, random_u32: fn() -> u32)271 pub fn set_random_u32_fn(&mut self, random_u32: fn() -> u32) {
272 self.random_u32 = Some(random_u32);
273 }
274
275 /// Clear the random number generator after setting it with via [`Self::set_random_u32_fn`]
disable_rng(&mut self)276 pub fn disable_rng(&mut self) {
277 self.random_u32 = None;
278 }
279
280 /// Change the output mode to fixed-width terminal style. This enables ASCII
281 /// graphs in the output.
set_output_mode_terminal(&mut self)282 pub fn set_output_mode_terminal(&mut self) {
283 self.output_mode = OutputMode::TerminalFixedWidth;
284 }
285
serialize_variables_internal(&self, write: &mut impl io::Write) -> FResult<()>286 fn serialize_variables_internal(&self, write: &mut impl io::Write) -> FResult<()> {
287 self.variables.len().serialize(write)?;
288 for (k, v) in &self.variables {
289 k.as_str().serialize(write)?;
290 v.serialize(write)?;
291 }
292 Ok(())
293 }
294
295 /// Serializes all variables defined in this context to a stream of bytes.
296 /// Note that the specific format is NOT stable, and can change with any
297 /// minor update.
298 ///
299 /// # Errors
300 /// This function returns an error if the input cannot be serialized.
serialize_variables(&self, write: &mut impl io::Write) -> Result<(), String>301 pub fn serialize_variables(&self, write: &mut impl io::Write) -> Result<(), String> {
302 match self.serialize_variables_internal(write) {
303 Ok(()) => Ok(()),
304 Err(e) => Err(e.to_string()),
305 }
306 }
307
deserialize_variables_internal(&mut self, read: &mut impl io::Read) -> FResult<()>308 fn deserialize_variables_internal(&mut self, read: &mut impl io::Read) -> FResult<()> {
309 let len = usize::deserialize(read)?;
310 self.variables.clear();
311 self.variables.reserve(len);
312 for _ in 0..len {
313 let s = String::deserialize(read)?;
314 let v = value::Value::deserialize(read)?;
315 self.variables.insert(s, v);
316 }
317 Ok(())
318 }
319
320 /// Deserializes the given variables, replacing all prior variables in
321 /// the given context.
322 ///
323 /// # Errors
324 /// Returns an error if the input byte stream is invalid and cannot be
325 /// deserialized.
deserialize_variables(&mut self, read: &mut impl io::Read) -> Result<(), String>326 pub fn deserialize_variables(&mut self, read: &mut impl io::Read) -> Result<(), String> {
327 match self.deserialize_variables_internal(read) {
328 Ok(()) => Ok(()),
329 Err(e) => Err(e.to_string()),
330 }
331 }
332
333 /// Set a handler function for loading exchange rates.
set_exchange_rate_handler_v1<T: ExchangeRateFn + 'static + Send + Sync>( &mut self, get_exchange_rate: T, )334 pub fn set_exchange_rate_handler_v1<T: ExchangeRateFn + 'static + Send + Sync>(
335 &mut self,
336 get_exchange_rate: T,
337 ) {
338 self.get_exchange_rate = Some(Arc::new(get_exchange_rate));
339 }
340
define_custom_unit_v1( &mut self, singular: &str, plural: &str, definition: &str, attribute: &CustomUnitAttribute, )341 pub fn define_custom_unit_v1(
342 &mut self,
343 singular: &str,
344 plural: &str,
345 definition: &str,
346 attribute: &CustomUnitAttribute,
347 ) {
348 let definition_prefix = match attribute {
349 CustomUnitAttribute::None => "",
350 CustomUnitAttribute::AllowLongPrefix => "l@",
351 CustomUnitAttribute::AllowShortPrefix => "s@",
352 CustomUnitAttribute::IsLongPrefix => "lp@",
353 CustomUnitAttribute::Alias => "=",
354 };
355 self.custom_units.push((
356 singular.to_string(),
357 plural.to_string(),
358 format!("{definition_prefix}{definition}"),
359 ));
360 }
361 }
362
363 /// These attributes make is possible to change the behaviour of custom units
364 #[non_exhaustive]
365 pub enum CustomUnitAttribute {
366 /// Don't allow using prefixes with this custom unit
367 None,
368 /// Support long prefixes (e.g. `milli-`, `giga-`) with this unit
369 AllowLongPrefix,
370 /// Support short prefixes (e.g. `k` for `kilo`) with this unit
371 AllowShortPrefix,
372 /// Allow using this unit as a long prefix with another unit
373 IsLongPrefix,
374 /// This unit definition is an alias and will always be replaced with its definition.
375 Alias,
376 }
377
378 /// This function evaluates a string using the given context. Any evaluation using this
379 /// function cannot be interrupted.
380 ///
381 /// For example, passing in the string `"1 + 1"` will return a result of `"2"`.
382 ///
383 /// # Errors
384 /// It returns an error if the given string is invalid.
385 /// This may be due to parser or runtime errors.
evaluate(input: &str, context: &mut Context) -> Result<FendResult, String>386 pub fn evaluate(input: &str, context: &mut Context) -> Result<FendResult, String> {
387 evaluate_with_interrupt(input, context, &interrupt::Never)
388 }
389
evaluate_with_interrupt_internal( input: &str, context: &mut Context, int: &impl Interrupt, ) -> Result<FendResult, String>390 fn evaluate_with_interrupt_internal(
391 input: &str,
392 context: &mut Context,
393 int: &impl Interrupt,
394 ) -> Result<FendResult, String> {
395 if input.is_empty() {
396 // no or blank input: return no output
397 return Ok(FendResult::empty());
398 }
399 let (result, is_unit, attrs) = match eval::evaluate_to_spans(input, None, context, int) {
400 Ok(value) => value,
401 Err(e) => return Err(e.to_string()),
402 };
403 let mut plain_result = String::new();
404 for s in &result {
405 plain_result.push_str(&s.string);
406 }
407 Ok(FendResult {
408 plain_result,
409 span_result: result,
410 is_unit,
411 attrs,
412 })
413 }
414
415 /// This function evaluates a string using the given context and the provided
416 /// Interrupt object.
417 ///
418 /// For example, passing in the string `"1 + 1"` will return a result of `"2"`.
419 ///
420 /// # Errors
421 /// It returns an error if the given string is invalid.
422 /// This may be due to parser or runtime errors.
evaluate_with_interrupt( input: &str, context: &mut Context, int: &impl Interrupt, ) -> Result<FendResult, String>423 pub fn evaluate_with_interrupt(
424 input: &str,
425 context: &mut Context,
426 int: &impl Interrupt,
427 ) -> Result<FendResult, String> {
428 evaluate_with_interrupt_internal(input, context, int)
429 }
430
431 /// Evaluate the given string to use as a live preview.
432 ///
433 /// Unlike the normal evaluation functions, `evaluate_preview_with_interrupt`
434 /// does not mutate the passed-in context, and only returns results suitable
435 /// for displaying as a live preview: overly long output, multi-line output,
436 /// unit types etc. are all filtered out. RNG functions (e.g. `roll d6`) are
437 /// also disabled. Currency conversions (exchange rates) are disabled.
evaluate_preview_with_interrupt( input: &str, context: &mut Context, int: &impl Interrupt, ) -> FendResult438 pub fn evaluate_preview_with_interrupt(
439 input: &str,
440 context: &mut Context,
441 int: &impl Interrupt,
442 ) -> FendResult {
443 let empty = FendResult::empty();
444 // unfortunately making a complete copy of the context is necessary
445 // because we want variables to still work in multi-statement inputs
446 // like `a = 2; 5a`.
447 let context_clone = context.clone();
448 context.random_u32 = None;
449 context.get_exchange_rate = None;
450 let result = evaluate_with_interrupt_internal(input, context, int);
451 *context = context_clone;
452 let Ok(result) = result else {
453 return empty;
454 };
455 let s = result.get_main_result();
456 if s.is_empty()
457 || result.is_unit_type()
458 || s.len() > 50
459 || s.trim() == input.trim()
460 || s.contains(|c| c < ' ')
461 {
462 return empty;
463 }
464 result
465 }
466
467 #[derive(Debug)]
468 pub struct Completion {
469 display: String,
470 insert: String,
471 }
472
473 impl Completion {
474 #[must_use]
display(&self) -> &str475 pub fn display(&self) -> &str {
476 &self.display
477 }
478
479 #[must_use]
insert(&self) -> &str480 pub fn insert(&self) -> &str {
481 &self.insert
482 }
483 }
484
485 static GREEK_LOWERCASE_LETTERS: [(&str, &str); 24] = [
486 ("alpha", "α"),
487 ("beta", "β"),
488 ("gamma", "γ"),
489 ("delta", "δ"),
490 ("epsilon", "ε"),
491 ("zeta", "ζ"),
492 ("eta", "η"),
493 ("theta", "θ"),
494 ("iota", "ι"),
495 ("kappa", "κ"),
496 ("lambda", "λ"),
497 ("mu", "μ"),
498 ("nu", "ν"),
499 ("xi", "ξ"),
500 ("omicron", "ο"),
501 ("pi", "π"),
502 ("rho", "ρ"),
503 ("sigma", "σ"),
504 ("tau", "τ"),
505 ("upsilon", "υ"),
506 ("phi", "φ"),
507 ("chi", "χ"),
508 ("psi", "ψ"),
509 ("omega", "ω"),
510 ];
511 static GREEK_UPPERCASE_LETTERS: [(&str, &str); 24] = [
512 ("Alpha", "Α"),
513 ("Beta", "Β"),
514 ("Gamma", "Γ"),
515 ("Delta", "Δ"),
516 ("Epsilon", "Ε"),
517 ("Zeta", "Ζ"),
518 ("Eta", "Η"),
519 ("Theta", "Θ"),
520 ("Iota", "Ι"),
521 ("Kappa", "Κ"),
522 ("Lambda", "Λ"),
523 ("Mu", "Μ"),
524 ("Nu", "Ν"),
525 ("Xi", "Ξ"),
526 ("Omicron", "Ο"),
527 ("Pi", "Π"),
528 ("Rho", "Ρ"),
529 ("Sigma", "Σ"),
530 ("Tau", "Τ"),
531 ("Upsilon", "Υ"),
532 ("Phi", "Φ"),
533 ("Chi", "Χ"),
534 ("Psi", "Ψ"),
535 ("Omega", "Ω"),
536 ];
537
538 #[must_use]
get_completions_for_prefix(mut prefix: &str) -> (usize, Vec<Completion>)539 pub fn get_completions_for_prefix(mut prefix: &str) -> (usize, Vec<Completion>) {
540 if let Some((prefix, letter)) = prefix.rsplit_once('\\') {
541 if letter.starts_with(|c: char| c.is_ascii_alphabetic()) && letter.len() <= 7 {
542 return if letter.starts_with(|c: char| c.is_ascii_uppercase()) {
543 GREEK_UPPERCASE_LETTERS
544 } else {
545 GREEK_LOWERCASE_LETTERS
546 }
547 .iter()
548 .find(|l| l.0 == letter)
549 .map_or((0, vec![]), |l| {
550 (
551 prefix.len(),
552 vec![Completion {
553 display: prefix.to_string(),
554 insert: l.1.to_string(),
555 }],
556 )
557 });
558 }
559 }
560
561 let mut prepend = "";
562 let position = prefix.len();
563 if let Some((a, b)) = prefix.rsplit_once(' ') {
564 prepend = a;
565 prefix = b;
566 }
567
568 if prefix.is_empty() {
569 return (0, vec![]);
570 }
571 let mut res = units::get_completions_for_prefix(prefix);
572 for c in &mut res {
573 c.display.insert_str(0, prepend);
574 }
575 (position, res)
576 }
577
578 pub use inline_substitutions::substitute_inline_fend_expressions;
579
get_version_as_str() -> &'static str580 const fn get_version_as_str() -> &'static str {
581 env!("CARGO_PKG_VERSION")
582 }
583
584 /// Returns the current version of `fend-core`.
585 #[must_use]
get_version() -> String586 pub fn get_version() -> String {
587 get_version_as_str().to_string()
588 }
589
590 /// Used by unit and integration tests
591 pub mod test_utils {
592 /// A simple currency handler used in unit and integration tests. Not intended
593 /// to be used outside of `fend_core`.
594 ///
595 /// # Panics
596 /// Panics on unknown currencies
597 ///
598 /// # Errors
599 /// Panics on error, so it never needs to return Err(_)
dummy_currency_handler( currency: &str, ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>600 pub fn dummy_currency_handler(
601 currency: &str,
602 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>> {
603 Ok(match currency {
604 "EUR" | "USD" => 1.0,
605 "GBP" => 0.9,
606 "NZD" => 1.5,
607 "HKD" => 8.0,
608 "AUD" => 1.3,
609 "PLN" => 0.2,
610 "JPY" => 149.9,
611 _ => panic!("unknown currency {currency}"),
612 })
613 }
614 }
615