sim-lib-lang-lua 0.1.4

Lua-style surface profile for the SIM expression runtime.
Documentation
use sim_kernel::{CapabilityName, Cx, Error, Expr, NumberLiteral, Symbol, Value};

use crate::{LuaEnv, LuaEvalPolicy, load::eval_lua_source, lua_rawget, lua_table_value};

use sim_kernel::testing::bare_cx as cx;

fn value_expr(cx: &mut Cx, value: &Value) -> Expr {
    value.object().as_expr(cx).unwrap()
}

fn string_value(cx: &mut Cx, value: &Value) -> String {
    match value_expr(cx, value) {
        Expr::String(text) => text,
        other => panic!("expected string, got {other:?}"),
    }
}

fn number_canonical(cx: &mut Cx, value: &Value) -> String {
    match value_expr(cx, value) {
        Expr::Number(NumberLiteral { canonical, .. }) => canonical,
        other => panic!("expected number, got {other:?}"),
    }
}

fn lua_form(name: &str, args: Vec<Expr>) -> Expr {
    let mut items = vec![Expr::Symbol(Symbol::qualified("lua", name))];
    items.extend(args);
    Expr::List(items)
}

fn lua_call(operator: Expr, args: Vec<Expr>) -> Expr {
    let mut values = vec![operator];
    values.extend(args);
    lua_form("call", values)
}

fn eval_values(cx: &mut Cx, env: &mut LuaEnv, expr: Expr) -> Vec<Value> {
    let policy = LuaEvalPolicy::new(cx).unwrap();
    policy.eval(cx, env, &expr).unwrap().into_values()
}

fn assert_capability(err: Error, name: &str) {
    match err {
        Error::CapabilityDenied { capability } => {
            assert_eq!(capability, CapabilityName::new(name));
        }
        other => panic!("expected capability denial, got {other:?}"),
    }
}

#[test]
fn lua_load_decodes_text_chunks_and_executes_them() {
    let mut cx = cx();
    let values = eval_lua_source(&mut cx, "return load('return 1 + 2')()").unwrap();
    assert_eq!(number_canonical(&mut cx, &values[0]), "3");

    let policy = LuaEvalPolicy::new(&mut cx).unwrap();
    let mut env = LuaEnv::new();
    policy.install_stdlib(&mut cx, &mut env).unwrap();
    let err = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("load")),
                vec![
                    Expr::String("return 1".to_owned()),
                    Expr::String("chunk".to_owned()),
                    Expr::String("b".to_owned()),
                ],
            ),
        )
        .unwrap_err();
    assert!(format!("{err}").contains("bytecode"));
}

#[test]
fn lua_math_core_functions_follow_lua_number_subtypes() {
    let mut cx = cx();
    let policy = LuaEvalPolicy::new(&mut cx).unwrap();
    let mut env = LuaEnv::new();
    policy.install_stdlib(&mut cx, &mut env).unwrap();

    let floor = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("math.floor")),
                vec![Expr::Number(NumberLiteral {
                    domain: Symbol::qualified("test", "f64"),
                    canonical: "4.8".to_owned(),
                })],
            ),
        )
        .unwrap()
        .into_values();
    assert_eq!(number_canonical(&mut cx, &floor[0]), "4");

    let sqrt = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("math.sqrt")),
                vec![Expr::Number(NumberLiteral {
                    domain: Symbol::qualified("test", "i64"),
                    canonical: "9".to_owned(),
                })],
            ),
        )
        .unwrap()
        .into_values();
    assert_eq!(number_canonical(&mut cx, &sqrt[0]), "3.0");

    let max = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("math.max")),
                vec![
                    Expr::Number(NumberLiteral {
                        domain: Symbol::qualified("test", "i64"),
                        canonical: "2".to_owned(),
                    }),
                    Expr::Number(NumberLiteral {
                        domain: Symbol::qualified("test", "i64"),
                        canonical: "7".to_owned(),
                    }),
                ],
            ),
        )
        .unwrap()
        .into_values();
    assert_eq!(number_canonical(&mut cx, &max[0]), "7");

    let ty = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("math.type")),
                vec![Expr::Number(NumberLiteral {
                    domain: Symbol::qualified("test", "i64"),
                    canonical: "7".to_owned(),
                })],
            ),
        )
        .unwrap()
        .into_values();
    assert_eq!(string_value(&mut cx, &ty[0]), "integer");
}

#[test]
fn lua_io_and_os_host_effects_fail_closed_without_capabilities() {
    let mut cx = cx();
    let policy = LuaEvalPolicy::new(&mut cx).unwrap();
    let mut env = LuaEnv::new();
    policy.install_stdlib(&mut cx, &mut env).unwrap();

    let io_err = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("io.open")),
                vec![
                    Expr::String("fixture.txt".to_owned()),
                    Expr::String("r".to_owned()),
                ],
            ),
        )
        .unwrap_err();
    assert_capability(io_err, "fs/read");

    let exec_err = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("os.execute")),
                vec![Expr::String("true".to_owned())],
            ),
        )
        .unwrap_err();
    assert_capability(exec_err, "exec");

    let getenv_err = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("os.getenv")),
                vec![Expr::String("HOME".to_owned())],
            ),
        )
        .unwrap_err();
    assert_capability(getenv_err, "env/read");
}

#[test]
fn lua_debug_safe_subset_reports_tracebacks_and_explicit_gaps() {
    let mut cx = cx();
    let policy = LuaEvalPolicy::new(&mut cx).unwrap();
    let mut env = LuaEnv::new();
    policy.install_stdlib(&mut cx, &mut env).unwrap();

    let traceback = eval_values(
        &mut cx,
        &mut env,
        lua_call(
            Expr::Local(Symbol::new("debug.traceback")),
            vec![Expr::String("boom".to_owned())],
        ),
    );
    let text = string_value(&mut cx, &traceback[0]);
    assert!(text.contains("boom"));
    assert!(text.contains("SIM Lua frame"));

    let sethook = eval_values(
        &mut cx,
        &mut env,
        lua_call(Expr::Local(Symbol::new("debug.sethook")), Vec::new()),
    );
    let kind_key = cx.factory().string("kind".to_owned()).unwrap();
    let kind = lua_rawget(&mut cx, &sethook[0], &kind_key)
        .unwrap()
        .unwrap();
    assert_eq!(string_value(&mut cx, &kind), "ExpectedGap");
}

#[test]
fn lua_package_table_exposes_searcher_order() {
    let mut cx = cx();
    let policy = LuaEvalPolicy::new(&mut cx).unwrap();
    let mut env = LuaEnv::new();
    policy.install_stdlib(&mut cx, &mut env).unwrap();

    let package = env.get(&Symbol::new("package")).unwrap();
    let searchers_key = cx.factory().string("searchers".to_owned()).unwrap();
    let searchers = lua_rawget(&mut cx, &package, &searchers_key)
        .unwrap()
        .unwrap();
    assert_eq!(
        lua_table_value(&searchers)
            .unwrap()
            .len_border(&mut cx)
            .unwrap(),
        4
    );

    let require_missing = policy
        .eval(
            &mut cx,
            &mut env,
            &lua_call(
                Expr::Local(Symbol::new("require")),
                vec![Expr::String("missing.module".to_owned())],
            ),
        )
        .unwrap_err();
    assert!(format!("{require_missing}").contains("module 'missing.module' not found"));
}