Skip to main content

simple_expressions/types/
coerce.rs

1use crate::types::error::{Error, Result};
2use crate::types::primitive::Primitive;
3use crate::types::value::Value;
4
5/// A coercion policy: decides how values of one type are read as another.
6///
7/// The evaluator owns the coercion boundary. Objects report their own opinion
8/// through [`Object::as_bool`](crate::types::object::Object::as_bool) and friends;
9/// a policy decides whether to consult that opinion, what the rules are for
10/// primitives, and what happens when there is no answer.
11///
12/// A policy that only cares about one conversion should delegate the rest to
13/// [`STANDARD`]:
14///
15/// ```
16/// use simple_expressions::types::coerce::{Coercions, Number, STANDARD};
17/// use simple_expressions::types::error::Result;
18/// use simple_expressions::types::value::Value;
19///
20/// /// Every value is usable as a bool; numbers keep the standard rules.
21/// struct EverythingIsTruthy;
22///
23/// impl Coercions for EverythingIsTruthy {
24///     fn to_bool(&self, v: &Value) -> Result<bool> {
25///         Ok(STANDARD.to_bool(v).unwrap_or(true))
26///     }
27///     fn to_number(&self, v: &Value) -> Result<Number> {
28///         STANDARD.to_number(v)
29///     }
30/// }
31/// ```
32///
33/// There is deliberately no string conversion here. Turning a value into text is
34/// formatting, which cannot fail and belongs to `Display`.
35pub trait Coercions {
36    fn to_bool(&self, v: &Value) -> Result<bool>;
37    fn to_number(&self, v: &Value) -> Result<Number>;
38}
39
40/// What a policy found when asked for a number.
41///
42/// Returning this rather than an `f64` is what lets integer operands produce
43/// integer results: the policy reports which kind it found, and the operator
44/// applies its own promotion rules. Asking "is it an int?" by probing a separate
45/// `to_int` would instead make the *operator* guess, and a truncating policy
46/// would silently turn `1.5 * 2` into `2`.
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub enum Number {
49    Int(i64),
50    Float(f64),
51}
52
53impl Number {
54    pub fn as_f64(&self) -> f64 {
55        match self {
56            Number::Int(i) => *i as f64,
57            Number::Float(f) => *f,
58        }
59    }
60}
61
62impl From<Number> for Value {
63    fn from(n: Number) -> Self {
64        match n {
65            Number::Int(i) => Value::Primitive(Primitive::Int(i)),
66            Number::Float(f) => Value::Primitive(Primitive::Float(f)),
67        }
68    }
69}
70
71fn not_coercible(v: &Value, target: &'static str) -> Error {
72    Error::NotCoercible { type_name: v.type_name().into(), target }
73}
74
75/// The default policy, matching the language's built-in behavior.
76pub struct StandardCoercions;
77
78/// A shared [`StandardCoercions`], usable wherever a `&'static dyn Coercions` is wanted.
79pub static STANDARD: StandardCoercions = StandardCoercions;
80
81impl Coercions for StandardCoercions {
82    fn to_bool(&self, v: &Value) -> Result<bool> {
83        let b = match v {
84            Value::Primitive(Primitive::Bool(b)) => Some(*b),
85            Value::Primitive(Primitive::Int(i)) => Some(*i != 0),
86            Value::Primitive(Primitive::Float(f)) => Some(*f != 0.0),
87            Value::Primitive(Primitive::Str(s)) if s == "true" || s == "false" => Some(s == "true"),
88            Value::Primitive(Primitive::Str(_)) => None,
89            Value::Object(obj) => obj.as_bool(),
90        };
91        b.ok_or_else(|| not_coercible(v, "bool"))
92    }
93
94    fn to_number(&self, v: &Value) -> Result<Number> {
95        let n = match v {
96            Value::Primitive(Primitive::Int(i)) => Some(Number::Int(*i)),
97            Value::Primitive(Primitive::Float(f)) => Some(Number::Float(*f)),
98            Value::Primitive(_) => None,
99            // an object that reports an integer keeps its integer-ness
100            Value::Object(obj) => obj.as_int().map(Number::Int).or_else(|| obj.as_float().map(Number::Float)),
101        };
102        n.ok_or_else(|| not_coercible(v, "number"))
103    }
104}
105
106/// Ambient state handed to [`Object::call`](crate::types::object::Object::call).
107///
108/// Functions receive this at call time rather than capturing it at construction
109/// time, which is what keeps it off the rest of the `Object` trait. Fields are
110/// private so the context can grow (fuel, recursion depth, ...) without another
111/// breaking change.
112#[derive(Clone, Copy)]
113pub struct Context<'a> {
114    coercions: &'a dyn Coercions,
115}
116
117impl<'a> Context<'a> {
118    pub fn new(coercions: &'a dyn Coercions) -> Self {
119        Self { coercions }
120    }
121
122    /// A context using [`STANDARD`].
123    pub fn standard() -> Context<'static> {
124        Context { coercions: &STANDARD }
125    }
126
127    pub fn to_bool(&self, v: &Value) -> Result<bool> {
128        self.coercions.to_bool(v)
129    }
130    pub fn to_number(&self, v: &Value) -> Result<Number> {
131        self.coercions.to_number(v)
132    }
133}
134
135impl Default for Context<'static> {
136    fn default() -> Self {
137        Context::standard()
138    }
139}
140
141/// A strict policy: only an actual bool is a bool, and only an actual number is
142/// a number.
143pub struct StrictCoercions;
144
145impl Coercions for StrictCoercions {
146    fn to_bool(&self, v: &Value) -> Result<bool> {
147        match v {
148            Value::Primitive(Primitive::Bool(b)) => Ok(*b),
149            _ => Err(not_coercible(v, "bool")),
150        }
151    }
152
153    fn to_number(&self, v: &Value) -> Result<Number> {
154        match v {
155            Value::Primitive(Primitive::Int(i)) => Ok(Number::Int(*i)),
156            Value::Primitive(Primitive::Float(f)) => Ok(Number::Float(*f)),
157            _ => Err(not_coercible(v, "number")),
158        }
159    }
160}