Skip to main content

doge_runtime/
builtins.rs

1use std::io::Write;
2
3use crate::error::{DogeError, DogeResult};
4use crate::value::Value;
5
6/// `bark x` — print a value on its own line and evaluate to `none`, so
7/// `bark` can sit anywhere an expression is expected.
8pub fn bark(v: &Value) -> Value {
9    println!("{v}");
10    Value::None
11}
12
13/// `gib()` / `gib("prompt")` — read one line from standard input. An optional
14/// prompt, which must be a Str, is written without a trailing newline and flushed
15/// first. The returned Str has its trailing newline stripped; at end of input the
16/// result is `none`. A stdin read failure is a catchable IOError.
17pub fn gib(prompt: Option<&Value>) -> DogeResult {
18    if let Some(p) = prompt {
19        let text = match p {
20            Value::Str(s) => s,
21            _ => {
22                return Err(DogeError::type_error(format!(
23                    "gib needs a Str prompt, got {}",
24                    p.describe()
25                )))
26            }
27        };
28        print!("{text}");
29        let _ = std::io::stdout().flush();
30    }
31    let mut line = String::new();
32    match std::io::stdin().read_line(&mut line) {
33        Ok(0) => Ok(Value::None),
34        Ok(_) => {
35            let line = line.strip_suffix('\n').unwrap_or(&line);
36            let line = line.strip_suffix('\r').unwrap_or(line);
37            Ok(Value::str(line))
38        }
39        Err(err) => Err(DogeError::io_error(format!("could not read input: {err}"))),
40    }
41}
42
43/// `len(x)` — character count for a Str, element count for a List or Dict.
44/// Anything else is a catchable type error.
45pub fn len(v: &Value) -> DogeResult {
46    match v {
47        Value::Str(s) => Ok(Value::Int(s.chars().count() as i64)),
48        Value::List(items) => Ok(Value::Int(items.borrow().len() as i64)),
49        Value::Dict(entries) => Ok(Value::Int(entries.borrow().len() as i64)),
50        _ => Err(crate::error::DogeError::type_error(format!(
51            "cannot take the len of {}",
52            v.describe()
53        ))),
54    }
55}
56
57/// `str(x)` — the value's printed form as a Str. Always succeeds.
58pub fn to_str(v: &Value) -> Value {
59    Value::str(v.to_string())
60}
61
62/// String interpolation (`"a {b} c"`) — join each part's display form, the same
63/// text `bark`/`str` would show, into one Str. Always succeeds.
64pub fn interp(parts: &[Value]) -> Value {
65    let mut out = String::new();
66    for part in parts {
67        out.push_str(&part.to_string());
68    }
69    Value::str(out)
70}
71
72/// `int(x)` — Int unchanged, Float truncated toward zero, Bool to 0/1, a Str
73/// parsed as a whole number. A Str that isn't a number is a catchable
74/// `ValueError`; other types are a `TypeError`.
75pub fn to_int(v: &Value) -> DogeResult {
76    match v {
77        Value::Int(n) => Ok(Value::Int(*n)),
78        Value::Float(f) => Ok(Value::Int(*f as i64)),
79        Value::Bool(b) => Ok(Value::Int(i64::from(*b))),
80        Value::Str(s) => s.trim().parse::<i64>().map(Value::Int).map_err(|_| {
81            crate::error::DogeError::value_error(format!("cannot turn {s:?} into an Int"))
82        }),
83        _ => Err(crate::error::DogeError::type_error(format!(
84            "cannot turn {} into an Int",
85            v.describe()
86        ))),
87    }
88}
89
90/// `float(x)` — Int and Bool widen to Float, Float is unchanged, a numeric Str
91/// is parsed. A non-numeric Str is a catchable `ValueError`; other types are a
92/// `TypeError`.
93pub fn to_float(v: &Value) -> DogeResult {
94    match v {
95        Value::Int(n) => Ok(Value::Float(*n as f64)),
96        Value::Float(f) => Ok(Value::Float(*f)),
97        Value::Bool(b) => Ok(Value::Float(if *b { 1.0 } else { 0.0 })),
98        Value::Str(s) => s.trim().parse::<f64>().map(Value::Float).map_err(|_| {
99            crate::error::DogeError::value_error(format!("cannot turn {s:?} into a Float"))
100        }),
101        _ => Err(crate::error::DogeError::type_error(format!(
102            "cannot turn {} into a Float",
103            v.describe()
104        ))),
105    }
106}
107
108/// `range(start, end)` — the Ints `start, start+1, …, end-1` as an eager List.
109/// When `end <= start` the List is naturally empty. Both arguments must be Int;
110/// anything else is a catchable type error. The one-argument Doge form
111/// `range(n)` is compiled as `range(0, n)`, so the runtime has one signature.
112pub fn range(start: &Value, end: &Value) -> DogeResult {
113    match (start, end) {
114        (Value::Int(a), Value::Int(b)) => Ok(Value::list((*a..*b).map(Value::Int).collect())),
115        (Value::Int(_), other) | (other, _) => Err(crate::error::DogeError::type_error(format!(
116            "range needs Int bounds, got {}",
117            other.describe()
118        ))),
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::error::ErrorKind;
126
127    #[test]
128    fn bark_returns_none() {
129        assert!(matches!(bark(&Value::str("much hello")), Value::None));
130    }
131
132    #[test]
133    fn len_counts_characters_and_elements() {
134        // Char count, not byte count — 'é' is one character.
135        assert!(matches!(len(&Value::str("héllo")).unwrap(), Value::Int(5)));
136        assert!(matches!(
137            len(&Value::list(vec![Value::Int(1), Value::Int(2)])).unwrap(),
138            Value::Int(2)
139        ));
140        assert_eq!(len(&Value::Int(3)).unwrap_err().kind, ErrorKind::TypeError);
141    }
142
143    #[test]
144    fn interp_joins_display_forms() {
145        let parts = [
146            Value::str("age "),
147            Value::Int(7),
148            Value::str(", "),
149            Value::None,
150        ];
151        assert!(matches!(interp(&parts), Value::Str(s) if &*s == "age 7, none"));
152        // Nested Strs embed bare, matching bark/str, not the quoted repr.
153        let nested = [Value::str("["), Value::str("hi"), Value::str("]")];
154        assert!(matches!(interp(&nested), Value::Str(s) if &*s == "[hi]"));
155        assert!(matches!(interp(&[]), Value::Str(s) if s.is_empty()));
156    }
157
158    #[test]
159    fn conversions_round_trip() {
160        assert!(matches!(to_str(&Value::Int(7)), Value::Str(s) if &*s == "7"));
161        assert!(matches!(to_int(&Value::Float(3.9)).unwrap(), Value::Int(3)));
162        assert!(matches!(
163            to_int(&Value::str(" 42 ")).unwrap(),
164            Value::Int(42)
165        ));
166        assert!(matches!(to_float(&Value::Int(4)).unwrap(), Value::Float(f) if f == 4.0));
167    }
168
169    #[test]
170    fn bad_conversions_are_catchable_value_errors() {
171        assert_eq!(
172            to_int(&Value::str("dog")).unwrap_err().kind,
173            ErrorKind::ValueError
174        );
175        assert_eq!(
176            to_float(&Value::str("woof")).unwrap_err().kind,
177            ErrorKind::ValueError
178        );
179    }
180
181    #[test]
182    fn range_two_args() {
183        let xs = range(&Value::Int(2), &Value::Int(5)).unwrap();
184        match xs {
185            Value::List(items) => {
186                let items = items.borrow();
187                assert_eq!(items.len(), 3);
188                assert!(matches!(items[0], Value::Int(2)));
189                assert!(matches!(items[2], Value::Int(4)));
190            }
191            _ => panic!("expected a list"),
192        }
193    }
194
195    #[test]
196    fn range_empty_when_end_not_after_start() {
197        let xs = range(&Value::Int(5), &Value::Int(5)).unwrap();
198        assert!(matches!(len(&xs).unwrap(), Value::Int(0)));
199        let ys = range(&Value::Int(5), &Value::Int(2)).unwrap();
200        assert!(matches!(len(&ys).unwrap(), Value::Int(0)));
201    }
202
203    #[test]
204    fn range_rejects_float() {
205        assert_eq!(
206            range(&Value::Int(0), &Value::Float(3.0)).unwrap_err().kind,
207            ErrorKind::TypeError
208        );
209        assert_eq!(
210            range(&Value::Float(0.0), &Value::Int(3)).unwrap_err().kind,
211            ErrorKind::TypeError
212        );
213    }
214}