Skip to main content

doge_runtime/
objects.rs

1//! Field access and method-dispatch helpers for `many Name:` instances. The
2//! generated code calls these; the object model — fields on assignment, missing
3//! field/method as a catchable error — lives here, not in codegen (Hard Rule 5).
4
5use crate::error::{DogeError, DogeResult};
6use crate::value::Value;
7
8/// A class name with its English article, e.g. `"a Shibe"` / `"an Ostrich"`.
9/// Objects always describe themselves by class, never as "an Object".
10fn a_class(name: &str) -> String {
11    let article = match name.chars().next() {
12        Some('A' | 'E' | 'I' | 'O' | 'U' | 'a' | 'e' | 'i' | 'o' | 'u') => "an",
13        _ => "a",
14    };
15    format!("{article} {name}")
16}
17
18/// Read `obj.name`. A missing field is a catchable [`ErrorKind::AttrError`];
19/// reading a field off a non-object is a catchable `TypeError`.
20///
21/// [`ErrorKind::AttrError`]: crate::ErrorKind::AttrError
22pub fn attr_get(obj: &Value, name: &str) -> DogeResult {
23    match obj {
24        Value::Object(o) => {
25            let data = o.borrow();
26            data.fields.get(name).cloned().ok_or_else(|| {
27                DogeError::attr_error(format!("{} has no field {name}", a_class(&data.class_name)))
28            })
29        }
30        Value::Error(e) => crate::error::error_field(e, name),
31        _ => Err(DogeError::type_error(format!(
32            "cannot read the field {name} of {}",
33            obj.describe()
34        ))),
35    }
36}
37
38/// Read `obj.name` as a *value*, binding a method when there is no field of that
39/// name — the semantics of `such f = a.speak`. A field always wins over a method
40/// (fields appear on assignment). For a `many` instance, `class_has_method` tells
41/// whether the receiver's class (or an ancestor) defines `name`; for a List/Dict,
42/// [`has_builtin_method`] decides. A name that is neither a field nor a method is
43/// the same catchable error a bare [`attr_get`] would raise.
44///
45/// [`has_builtin_method`]: crate::methods::has_builtin_method
46pub fn attr_get_or_bind(
47    obj: &Value,
48    name: &str,
49    class_has_method: &dyn Fn(u32, &str) -> bool,
50) -> DogeResult {
51    match obj {
52        Value::Object(o) => {
53            let data = o.borrow();
54            if let Some(value) = data.fields.get(name) {
55                return Ok(value.clone());
56            }
57            if class_has_method(data.class_id, name) {
58                return Ok(Value::bound_method(obj.clone(), name));
59            }
60            Err(DogeError::attr_error(format!(
61                "{} has no field or method {name}",
62                a_class(&data.class_name)
63            )))
64        }
65        Value::List(_) | Value::Dict(_) if crate::methods::has_builtin_method(obj, name) => {
66            Ok(Value::bound_method(obj.clone(), name))
67        }
68        _ => attr_get(obj, name),
69    }
70}
71
72/// Write `obj.name = value`. A field appears the first time it is assigned;
73/// setting a field on a non-object is a catchable `TypeError`.
74pub fn attr_set(obj: &Value, name: &str, value: Value) -> DogeResult<()> {
75    match obj {
76        Value::Object(o) => {
77            o.borrow_mut().fields.insert(name.to_string(), value);
78            Ok(())
79        }
80        _ => Err(DogeError::type_error(format!(
81            "cannot set the field {name} on {}",
82            obj.describe()
83        ))),
84    }
85}
86
87/// The class id of a method-call receiver, so the dispatcher can pick the right
88/// arm. Calling a method on a value that has no methods is a catchable
89/// `AttrError`.
90pub fn object_class_id(recv: &Value) -> DogeResult<u32> {
91    match recv {
92        Value::Object(o) => Ok(o.borrow().class_id),
93        _ => Err(no_methods_error(recv)),
94    }
95}
96
97/// The error a method-call site raises when the receiver's class has no such
98/// method. `recv` is an object at every real call site; the non-object branch is
99/// a defensive fallback that mirrors [`object_class_id`].
100pub fn no_such_method(recv: &Value, method: &str) -> DogeError {
101    match recv {
102        Value::Object(o) => {
103            let data = o.borrow();
104            DogeError::attr_error(format!(
105                "{} has no method {method}",
106                a_class(&data.class_name)
107            ))
108        }
109        _ => no_methods_error(recv),
110    }
111}
112
113/// The error raised when a method is called on a value whose type has no methods
114/// at all (an Int, Str, Bool, …). Single source of the wording so it reads the
115/// same whether the receiver reached here through the dispatcher or a builtin.
116pub fn no_methods_error(recv: &Value) -> DogeError {
117    DogeError::attr_error(format!("{} has no methods", recv.describe()))
118}
119
120/// The error a method call raises when the argument count is wrong, worded like
121/// the compiler's user-function arity message. `max` is `None` when the method is
122/// variadic.
123pub fn method_arity_error(
124    class: &str,
125    method: &str,
126    min: usize,
127    max: Option<usize>,
128    got: usize,
129) -> DogeError {
130    DogeError::type_error(crate::functions::arity_phrase(
131        &format!("{class}.{method}"),
132        min,
133        max,
134        got,
135    ))
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::error::ErrorKind;
142
143    #[test]
144    fn set_then_get_round_trips_a_field() {
145        let obj = Value::object(0, "Shibe");
146        attr_set(&obj, "name", Value::str("kabosu")).unwrap();
147        assert!(matches!(attr_get(&obj, "name").unwrap(), Value::Str(s) if &*s == "kabosu"));
148    }
149
150    #[test]
151    fn binds_a_method_when_the_class_defines_it() {
152        let obj = Value::object(0, "Shibe");
153        let bound = attr_get_or_bind(&obj, "speak", &|cid, name| cid == 0 && name == "speak")
154            .expect("speak binds");
155        match bound {
156            Value::BoundMethod(m) => {
157                assert_eq!(&*m.method, "speak");
158                assert!(matches!(&m.receiver, Value::Object(_)));
159            }
160            other => panic!("expected a bound method, got {}", other.type_name()),
161        }
162    }
163
164    #[test]
165    fn a_field_wins_over_a_method_of_the_same_name() {
166        let obj = Value::object(0, "Shibe");
167        attr_set(&obj, "speak", Value::int(1)).unwrap();
168        // The class "has" a method speak, but the field shadows it.
169        let got = attr_get_or_bind(&obj, "speak", &|_, _| true).unwrap();
170        assert!(crate::values_equal(&got, &Value::int(1)));
171    }
172
173    #[test]
174    fn neither_field_nor_method_is_a_catchable_attr_error() {
175        let obj = Value::object(0, "Shibe");
176        let err = attr_get_or_bind(&obj, "fly", &|_, _| false).unwrap_err();
177        assert_eq!(err.kind, ErrorKind::AttrError);
178        assert_eq!(err.message, "a Shibe has no field or method fly");
179    }
180
181    #[test]
182    fn binds_a_collection_method() {
183        let list = Value::list(vec![]);
184        let bound = attr_get_or_bind(&list, "append", &|_, _| false).expect("append binds");
185        assert!(matches!(bound, Value::BoundMethod(_)));
186        // An unknown collection method stays the same error a bare read gives.
187        let err = attr_get_or_bind(&list, "nope", &|_, _| false).unwrap_err();
188        assert_eq!(err.kind, ErrorKind::TypeError);
189    }
190
191    #[test]
192    fn missing_field_is_a_catchable_attr_error() {
193        let obj = Value::object(0, "Shibe");
194        let err = attr_get(&obj, "tail").unwrap_err();
195        assert_eq!(err.kind, ErrorKind::AttrError);
196        assert_eq!(err.message, "a Shibe has no field tail");
197    }
198
199    #[test]
200    fn attr_on_a_non_object_is_a_type_error() {
201        let err = attr_get(&Value::int(1), "name").unwrap_err();
202        assert_eq!(err.kind, ErrorKind::TypeError);
203        let err = attr_set(&Value::int(1), "name", Value::None).unwrap_err();
204        assert_eq!(err.kind, ErrorKind::TypeError);
205    }
206
207    #[test]
208    fn class_id_reads_the_object_and_rejects_others() {
209        assert_eq!(object_class_id(&Value::object(3, "Shibe")).unwrap(), 3);
210        let err = object_class_id(&Value::int(1)).unwrap_err();
211        assert_eq!(err.kind, ErrorKind::AttrError);
212    }
213
214    #[test]
215    fn no_such_method_names_the_class() {
216        let err = no_such_method(&Value::object(0, "Shibe"), "fly");
217        assert_eq!(err.kind, ErrorKind::AttrError);
218        assert_eq!(err.message, "a Shibe has no method fly");
219    }
220
221    #[test]
222    fn no_methods_error_names_the_type_with_its_article() {
223        let err = no_methods_error(&Value::int(1));
224        assert_eq!(err.kind, ErrorKind::AttrError);
225        assert_eq!(err.message, "an Int has no methods");
226        assert_eq!(
227            no_methods_error(&Value::str("x")).message,
228            "a Str has no methods"
229        );
230    }
231
232    #[test]
233    fn method_arity_error_matches_the_user_wording() {
234        assert_eq!(
235            method_arity_error("Shibe", "init", 2, Some(2), 1).message,
236            "Shibe.init takes 2 arguments, got 1"
237        );
238        assert_eq!(
239            method_arity_error("Shibe", "speak", 1, Some(1), 0).message,
240            "Shibe.speak takes 1 argument, got 0"
241        );
242        assert_eq!(
243            method_arity_error("Shibe", "greet", 1, None, 3).message,
244            "Shibe.greet takes at least 1 argument, got 3"
245        );
246    }
247}