Skip to main content

doge_runtime/
builtins.rs

1use std::io::Write;
2
3use bigdecimal::{BigDecimal, FromPrimitive, RoundingMode, ToPrimitive};
4use num_bigint::BigInt;
5
6use crate::error::{DogeError, DogeResult};
7use crate::value::Value;
8
9/// `bark x` — print a value on its own line and evaluate to `none`, so
10/// `bark` can sit anywhere an expression is expected.
11pub fn bark(v: &Value) -> Value {
12    println!("{v}");
13    Value::None
14}
15
16/// `gib()` / `gib("prompt")` — read one line from standard input. An optional
17/// prompt, which must be a Str, is written without a trailing newline and flushed
18/// first. The returned Str has its trailing newline stripped; at end of input the
19/// result is `none`. A stdin read failure is a catchable IOError.
20pub fn gib(prompt: Option<&Value>) -> DogeResult {
21    if let Some(p) = prompt {
22        let text = match p {
23            Value::Str(s) => s,
24            _ => {
25                return Err(DogeError::type_error(format!(
26                    "gib needs a Str prompt, got {}",
27                    p.describe()
28                )))
29            }
30        };
31        print!("{text}");
32        let _ = std::io::stdout().flush();
33    }
34    let mut line = String::new();
35    match std::io::stdin().read_line(&mut line) {
36        Ok(0) => Ok(Value::None),
37        Ok(_) => {
38            let line = line.strip_suffix('\n').unwrap_or(&line);
39            let line = line.strip_suffix('\r').unwrap_or(line);
40            Ok(Value::str(line))
41        }
42        Err(err) => Err(DogeError::io_error(format!("could not read input: {err}"))),
43    }
44}
45
46/// `len(x)` — character count for a Str, byte count for a Bytes, element count
47/// for a List or Dict. Anything else is a catchable type error.
48pub fn len(v: &Value) -> DogeResult {
49    match v {
50        Value::Str(s) => Ok(Value::int(s.chars().count())),
51        Value::Bytes(b) => Ok(Value::int(b.len())),
52        Value::List(items) => Ok(Value::int(items.borrow().len())),
53        Value::Dict(entries) => Ok(Value::int(entries.borrow().len())),
54        _ => Err(crate::error::DogeError::type_error(format!(
55            "cannot take the len of {}",
56            v.describe()
57        ))),
58    }
59}
60
61/// `str(x)` — the value's printed form as a Str. Always succeeds.
62pub fn to_str(v: &Value) -> Value {
63    Value::str(v.to_string())
64}
65
66/// String interpolation (`"a {b} c"`) — join each part's display form, the same
67/// text `bark`/`str` would show, into one Str. Always succeeds.
68pub fn interp(parts: &[Value]) -> Value {
69    let mut out = String::new();
70    for part in parts {
71        out.push_str(&part.to_string());
72    }
73    Value::str(out)
74}
75
76/// `int(x)` — Int unchanged, Float and Decimal truncated toward zero, Bool to
77/// 0/1, a Str parsed as a whole number (of any size). A Str that isn't a number
78/// is a catchable `ValueError`; other types are a `TypeError`.
79pub fn to_int(v: &Value) -> DogeResult {
80    match v {
81        Value::Int(n) => Ok(Value::Int(n.clone())),
82        Value::Float(f) => BigInt::from_f64(f.trunc())
83            .map(Value::Int)
84            .ok_or_else(|| DogeError::value_error(format!("cannot turn {f} into an Int"))),
85        Value::Decimal(d) => {
86            let (digits, _) = d
87                .with_scale_round(0, RoundingMode::Down)
88                .into_bigint_and_exponent();
89            Ok(Value::Int(digits))
90        }
91        Value::Bool(b) => Ok(Value::int(i64::from(*b))),
92        Value::Str(s) => s.trim().parse::<BigInt>().map(Value::Int).map_err(|_| {
93            crate::error::DogeError::value_error(format!("cannot turn {s:?} into an Int"))
94        }),
95        _ => Err(crate::error::DogeError::type_error(format!(
96            "cannot turn {} into an Int",
97            v.describe()
98        ))),
99    }
100}
101
102/// `float(x)` — Int and Bool widen to Float, Decimal rounds to the nearest Float,
103/// Float is unchanged, a numeric Str is parsed. A non-numeric Str is a catchable
104/// `ValueError`; other types are a `TypeError`.
105pub fn to_float(v: &Value) -> DogeResult {
106    match v {
107        Value::Int(n) => n
108            .to_f64()
109            .map(Value::Float)
110            .ok_or_else(|| DogeError::value_error(format!("{n} is too large for a Float"))),
111        Value::Float(f) => Ok(Value::Float(*f)),
112        Value::Decimal(d) => d
113            .to_f64()
114            .map(Value::Float)
115            .ok_or_else(|| DogeError::value_error(format!("{d} is too large for a Float"))),
116        Value::Bool(b) => Ok(Value::Float(if *b { 1.0 } else { 0.0 })),
117        Value::Str(s) => s.trim().parse::<f64>().map(Value::Float).map_err(|_| {
118            crate::error::DogeError::value_error(format!("cannot turn {s:?} into a Float"))
119        }),
120        _ => Err(crate::error::DogeError::type_error(format!(
121            "cannot turn {} into a Float",
122            v.describe()
123        ))),
124    }
125}
126
127/// `dec(x)` — an exact `Decimal`. An Int converts exactly; a Bool to 0/1; a
128/// numeric Str parses exactly (`dec("0.10")`); a Float converts via its shortest
129/// decimal form, so `dec(0.1)` is exactly `0.1` and not the binary noise a raw
130/// cast would carry. A Decimal is returned unchanged. A non-numeric Str is a
131/// catchable `ValueError`; other types are a `TypeError`.
132pub fn to_decimal(v: &Value) -> DogeResult {
133    match v {
134        Value::Decimal(_) => Ok(v.clone()),
135        Value::Int(n) => Ok(Value::decimal(BigDecimal::from(n.clone()))),
136        Value::Bool(b) => Ok(Value::decimal(BigDecimal::from(i64::from(*b)))),
137        Value::Float(f) => format!("{f}")
138            .parse::<BigDecimal>()
139            .map(Value::decimal)
140            .map_err(|_| DogeError::value_error(format!("cannot turn {f} into a Decimal"))),
141        Value::Str(s) => s
142            .trim()
143            .parse::<BigDecimal>()
144            .map(Value::decimal)
145            .map_err(|_| DogeError::value_error(format!("cannot turn {s:?} into a Decimal"))),
146        _ => Err(DogeError::type_error(format!(
147            "cannot turn {} into a Decimal",
148            v.describe()
149        ))),
150    }
151}
152
153/// `bytes(x)` — raw bytes from a value. A Str is UTF-8 encoded; a List of Ints
154/// becomes those bytes, each of which must be in `0..=255` (a catchable
155/// `ValueError` otherwise); a Bytes is returned unchanged. Any other type is a
156/// catchable `TypeError`.
157pub fn to_bytes(v: &Value) -> DogeResult {
158    match v {
159        Value::Bytes(_) => Ok(v.clone()),
160        Value::Str(s) => Ok(Value::bytes(s.as_bytes())),
161        Value::List(items) => {
162            let items = items.borrow();
163            let mut out = Vec::with_capacity(items.len());
164            for item in items.iter() {
165                match item {
166                    Value::Int(n) => {
167                        let byte = n.to_u8().ok_or_else(|| {
168                            crate::error::DogeError::value_error(format!(
169                                "bytes needs each Int in 0..=255, got {n}"
170                            ))
171                        })?;
172                        out.push(byte);
173                    }
174                    other => {
175                        return Err(crate::error::DogeError::type_error(format!(
176                            "bytes needs a List of Ints, got {} in the List",
177                            other.describe()
178                        )))
179                    }
180                }
181            }
182            Ok(Value::bytes(out))
183        }
184        _ => Err(crate::error::DogeError::type_error(format!(
185            "cannot turn {} into Bytes",
186            v.describe()
187        ))),
188    }
189}
190
191/// `range(start, end)` — the Ints `start, start+1, …, end-1` as an eager List.
192/// When `end <= start` the List is naturally empty. Both arguments must be Int;
193/// anything else is a catchable type error. A bound too large to be a machine
194/// index is a catchable value error (the List could never fit in memory anyway).
195/// The one-argument Doge form `range(n)` is compiled as `range(0, n)`, so the
196/// runtime has one signature.
197pub fn range(start: &Value, end: &Value) -> DogeResult {
198    match (start, end) {
199        (Value::Int(a), Value::Int(b)) => {
200            let a = a.to_i64().ok_or_else(|| range_bound_too_large(a))?;
201            let b = b.to_i64().ok_or_else(|| range_bound_too_large(b))?;
202            Ok(Value::list((a..b).map(Value::int).collect()))
203        }
204        (Value::Int(_), other) | (other, _) => Err(crate::error::DogeError::type_error(format!(
205            "range needs Int bounds, got {}",
206            other.describe()
207        ))),
208    }
209}
210
211fn range_bound_too_large(n: &BigInt) -> DogeError {
212    DogeError::value_error(format!("range bound {n} is too large"))
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::error::ErrorKind;
219    use crate::ops::values_equal;
220
221    #[test]
222    fn bark_returns_none() {
223        assert!(matches!(bark(&Value::str("much hello")), Value::None));
224    }
225
226    #[test]
227    fn len_counts_characters_and_elements() {
228        // Char count, not byte count — 'é' is one character.
229        assert!(values_equal(
230            &len(&Value::str("héllo")).unwrap(),
231            &Value::int(5)
232        ));
233        assert!(values_equal(
234            &len(&Value::list(vec![Value::int(1), Value::int(2)])).unwrap(),
235            &Value::int(2)
236        ));
237        assert_eq!(len(&Value::int(3)).unwrap_err().kind, ErrorKind::TypeError);
238    }
239
240    #[test]
241    fn interp_joins_display_forms() {
242        let parts = [
243            Value::str("age "),
244            Value::int(7),
245            Value::str(", "),
246            Value::None,
247        ];
248        assert!(matches!(interp(&parts), Value::Str(s) if &*s == "age 7, none"));
249        // Nested Strs embed bare, matching bark/str, not the quoted repr.
250        let nested = [Value::str("["), Value::str("hi"), Value::str("]")];
251        assert!(matches!(interp(&nested), Value::Str(s) if &*s == "[hi]"));
252        assert!(matches!(interp(&[]), Value::Str(s) if s.is_empty()));
253    }
254
255    #[test]
256    fn conversions_round_trip() {
257        assert!(matches!(to_str(&Value::int(7)), Value::Str(s) if &*s == "7"));
258        assert!(values_equal(
259            &to_int(&Value::Float(3.9)).unwrap(),
260            &Value::int(3)
261        ));
262        assert!(values_equal(
263            &to_int(&Value::str(" 42 ")).unwrap(),
264            &Value::int(42)
265        ));
266        assert!(matches!(to_float(&Value::int(4)).unwrap(), Value::Float(f) if f == 4.0));
267    }
268
269    #[test]
270    fn int_parses_arbitrary_precision_strings() {
271        // A value far past i64 parses exactly as an Int.
272        let big = "123456789012345678901234567890";
273        let parsed = to_int(&Value::str(big)).unwrap();
274        assert_eq!(parsed.to_string(), big);
275    }
276
277    #[test]
278    fn dec_is_exact_from_str_int_and_float() {
279        assert_eq!(to_decimal(&Value::str("0.10")).unwrap().to_string(), "0.10");
280        assert!(values_equal(
281            &to_decimal(&Value::int(3)).unwrap(),
282            &to_decimal(&Value::str("3")).unwrap()
283        ));
284        // A Float converts via its shortest decimal form, not the binary noise.
285        assert_eq!(to_decimal(&Value::Float(0.1)).unwrap().to_string(), "0.1");
286        assert_eq!(
287            to_decimal(&Value::str("nope")).unwrap_err().kind,
288            ErrorKind::ValueError
289        );
290    }
291
292    #[test]
293    fn int_truncates_a_decimal_toward_zero() {
294        assert!(values_equal(
295            &to_int(&to_decimal(&Value::str("3.9")).unwrap()).unwrap(),
296            &Value::int(3)
297        ));
298        assert!(values_equal(
299            &to_int(&to_decimal(&Value::str("-3.9")).unwrap()).unwrap(),
300            &Value::int(-3)
301        ));
302    }
303
304    #[test]
305    fn bad_conversions_are_catchable_value_errors() {
306        assert_eq!(
307            to_int(&Value::str("dog")).unwrap_err().kind,
308            ErrorKind::ValueError
309        );
310        assert_eq!(
311            to_float(&Value::str("woof")).unwrap_err().kind,
312            ErrorKind::ValueError
313        );
314    }
315
316    #[test]
317    fn to_bytes_from_str_list_and_bytes() {
318        // A Str UTF-8-encodes: 'é' is the two bytes 0xC3 0xA9.
319        assert!(matches!(
320            to_bytes(&Value::str("é")).unwrap(),
321            Value::Bytes(b) if b[..] == [0xc3, 0xa9]
322        ));
323        // A List of Ints becomes those bytes.
324        let from_list = to_bytes(&Value::list(vec![Value::int(104), Value::int(105)])).unwrap();
325        assert!(matches!(from_list, Value::Bytes(b) if b[..] == [104, 105]));
326        // A Bytes is returned unchanged.
327        assert!(matches!(
328            to_bytes(&Value::bytes([1, 2])).unwrap(),
329            Value::Bytes(b) if b[..] == [1, 2]
330        ));
331    }
332
333    #[test]
334    fn to_bytes_rejects_out_of_range_and_wrong_types() {
335        assert_eq!(
336            to_bytes(&Value::list(vec![Value::int(256)]))
337                .unwrap_err()
338                .kind,
339            ErrorKind::ValueError
340        );
341        assert_eq!(
342            to_bytes(&Value::list(vec![Value::str("x")]))
343                .unwrap_err()
344                .kind,
345            ErrorKind::TypeError
346        );
347        assert_eq!(
348            to_bytes(&Value::int(1)).unwrap_err().kind,
349            ErrorKind::TypeError
350        );
351    }
352
353    #[test]
354    fn len_counts_bytes_for_bytes() {
355        assert!(values_equal(
356            &len(&Value::bytes("héllo")).unwrap(),
357            &Value::int(6)
358        ));
359    }
360
361    #[test]
362    fn range_two_args() {
363        let xs = range(&Value::int(2), &Value::int(5)).unwrap();
364        match xs {
365            Value::List(items) => {
366                let items = items.borrow();
367                assert_eq!(items.len(), 3);
368                assert!(values_equal(&items[0], &Value::int(2)));
369                assert!(values_equal(&items[2], &Value::int(4)));
370            }
371            _ => panic!("expected a list"),
372        }
373    }
374
375    #[test]
376    fn range_empty_when_end_not_after_start() {
377        let xs = range(&Value::int(5), &Value::int(5)).unwrap();
378        assert!(values_equal(&len(&xs).unwrap(), &Value::int(0)));
379        let ys = range(&Value::int(5), &Value::int(2)).unwrap();
380        assert!(values_equal(&len(&ys).unwrap(), &Value::int(0)));
381    }
382
383    #[test]
384    fn range_rejects_float() {
385        assert_eq!(
386            range(&Value::int(0), &Value::Float(3.0)).unwrap_err().kind,
387            ErrorKind::TypeError
388        );
389        assert_eq!(
390            range(&Value::Float(0.0), &Value::int(3)).unwrap_err().kind,
391            ErrorKind::TypeError
392        );
393    }
394}