narju/floor/
host.rs

1use std::any::Any;
2use std::collections::HashMap;
3use std::fmt;
4use std::sync::OnceLock;
5
6use super::{Shared, Val};
7
8/// Data, emphatically: an atom declares a wire encoding because it must cross
9/// a channel as a message payload. What cannot `encode` is a capability, and
10/// capabilities are addresses.
11pub trait HostType: Send + Sync + fmt::Debug + fmt::Display {
12    /// Wire tag and registry key, stable across processes. Table indices are
13    /// process-local, names are not.
14    fn type_name(&self) -> &'static str;
15
16    /// Implementations downcast and return `false` on a foreign type rather
17    /// than comparing tags.
18    fn eq_atom(&self, other: &dyn HostType) -> bool;
19
20    fn encode(&self, out: &mut Vec<u8>);
21
22    fn as_any(&self) -> &dyn Any;
23}
24
25/// The newtype carries the `PartialEq`, `Debug` and `Display` that
26/// `dyn HostType` cannot, which lets `Exp` keep a derived `PartialEq`.
27#[derive(Clone)]
28pub struct Atom(pub Shared<dyn HostType>);
29
30impl Atom {
31    pub fn new<T: HostType + 'static>(t: T) -> Atom {
32        Atom(Shared::new(t))
33    }
34
35    pub fn downcast<T: 'static>(&self) -> Option<&T> {
36        self.0.as_any().downcast_ref::<T>()
37    }
38
39    pub fn type_name(&self) -> &'static str {
40        self.0.type_name()
41    }
42}
43
44impl PartialEq for Atom {
45    fn eq(&self, other: &Self) -> bool {
46        Shared::ptr_eq(&self.0, &other.0) || self.0.eq_atom(&*other.0)
47    }
48}
49
50impl fmt::Debug for Atom {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        fmt::Debug::fmt(&*self.0, f)
53    }
54}
55
56impl fmt::Display for Atom {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        fmt::Display::fmt(&*self.0, f)
59    }
60}
61
62/// An op's failure is an ordinary narju value, so a partial op is a `throw`
63/// the program can catch, not a second error channel.
64pub type OpResult = Result<Val, Val>;
65
66pub struct OpDef {
67    pub name: &'static str,
68    pub arity: usize,
69    pub run: fn(&[Val]) -> OpResult,
70}
71
72pub type Decoder = fn(&[u8]) -> Option<Atom>;
73
74/// Names resolve to indices once, in the reader, so a call site costs an
75/// array index and not a hash lookup.
76#[derive(Default)]
77pub struct Registry {
78    ops: Vec<OpDef>,
79    by_name: HashMap<&'static str, u16>,
80    decoders: HashMap<&'static str, Decoder>,
81}
82
83impl Registry {
84    pub fn new() -> Registry {
85        Registry::default()
86    }
87
88    pub fn op(mut self, def: OpDef) -> Registry {
89        let idx = u16::try_from(self.ops.len()).expect("op table exceeds u16");
90        if self.by_name.insert(def.name, idx).is_some() {
91            panic!("duplicate op {}", def.name);
92        }
93        self.ops.push(def);
94        self
95    }
96
97    pub fn ty(mut self, name: &'static str, decode: Decoder) -> Registry {
98        if self.decoders.insert(name, decode).is_some() {
99            panic!("duplicate host type {name}");
100        }
101        self
102    }
103
104    pub fn lookup(&self, name: &str) -> Option<u16> {
105        self.by_name.get(name).copied()
106    }
107
108    pub fn get(&self, idx: u16) -> &OpDef {
109        &self.ops[idx as usize]
110    }
111
112    pub fn decoder(&self, type_name: &str) -> Option<Decoder> {
113        self.decoders.get(type_name).copied()
114    }
115}
116
117static REGISTRY: OnceLock<Registry> = OnceLock::new();
118
119/// Must happen before the first read, since reading is what resolves names to
120/// indices. An embedder extends [`crate::registry`] rather than starting empty.
121pub fn install(r: Registry) -> Result<(), Registry> {
122    REGISTRY.set(r)
123}
124
125/// The default is the whole language's table, not this file's: the reader
126/// resolves names and sits below every layer that adds to it, so an op the
127/// surface adds would otherwise be unreachable.
128pub fn registry() -> &'static Registry {
129    REGISTRY.get_or_init(crate::registry)
130}
131
132/// A pair rather than a list, the shape every raise above here already has.
133fn refused(tag: &str, detail: Val) -> Val {
134    Val::Pair(super::rc_val(Val::Sym(tag.into())), super::rc_val(detail))
135}
136
137fn wrong_type(op: &str) -> Val {
138    refused("wrong-type", Val::Sym(op.into()))
139}
140
141fn as_f64(v: &Val) -> Option<f64> {
142    match v {
143        Val::Num(n) => Some(*n as f64),
144        Val::Flo(x) => Some(*x),
145        _ => None,
146    }
147}
148
149/// Macros rather than functions taking the arithmetic as an argument: `run` is
150/// a bare `fn` pointer and so cannot close over one.
151macro_rules! flo1 {
152    ($name:literal, $f:expr) => {
153        OpDef {
154            name: $name,
155            arity: 1,
156            run: |a| match as_f64(&a[0]) {
157                Some(x) => Ok(Val::Flo($f(x))),
158                None => Err(wrong_type($name)),
159            },
160        }
161    };
162}
163
164macro_rules! int1 {
165    ($name:literal, $f:expr) => {
166        OpDef {
167            name: $name,
168            arity: 1,
169            run: |a| match &a[0] {
170                Val::Num(n) => Ok(Val::Num(*n)),
171                Val::Flo(x) => Ok(Val::Num($f(*x) as i64)),
172                _ => Err(wrong_type($name)),
173            },
174        }
175    };
176}
177
178pub fn builtins() -> Registry {
179    Registry::new()
180        .ty(Str::NAME, Str::decode)
181        .op(OpDef {
182            name: "str-append",
183            arity: 2,
184            run: |a| match (Str::of(&a[0]), Str::of(&a[1])) {
185                (Some(x), Some(y)) => Ok(Str::val(format!("{x}{y}"))),
186                _ => Err(wrong_type("str-append")),
187            },
188        })
189        .op(OpDef {
190            name: "str-len",
191            arity: 1,
192            run: |a| match Str::of(&a[0]) {
193                Some(s) => Ok(Val::Num(s.chars().count() as i64)),
194                None => Err(wrong_type("str-len")),
195            },
196        })
197        .op(OpDef {
198            name: "sym->str",
199            arity: 1,
200            run: |a| match &a[0] {
201                Val::Sym(s) => Ok(Str::val(s.to_string())),
202                _ => Err(wrong_type("sym->str")),
203            },
204        })
205        .op(OpDef {
206            name: "str->sym",
207            arity: 1,
208            run: |a| match Str::of(&a[0]) {
209                Some(s) => Ok(Val::Sym(s.into())),
210                None => Err(wrong_type("str->sym")),
211            },
212        })
213        // Characters, not bytes, and clamped rather than refused: an index past
214        // the end is how a scan finds out it has reached one.
215        .op(OpDef {
216            name: "substr",
217            arity: 3,
218            run: |a| match (Str::of(&a[0]), &a[1], &a[2]) {
219                (Some(s), Val::Num(from), Val::Num(len)) => {
220                    let from = (*from).max(0) as usize;
221                    let len = (*len).max(0) as usize;
222                    Ok(Str::val(s.chars().skip(from).take(len).collect::<String>()))
223                }
224                _ => Err(wrong_type("substr")),
225            },
226        })
227        // The one op here that exists for speed rather than reach: the
228        // evaluator's inner loop walks a frame per name, and a written-out walk
229        // allocates an environment chunk per step. A miss answers `0` rather
230        // than raising - the raise belongs to whoever runs out of frames.
231        .op(OpDef {
232            name: "assq",
233            arity: 2,
234            run: |a| {
235                let mut al = &a[1];
236                while let Val::Pair(entry, rest) = al {
237                    if let Val::Pair(key, _) = &**entry {
238                        if **key == a[0] {
239                            return Ok(Val::clone(entry));
240                        }
241                    }
242                    al = rest;
243                }
244                Ok(Val::Num(0))
245            },
246        })
247        // A raise rather than a sentinel: every number is a possible answer, so
248        // none is free to stand for the absence of one.
249        .op(OpDef {
250            name: "str->num",
251            arity: 1,
252            run: |a| match Str::of(&a[0]) {
253                Some(s) => {
254                    let t = s.trim();
255                    if let Ok(n) = t.parse::<i64>() {
256                        return Ok(Val::Num(n));
257                    }
258                    match t.parse::<f64>() {
259                        Ok(x) => Ok(Val::Flo(x)),
260                        Err(_) => Err(refused("not-a-number", Str::val(s.to_string()))),
261                    }
262                }
263                None => Err(wrong_type("str->num")),
264            },
265        })
266        // Case is a property of the alphabet, not the string: a program with
267        // `ord` and `chr` would be quietly wrong outside ASCII.
268        .op(OpDef {
269            name: "str-upper",
270            arity: 1,
271            run: |a| match Str::of(&a[0]) {
272                Some(s) => Ok(Str::val(s.to_uppercase())),
273                None => Err(wrong_type("str-upper")),
274            },
275        })
276        .op(OpDef {
277            name: "str-lower",
278            arity: 1,
279            run: |a| match Str::of(&a[0]) {
280                Some(s) => Ok(Str::val(s.to_lowercase())),
281                None => Err(wrong_type("str-lower")),
282            },
283        })
284        // A character is a one-character string rather than a type of its own:
285        // a second type would need its own literal syntax, equality and printed
286        // form to buy what `substr` already answers with.
287        .op(OpDef {
288            name: "ord",
289            arity: 1,
290            run: |a| match Str::of(&a[0]) {
291                Some(s) => match s.chars().next() {
292                    Some(c) => Ok(Val::Num(c as i64)),
293                    None => Err(refused("empty-string", Val::Sym("ord".into()))),
294                },
295                None => Err(wrong_type("ord")),
296            },
297        })
298        .op(OpDef {
299            name: "chr",
300            arity: 1,
301            run: |a| match &a[0] {
302                Val::Num(n) => match u32::try_from(*n).ok().and_then(char::from_u32) {
303                    Some(c) => Ok(Str::val(c.to_string())),
304                    None => Err(refused("not-a-character", Val::Num(*n))),
305                },
306                _ => Err(wrong_type("chr")),
307            },
308        })
309        // The printed form is not the text a value writes: a prompt wants to
310        // see `"hi"` and a write wants to emit `hi`.
311        .op(OpDef {
312            name: "show",
313            arity: 1,
314            run: |a| Ok(Str::val(a[0].to_string())),
315        })
316        // Each widens an integer argument and answers a float: `(sqrt 4)` is
317        // 2.0, and rounding here would decide what the caller can with `round`.
318        .op(flo1!("sqrt", f64::sqrt))
319        .op(flo1!("exp", f64::exp))
320        .op(flo1!("log", f64::ln))
321        .op(flo1!("sin", f64::sin))
322        .op(flo1!("cos", f64::cos))
323        .op(flo1!("tan", f64::tan))
324        .op(flo1!("asin", f64::asin))
325        .op(flo1!("acos", f64::acos))
326        .op(flo1!("atan", f64::atan))
327        .op(OpDef {
328            name: "pow",
329            arity: 2,
330            run: |a| match (as_f64(&a[0]), as_f64(&a[1])) {
331                (Some(x), Some(y)) => Ok(Val::Flo(x.powf(y))),
332                _ => Err(wrong_type("pow")),
333            },
334        })
335        // Two arguments because one cannot tell the quadrant: `(atan (/ y x))`
336        // loses the signs before it is called.
337        .op(OpDef {
338            name: "atan2",
339            arity: 2,
340            run: |a| match (as_f64(&a[0]), as_f64(&a[1])) {
341                (Some(y), Some(x)) => Ok(Val::Flo(y.atan2(x))),
342                _ => Err(wrong_type("atan2")),
343            },
344        })
345        .op(OpDef {
346            name: "read",
347            arity: 1,
348            run: super::read::read_op,
349        })
350        .op(OpDef {
351            name: "trans",
352            arity: 2,
353            run: super::read::trans_op,
354        })
355        // An integer argument is answered unchanged rather than widened and
356        // narrowed again.
357        .op(int1!("floor", f64::floor))
358        .op(int1!("ceil", f64::ceil))
359        .op(int1!("round", f64::round))
360        .op(int1!("trunc", f64::trunc))
361}
362
363/// The worked example: a host type is a payload, a name, an equality, an
364/// encoding.
365#[derive(Debug, Clone, PartialEq)]
366pub struct Str(pub Shared<str>);
367
368impl Str {
369    pub const NAME: &'static str = "str";
370
371    pub fn val(s: impl Into<Shared<str>>) -> Val {
372        Val::Atom(Atom::new(Str(s.into())))
373    }
374
375    pub fn of(v: &Val) -> Option<&str> {
376        match v {
377            Val::Atom(a) => a.downcast::<Str>().map(|s| &*s.0),
378            _ => None,
379        }
380    }
381
382    fn decode(bytes: &[u8]) -> Option<Atom> {
383        std::str::from_utf8(bytes)
384            .ok()
385            .map(|s| Atom::new(Str(s.into())))
386    }
387}
388
389impl fmt::Display for Str {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        write!(f, "{:?}", &*self.0)
392    }
393}
394
395impl HostType for Str {
396    fn type_name(&self) -> &'static str {
397        Str::NAME
398    }
399
400    fn eq_atom(&self, other: &dyn HostType) -> bool {
401        other
402            .as_any()
403            .downcast_ref::<Str>()
404            .is_some_and(|o| self.0 == o.0)
405    }
406
407    fn encode(&self, out: &mut Vec<u8>) {
408        out.extend_from_slice(self.0.as_bytes());
409    }
410
411    fn as_any(&self) -> &dyn Any {
412        self
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    fn call(name: &str, args: &[Val]) -> OpResult {
421        let r = builtins();
422        let idx = r.lookup(name).expect("op present");
423        let def = r.get(idx);
424        assert_eq!(def.arity, args.len());
425        (def.run)(args)
426    }
427
428    #[test]
429    fn atoms_compare_by_value_within_a_type() {
430        assert_eq!(Str::val("ab"), Str::val("ab"));
431        assert_ne!(Str::val("ab"), Str::val("ba"));
432        assert_ne!(Str::val("ab"), Val::Sym("ab".into()));
433    }
434
435    #[test]
436    fn ops_dispatch_and_throw_narju_values() {
437        assert_eq!(
438            call("str-append", &[Str::val("na"), Str::val("rju")]),
439            Ok(Str::val("narju"))
440        );
441        assert_eq!(call("str-len", &[Str::val("narju")]), Ok(Val::Num(5)));
442        assert!(matches!(
443            call("str-len", &[Val::Num(1)]),
444            Err(Val::Pair(_, _))
445        ));
446    }
447
448    #[test]
449    fn a_substring_is_counted_in_characters_and_clamped() {
450        let s = Str::val("nàrju");
451        assert_eq!(
452            call("substr", &[s.clone(), Val::Num(1), Val::Num(2)]),
453            Ok(Str::val("àr"))
454        );
455        assert_eq!(
456            call("substr", &[s.clone(), Val::Num(3), Val::Num(99)]),
457            Ok(Str::val("ju"))
458        );
459        assert_eq!(
460            call("substr", &[s, Val::Num(9), Val::Num(1)]),
461            Ok(Str::val(""))
462        );
463    }
464
465    #[test]
466    fn a_symbol_names_the_text_it_was_made_from() {
467        assert_eq!(
468            call("str->sym", &[Str::val("narju")]),
469            Ok(Val::Sym("narju".into()))
470        );
471        assert_eq!(
472            call("sym->str", &[Val::Sym("narju".into())]),
473            Ok(Str::val("narju"))
474        );
475    }
476
477    #[test]
478    fn a_host_value_round_trips_the_wire_encoding() {
479        let atom = match Str::val("narju") {
480            Val::Atom(a) => a,
481            _ => unreachable!(),
482        };
483        let mut bytes = Vec::new();
484        atom.0.encode(&mut bytes);
485        let decode = builtins().decoder(atom.type_name()).expect("decoder");
486        assert_eq!(decode(&bytes), Some(atom));
487    }
488}