eclisp 0.2.0

Bindings for Embeddable Common Lisp
use eclisp::{Lisp, LispClass, LispContext, LispError, lisp_impl};

#[derive(LispClass)]
#[lisp(name = "impl-player", impl)]
struct Player {
    #[lisp]
    health: i32,
}

#[lisp_impl]
impl Player {
    #[method]
    fn damage(&mut self, amount: i32) {
        self.health -= amount;
    }

    #[method]
    fn is_alive(&self) -> bool {
        self.health > 0
    }

    #[method(name = "maximum-health")]
    fn max_health() -> i32 {
        100
    }

    #[method]
    fn checked_health(&self) -> Result<i32, LispError> {
        Ok(self.health)
    }

    #[method]
    fn validate_damage(&self, amount: i32) -> Result<(), LispError> {
        if amount < 0 {
            return Err(LispError::RuntimeError("Negative damage".into()));
        }

        Ok(())
    }

    #[method]
    fn panic_native(&self) {
        panic!("method panic")
    }
}

#[lisp_impl(name = "player_combat")]
impl Player {
    #[method]
    fn heal(&mut self, amount: i32) {
        self.health += amount;
    }
}

#[lisp_impl(name = "player_movement")]
impl Player {
    #[method]
    fn move_by(&self, dx: f32, dy: f32) -> f32 {
        dx + dy
    }
}

#[derive(LispClass)]
#[lisp(name = "impl-other")]
struct Other {
    #[lisp]
    value: i32,
}

fn test_generated_methods(ctx: &LispContext<'_>) -> Result<(), LispError> {
    ctx.defclass(Player::class())?;
    ctx.defclass(Other::class())?;
    ctx.defimpl(player_combat::methods())?;
    ctx.defimpl(player_movement::methods())?;

    let mut player = Player { health: 75 };

    ctx.with(&mut player, |object| {
        let amount = ctx.int(25)?;
        ctx.call("impl-player-damage", &[object, &amount])?;
        assert_eq!(ctx.call("impl-player-health", &[object])?.try_as_i64()?, 50);
        assert!(ctx.call("impl-player-is-alive", &[object])?.try_as_bool()?);
        assert_eq!(
            ctx.call("impl-player-checked-health", &[object])?
                .try_as_i64()?,
            50
        );

        let heal = ctx.int(10)?;
        ctx.call("impl-player-heal", &[object, &heal])?;
        assert_eq!(ctx.call("impl-player-health", &[object])?.try_as_i64()?, 60);

        let dx = ctx.float(2.5)?;
        let dy = ctx.float(4.0)?;
        assert_eq!(
            ctx.call("impl-player-move-by", &[object, &dx, &dy])?
                .try_as_f64()?,
            6.5
        );

        Ok(())
    })?;

    assert_eq!(player.health, 60);
    assert_eq!(ctx.eval("(impl-player-maximum-health)")?.try_as_i64()?, 100);

    Ok(())
}

fn test_generated_errors(ctx: &LispContext<'_>) -> Result<(), LispError> {
    let mut player = Player { health: 50 };

    ctx.with(&mut player, |object| {
        let invalid = ctx.string("invalid")?;
        let conversion = ctx
            .call("impl-player-damage", &[object, &invalid])
            .unwrap_err()
            .to_string();
        assert!(conversion.contains("Native method impl-player-damage failed"));
        assert!(conversion.contains("Expected integer, got other"));

        let negative = ctx.int(-1)?;
        let result = ctx
            .call("impl-player-validate-damage", &[object, &negative])
            .unwrap_err()
            .to_string();
        assert!(result.contains("Negative damage"));

        let panic = ctx
            .call("impl-player-panic-native", &[object])
            .unwrap_err()
            .to_string();
        assert!(panic.contains("method panic"));

        assert_eq!(ctx.eval("(+ 1 2)")?.try_as_i64()?, 3);
        Ok(())
    })?;

    assert_eq!(player.health, 50);
    Ok(())
}

fn test_invalid_handles(ctx: &LispContext<'_>) -> Result<(), LispError> {
    ctx.eval(
        r#"
(defparameter *escaped-impl-player* nil)
(defun escape-impl-player (obj)
  (setf *escaped-impl-player* obj))
"#,
    )?;

    let mut player = Player { health: 50 };

    ctx.with(&mut player, |object| {
        ctx.call("escape-impl-player", &[object])?;
        Ok(())
    })?;

    assert!(
        ctx.eval("(impl-player-is-alive *escaped-impl-player*)")
            .is_err()
    );

    let mut other = Other { value: 10 };

    ctx.with(&mut other, |object| {
        let handle = ctx.call("%rust-handle", &[object])?;
        let class = ctx.eval("'impl-player")?;
        let keyword = ctx.eval(":handle")?;
        let forged = ctx.call("make-instance", &[&class, &keyword, &handle])?;

        assert!(ctx.call("impl-player-is-alive", &[&forged]).is_err());
        Ok(())
    })?;

    assert_eq!(ctx.eval("(+ 2 3)")?.try_as_i64()?, 5);
    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 main() -> Result<(), LispError> {
    let lisp = Lisp::new()?;

    lisp.enter(|ctx| {
        run("generated_methods", &ctx, test_generated_methods);
        run("generated_errors", &ctx, test_generated_errors);
        run("invalid_handles", &ctx, test_invalid_handles);
    });

    Ok(())
}