blue-lang-runtime 0.0.145

The blue runtime — one definition of what a blue program runs against.
Documentation
use std::sync::{Arc, Mutex, PoisonError};

use blue_lang_senses::canvas::{self, Canvas};
use blue_lang_senses::display::Screen;
use tatara_lisp_eval::ffi::Arity;
use tatara_lisp_eval::value::MapKey;
use tatara_lisp_eval::{EvalError, Interpreter, Value};

use crate::errors::raise_kind;
use crate::provide::Key;

type Span = tatara_lisp::Span;

#[derive(Clone, Copy)]
enum Surface {
    Host,
    Framebuffer,
}

struct Open {
    canvas: Canvas,
    screen: Option<Screen>,
}

struct Handle(Mutex<Option<Open>>);

fn bad_option(word: &'static str, message: String, span: Span) -> EvalError {
    raise_kind("bad_option", format!("{word}: {message}"), span)
}

fn int(v: &Value, what: &str, word: &'static str, span: Span) -> Result<i64, EvalError> {
    match v {
        Value::Int(n) => Ok(*n),
        other => Err(bad_option(
            word,
            format!("{what} is an integer, got {}", other.type_name()),
            span,
        )),
    }
}

fn color(v: &Value, word: &'static str, span: Span) -> Result<u32, EvalError> {
    match v {
        Value::Str(s) => canvas::color(s).map_err(|e| bad_option(word, e, span)),
        other => Err(bad_option(
            word,
            format!(
                "a color is a string like \"#ff8800\", got {}",
                other.type_name()
            ),
            span,
        )),
    }
}

fn field<'a>(v: &'a Value, name: &str) -> Option<&'a Value> {
    match v {
        Value::Map(m) => m.get(&MapKey::Keyword(Arc::from(name))),
        _ => None,
    }
}

fn handle(v: &Value, word: &'static str, span: Span) -> Result<Arc<Handle>, EvalError> {
    if let Value::Foreign(any) = v {
        if let Ok(h) = Arc::downcast::<Handle>(any.clone()) {
            return Ok(h);
        }
    }
    Err(bad_option(
        word,
        format!("expected a canvas from canvas_open, got {}", v.type_name()),
        span,
    ))
}

fn with_open<T>(
    v: &Value,
    word: &'static str,
    span: Span,
    f: impl FnOnce(&mut Open) -> Result<T, EvalError>,
) -> Result<T, EvalError> {
    let h = handle(v, word, span)?;
    let mut guard = h.0.lock().unwrap_or_else(PoisonError::into_inner);
    let open = guard
        .as_mut()
        .ok_or_else(|| raise_kind("closed", format!("{word}: this canvas is closed"), span))?;
    f(open)
}

fn open_canvas(surface: Surface, opts: &Value, span: Span) -> Result<Value, EvalError> {
    const W: &str = "canvas_open";
    if !matches!(opts, Value::Map(_)) {
        return Err(bad_option(
            W,
            format!(
                "opts is a map with width: and height:, got {}",
                opts.type_name()
            ),
            span,
        ));
    }
    let side = |name: &str| match field(opts, name) {
        Some(v) => int(v, name, W, span),
        None => Err(bad_option(W, format!("opts needs {name}:"), span)),
    };
    let canvas =
        Canvas::new(side("width")?, side("height")?).map_err(|e| bad_option(W, e, span))?;
    let title = match field(opts, "title") {
        None => "blue".to_string(),
        Some(Value::Str(s)) => s.to_string(),
        Some(other) => {
            return Err(bad_option(
                W,
                format!("title: is a string, got {}", other.type_name()),
                span,
            ))
        }
    };
    let screen = match surface {
        Surface::Host => Some(
            Screen::open(&canvas, &title)
                .map_err(|r| raise_kind(r.kind(), format!("{W}: {}", r.message()), span))?,
        ),
        Surface::Framebuffer => None,
    };
    Ok(Value::Foreign(Arc::new(Handle(Mutex::new(Some(Open {
        canvas,
        screen,
    }))))))
}

fn install<H: 'static>(interp: &mut Interpreter<H>, key: Key, surface: Surface) {
    interp.register_fn(
        key("canvas_open"),
        Arity::Exact(1),
        move |a: &[Value], _: &mut H, span| open_canvas(surface, &a[0], span),
    );
    interp.register_fn(
        key("canvas_clear"),
        Arity::Exact(2),
        |a: &[Value], _: &mut H, span| {
            const W: &str = "canvas_clear";
            let rgb = color(&a[1], W, span)?;
            with_open(&a[0], W, span, |o| {
                o.canvas.clear(rgb);
                Ok(Value::Nil)
            })
        },
    );
    interp.register_fn(
        key("canvas_fill_rect"),
        Arity::Exact(6),
        |a: &[Value], _: &mut H, span| {
            const W: &str = "canvas_fill_rect";
            let x = int(&a[1], "x", W, span)?;
            let y = int(&a[2], "y", W, span)?;
            let w = int(&a[3], "width", W, span)?;
            let h = int(&a[4], "height", W, span)?;
            let rgb = color(&a[5], W, span)?;
            with_open(&a[0], W, span, |o| {
                o.canvas.fill_rect(x, y, w, h, rgb);
                Ok(Value::Nil)
            })
        },
    );
    interp.register_fn(
        key("canvas_text"),
        Arity::Exact(5),
        |a: &[Value], _: &mut H, span| {
            const W: &str = "canvas_text";
            let x = int(&a[1], "x", W, span)?;
            let y = int(&a[2], "y", W, span)?;
            let text = match &a[3] {
                Value::Str(s) => s.clone(),
                other => {
                    return Err(bad_option(
                        W,
                        format!("text is a string, got {}", other.type_name()),
                        span,
                    ))
                }
            };
            let rgb = color(&a[4], W, span)?;
            with_open(&a[0], W, span, |o| {
                o.canvas.text(x, y, &text, rgb);
                Ok(Value::Nil)
            })
        },
    );
    interp.register_fn(
        key("canvas_present"),
        Arity::Exact(1),
        |a: &[Value], _: &mut H, span| {
            const W: &str = "canvas_present";
            with_open(&a[0], W, span, |o| {
                o.canvas.present();
                if let Some(screen) = o.screen.as_mut() {
                    screen
                        .present(&o.canvas)
                        .map_err(|r| raise_kind(r.kind(), format!("{W}: {}", r.message()), span))?;
                }
                Ok(Value::Nil)
            })
        },
    );
    interp.register_fn(
        key("canvas_pixel"),
        Arity::Exact(3),
        |a: &[Value], _: &mut H, span| {
            const W: &str = "canvas_pixel";
            let x = int(&a[1], "x", W, span)?;
            let y = int(&a[2], "y", W, span)?;
            with_open(&a[0], W, span, |o| {
                Ok(o.canvas
                    .pixel(x, y)
                    .map_or(Value::Nil, |p| Value::Str(Arc::from(canvas::hex(p)))))
            })
        },
    );
    interp.register_fn(
        key("canvas_close"),
        Arity::Exact(1),
        |a: &[Value], _: &mut H, span| {
            let h = handle(&a[0], "canvas_close", span)?;
            let open = h.0.lock().unwrap_or_else(PoisonError::into_inner).take();
            if let Some(screen) = open.and_then(|o| o.screen) {
                let _ = screen.close();
            }
            Ok(Value::Nil)
        },
    );
}

pub fn install_display<H: 'static>(interp: &mut Interpreter<H>) {
    install(interp, String::from, Surface::Host);
}

pub fn provide_framebuffer<H: 'static>(interp: &mut Interpreter<H>, key: Key) {
    install(interp, key, Surface::Framebuffer);
}

#[cfg(test)]
mod tests {
    use super::*;

    fn framebuffer() -> Interpreter<()> {
        let mut interp = crate::interpreter_hostless();
        provide_framebuffer(&mut interp, String::from);
        interp
    }

    fn eval(interp: &mut Interpreter<()>, src: &str) -> Result<Value, EvalError> {
        let forms = tatara_lisp::read_spanned(src).expect("reads");
        interp.eval_program(&forms, &mut ())
    }

    fn ok(interp: &mut Interpreter<()>, src: &str) -> String {
        eval(interp, src)
            .unwrap_or_else(|e| panic!("{src}: {e}"))
            .to_string()
    }

    fn kind(interp: &mut Interpreter<()>, src: &str) -> String {
        match eval(interp, src) {
            Err(EvalError::User {
                value: Value::Error(e),
                ..
            }) => e.tag.to_string(),
            other => panic!("{src}: {other:?}"),
        }
    }

    #[test]
    fn the_framebuffer_shows_what_was_drawn_once_it_is_presented() {
        let mut i = framebuffer();
        ok(
            &mut i,
            "(define c (canvas_open (hash-map :width 16 :height 8 :title \"t\")))",
        );
        ok(&mut i, "(canvas_clear c \"#102030\")");
        ok(&mut i, "(canvas_fill_rect c 1 1 2 2 \"#f00\")");
        ok(&mut i, "(canvas_text c 8 0 \"A\" \"#ffffff\")");
        assert_eq!(ok(&mut i, "(canvas_pixel c 1 1)"), "\"#000000\"");
        ok(&mut i, "(canvas_present c)");
        assert_eq!(ok(&mut i, "(canvas_pixel c 0 0)"), "\"#102030\"");
        assert_eq!(ok(&mut i, "(canvas_pixel c 2 2)"), "\"#ff0000\"");
        assert_eq!(ok(&mut i, "(canvas_pixel c 3 3)"), "\"#102030\"");
        assert_eq!(ok(&mut i, "(canvas_pixel c 10 0)"), "\"#ffffff\"");
        assert_eq!(ok(&mut i, "(canvas_pixel c 16 0)"), "()");
        assert_eq!(ok(&mut i, "(canvas_close c)"), "()");
        assert_eq!(
            ok(&mut i, "(canvas_close c)"),
            "()",
            "closing twice is done"
        );
        assert_eq!(kind(&mut i, "(canvas_pixel c 0 0)"), "closed");
    }

    #[test]
    fn a_bad_size_color_or_handle_is_refused() {
        let mut i = framebuffer();
        for bad in [
            "(canvas_open (hash-map :width 0 :height 8))",
            "(canvas_open (hash-map :width 8))",
            "(canvas_open 8)",
            "(canvas_open (hash-map :width 8 :height 8 :title 3))",
            "(canvas_clear 1 \"#000\")",
        ] {
            assert_eq!(kind(&mut i, bad), "bad_option", "{bad}");
        }
        ok(
            &mut i,
            "(define c (canvas_open (hash-map :width 2 :height 2)))",
        );
        assert_eq!(kind(&mut i, "(canvas_clear c \"red\")"), "bad_option");
        assert_eq!(
            kind(&mut i, "(canvas_fill_rect c 0 0 1 1.5 \"#000\")"),
            "bad_option"
        );
    }
}