eclisp 0.1.5

Bindings for Embeddable Common Lisp
use std::io::Write as _;

use eclisp::{Lisp, LispContext, LispError, ListError};

fn test_eval(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let value = lisp.eval("(+ 1 2)")?;
    assert_eq!(value.try_as_i64()?, 3);
    Ok(())
}

fn test_errors(lisp: &LispContext<'_>) -> Result<(), LispError> {
    assert!(lisp.eval("(error \"test\")").is_err());
    Ok(())
}

fn test_values(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let value = lisp.eval(r#""hello""#)?;
    assert_eq!(value.try_as_string()?, "hello");
    Ok(())
}

fn test_multiple_forms(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let result = lisp.eval(
        r#"
        (defparameter *x* 10)
        (defparameter *y* 20)

        (+ *x* *y*)
        "#,
    )?;

    assert_eq!(result.try_as_i64()?, 30);
    Ok(())
}

fn test_reader_errors(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let result = lisp.eval(
        r#"
        (defparameter *x* 10)
        (defun broken (
        "#,
    );

    assert!(result.is_err());

    let result = lisp.eval("(+ 2 3)")?;
    assert_eq!(result.try_as_i64()?, 5);

    Ok(())
}

fn test_strings(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let value = lisp.string("Hello, 世界!")?;

    assert!(value.is_string());
    assert_eq!(value.try_as_string()?, "Hello, 世界!");

    let empty = lisp.string("")?;
    assert_eq!(empty.try_as_string()?, "");

    let code = lisp.string("(+ 1 2)")?;
    assert_eq!(code.try_as_string()?, "(+ 1 2)");

    Ok(())
}

fn test_call(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let a = lisp.eval("10")?;
    let b = lisp.eval("20")?;

    let result = lisp.call("+", &[&a, &b])?;
    assert_eq!(result.try_as_i64()?, 30);

    lisp.eval(
        r#"
        (defun multiply (a b)
          (* a b))
    "#,
    )?;

    let result = lisp.call("multiply", &[&a, &b])?;
    assert_eq!(result.try_as_i64()?, 200);

    let result = lisp.call("list", &[])?;
    assert!(result.is_nil());

    assert!(lisp.call("multiply", &[&a]).is_err());

    Ok(())
}

fn test_value_constructors(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let int_value = lisp.int(42)?;
    assert_eq!(int_value.try_as_i64()?, 42);

    let float_value = lisp.float(3.15)?;
    assert_eq!(float_value.try_as_f64()?, 3.15);

    let string_value = lisp.string("Hello")?;
    assert_eq!(string_value.try_as_string()?, "Hello");

    let string_value = lisp.string("Hello\0世界")?;
    assert_eq!(string_value.try_as_string()?, "Hello\0世界");

    let list_value = lisp.list(&[&int_value, &float_value, &string_value])?;
    assert!(list_value.is_list());
    let mut iter = list_value.iter();
    assert_eq!(iter.next().unwrap()?.try_as_i64()?, 42);
    assert_eq!(iter.next().unwrap()?.try_as_f64()?, 3.15);
    assert_eq!(iter.next().unwrap()?.try_as_string()?, "Hello\0世界");
    assert!(iter.next().is_none());

    let cons_value = lisp.cons(&int_value, &float_value)?;
    assert!(cons_value.is_cons());
    assert!(cons_value.is_list());
    assert_eq!(cons_value.car()?.try_as_i64()?, 42);
    assert_eq!(cons_value.cdr()?.try_as_f64()?, 3.15);

    let mut iter = cons_value.iter();
    assert_eq!(iter.next().unwrap()?.try_as_i64()?, 42);
    assert!(matches!(iter.next(), Some(Err(ListError::ImproperList))));
    assert!(iter.next().is_none());

    Ok(())
}

fn test_big_ints(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let value = lisp.int(i64::MAX)?;
    assert_eq!(value.try_as_i64()?, i64::MAX);

    let value = lisp.int(i64::MIN)?;
    assert_eq!(value.try_as_i64()?, i64::MIN);

    Ok(())
}

fn test_improper_list(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let a = lisp.int(1)?;
    let b = lisp.int(2)?;
    let c = lisp.int(3)?;

    let tail = lisp.cons(&b, &c)?;
    let improper_list = lisp.cons(&a, &tail)?;

    assert!(improper_list.is_cons());
    assert!(improper_list.is_list());

    let mut iter = improper_list.iter();

    assert_eq!(iter.next().unwrap()?.try_as_i64()?, 1);
    assert_eq!(iter.next().unwrap()?.try_as_i64()?, 2);
    assert!(matches!(iter.next(), Some(Err(ListError::ImproperList))));
    assert!(iter.next().is_none());

    Ok(())
}

fn test_error_recovery(lisp: &LispContext<'_>) -> Result<(), LispError> {
    assert!(lisp.eval("(error \"test\")").is_err());

    let value = lisp.int(42)?;
    assert_eq!(value.try_as_i64()?, 42);

    let value = lisp.float(3.15)?;
    assert_eq!(value.try_as_f64()?, 3.15);

    let value = lisp.string("hello")?;
    assert_eq!(value.try_as_string()?, "hello");

    let list = lisp.list(&[&value])?;
    assert!(list.is_list());

    assert_eq!(lisp.eval("(+ 1 2)")?.try_as_i64()?, 3);

    Ok(())
}

fn test_value_ref(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let owned = ctx.int(42)?;
    let borrowed = owned.as_ref();

    assert!(borrowed.is_int());
    assert_eq!(borrowed.try_as_i64()?, 42);

    let second_root = ctx.root(borrowed)?;

    drop(owned);

    assert_eq!(second_root.try_as_i64()?, 42);

    Ok(())
}

fn run_file(lisp: &LispContext<'_>) -> Result<(), LispError> {
    let mut file = tempfile::NamedTempFile::new().unwrap();

    file.write_all("(defparameter *x* 42)".as_bytes()).unwrap();

    lisp.run(file.path().to_str().unwrap())?;

    Ok(())
}

fn function_refs(lisp: &Lisp) -> Result<(), LispError> {
    lisp.enter(|ctx| ctx.eval("(defun increment (x) (+ x 1))"))?;

    let func = lisp.fun("increment")?;
    let cloned = func.clone();

    let result = lisp.enter(|ctx| {
        let arg = ctx.int(5)?;
        ctx.callfun(&cloned, &[&arg])
    })?;

    assert_eq!(result.try_as_i64()?, 6);

    drop(func);
    drop(cloned);

    Ok(())
}

fn run(name: &str, lisp: &LispContext<'_>, test: fn(&LispContext<'_>) -> Result<(), LispError>) {
    print!("test {name} ... ");

    match test(lisp) {
        Ok(()) => println!("ok"),
        Err(error) => panic!("{name} failed: {error}"),
    }
}

fn run_raw(name: &str, lisp: &Lisp, test: fn(&Lisp) -> Result<(), LispError>) {
    print!("test {name} ... ");

    match test(lisp) {
        Ok(()) => println!("ok"),
        Err(error) => panic!("{name} failed: {error}"),
    }
}

fn main() -> Result<(), LispError> {
    let lisp = Lisp::new()?;

    lisp.enter(|ctx| {
        run("eval", &ctx, test_eval);
        run("errors", &ctx, test_errors);
        run("values", &ctx, test_values);
        run("multiple_forms", &ctx, test_multiple_forms);
        run("reader_errors", &ctx, test_reader_errors);
        run("strings", &ctx, test_strings);
        run("call", &ctx, test_call);
        run("value_constructors", &ctx, test_value_constructors);
        run("big_ints", &ctx, test_big_ints);
        run("improper_list", &ctx, test_improper_list);
        run("error_recovery", &ctx, test_error_recovery);
        run("value_ref", &ctx, test_value_ref);
        run("run_file", &ctx, |ctx| run_file(ctx));
    });

    run_raw("function_refs", &lisp, function_refs);

    Ok(())
}