sim-lib-lang-lua 0.1.4

Lua-style surface profile for the SIM expression runtime.
Documentation
use std::sync::Arc;

use sim_kernel::{
    Args, Callable, ClassRef, Cx, Error, Expr, Object, ObjectCompat, Result, Symbol, Value,
};
use sim_lib_standard_core::{Arity, SharedOrganRuntime};

use crate::{
    LuaEvalPolicy, call::call_lua_value, lua_core_profile, lua_integer_value, lua_rawget,
    lua_rawset, lua_table_from_values, stdlib_debug::lua_expected_gap_table,
};

#[derive(Clone, Copy)]
pub(crate) enum LuaPackageKind {
    Require,
    PreloadSearcher,
    SourceSearcher,
    CSearcher,
    AllInOneSearcher,
    SearchPath,
    LoadLib,
}

impl LuaPackageKind {
    fn env_name(self) -> &'static str {
        match self {
            Self::Require => "require",
            Self::PreloadSearcher => "preload-searcher",
            Self::SourceSearcher => "source-searcher",
            Self::CSearcher => "c-searcher",
            Self::AllInOneSearcher => "all-in-one-searcher",
            Self::SearchPath => "searchpath",
            Self::LoadLib => "loadlib",
        }
    }

    fn function_symbol(self) -> Symbol {
        Symbol::qualified("lua/package", self.env_name())
    }
}

#[derive(Clone)]
pub(crate) struct LuaPackageFunction {
    kind: LuaPackageKind,
    package_table: Option<Value>,
    preload_table: Option<Value>,
}

impl LuaPackageFunction {
    fn new(kind: LuaPackageKind) -> Self {
        Self {
            kind,
            package_table: None,
            preload_table: None,
        }
    }

    fn with_package(kind: LuaPackageKind, package_table: Value) -> Self {
        Self {
            kind,
            package_table: Some(package_table),
            preload_table: None,
        }
    }

    fn with_preload(kind: LuaPackageKind, preload_table: Value) -> Self {
        Self {
            kind,
            package_table: None,
            preload_table: Some(preload_table),
        }
    }

    pub(crate) fn kind(&self) -> LuaPackageKind {
        self.kind
    }
}

impl Object for LuaPackageFunction {
    fn display(&self, _cx: &mut Cx) -> Result<String> {
        Ok(format!("#<lua-package-function {}>", self.kind.env_name()))
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

impl ObjectCompat for LuaPackageFunction {
    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
        cx.resolve_class(&Symbol::qualified("core", "Function"))
    }

    fn as_callable(&self) -> Option<&dyn Callable> {
        Some(self)
    }
}

impl Callable for LuaPackageFunction {
    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
        let policy = LuaEvalPolicy::new(cx)?;
        let values = run_lua_package_function(cx, &policy, self, args.into_vec())?;
        Ok(policy
            .kit()
            .adjust_values(values, Arity::AtLeastOne)
            .into_iter()
            .next()
            .unwrap_or_else(|| policy.kit().nil.clone()))
    }
}

pub(crate) fn install_lua_package_stdlib(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    env: &mut crate::LuaEnv,
) -> Result<()> {
    let mut runtime = SharedOrganRuntime::new();
    let profile = lua_core_profile();
    let profile_symbol = profile.symbol.clone();
    runtime.register_profile(profile)?;
    runtime.register_kit(&profile_symbol, policy.kit().clone())?;

    let loaded = lua_table_from_values(cx, Vec::new())?;
    let preload = lua_table_from_values(cx, Vec::new())?;
    let searchers = lua_package_searchers(cx, &mut runtime, &profile_symbol, preload.clone())?;
    let searchpath = package_function(
        cx,
        &mut runtime,
        &profile_symbol,
        LuaPackageFunction::new(LuaPackageKind::SearchPath),
    )?;
    let loadlib = package_function(
        cx,
        &mut runtime,
        &profile_symbol,
        LuaPackageFunction::new(LuaPackageKind::LoadLib),
    )?;
    let package = lua_table_from_values(
        cx,
        vec![
            (cx.factory().string("loaded".to_owned())?, loaded),
            (cx.factory().string("preload".to_owned())?, preload),
            (cx.factory().string("searchers".to_owned())?, searchers),
            (
                cx.factory().string("path".to_owned())?,
                cx.factory().string("./?.lua;./?/init.lua".to_owned())?,
            ),
            (
                cx.factory().string("cpath".to_owned())?,
                cx.factory().string(String::new())?,
            ),
            (
                cx.factory().string("config".to_owned())?,
                cx.factory().string("/\n;\n?\n!\n-".to_owned())?,
            ),
            (
                cx.factory().string("searchpath".to_owned())?,
                searchpath.clone(),
            ),
            (cx.factory().string("loadlib".to_owned())?, loadlib.clone()),
        ],
    )?;
    let require = package_function(
        cx,
        &mut runtime,
        &profile_symbol,
        LuaPackageFunction::with_package(LuaPackageKind::Require, package.clone()),
    )?;

    define_or_assign(env, Symbol::new("package"), package)?;
    define_or_assign(env, Symbol::new("package.searchpath"), searchpath)?;
    define_or_assign(env, Symbol::new("package.loadlib"), loadlib)?;
    define_or_assign(env, Symbol::new("require"), require)
}

pub(crate) fn run_lua_package_function(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    function: &LuaPackageFunction,
    args: Vec<Value>,
) -> Result<Vec<Value>> {
    match function.kind {
        LuaPackageKind::Require => lua_require(cx, policy, function, args),
        LuaPackageKind::PreloadSearcher => lua_preload_searcher(cx, policy, function, args),
        LuaPackageKind::SourceSearcher
        | LuaPackageKind::CSearcher
        | LuaPackageKind::AllInOneSearcher => lua_gap_searcher(cx, policy, function.kind, args),
        LuaPackageKind::SearchPath => lua_searchpath(cx, policy, args),
        LuaPackageKind::LoadLib => lua_loadlib_gap(cx),
    }
}

fn lua_package_searchers(
    cx: &mut Cx,
    runtime: &mut SharedOrganRuntime,
    profile_symbol: &Symbol,
    preload: Value,
) -> Result<Value> {
    let kinds = [
        LuaPackageFunction::with_preload(LuaPackageKind::PreloadSearcher, preload),
        LuaPackageFunction::new(LuaPackageKind::SourceSearcher),
        LuaPackageFunction::new(LuaPackageKind::CSearcher),
        LuaPackageFunction::new(LuaPackageKind::AllInOneSearcher),
    ];
    let mut entries = Vec::new();
    for (index, function) in kinds.into_iter().enumerate() {
        entries.push((
            lua_integer_value(cx, index as i64 + 1)?,
            package_function(cx, runtime, profile_symbol, function)?,
        ));
    }
    lua_table_from_values(cx, entries)
}

fn package_function(
    cx: &mut Cx,
    runtime: &mut SharedOrganRuntime,
    profile_symbol: &Symbol,
    function: LuaPackageFunction,
) -> Result<Value> {
    let kind = function.kind();
    let value = cx.factory().opaque(Arc::new(function))?;
    runtime.define_function(
        profile_symbol,
        sim_lib_dispatch::dispatch_organ_symbol(),
        kind.function_symbol(),
        value.clone(),
    )?;
    Ok(value)
}

fn lua_require(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    function: &LuaPackageFunction,
    args: Vec<Value>,
) -> Result<Vec<Value>> {
    let module = string_arg(cx, &args, 0, "require module")?;
    let package = function
        .package_table
        .as_ref()
        .ok_or_else(|| Error::Eval("require is missing package table".to_owned()))?;
    let loaded = table_field(cx, package, "loaded")?;
    let module_key = cx.factory().string(module.clone())?;
    let loaded_value = lua_rawget(cx, &loaded, &module_key)?;
    if let Some(value) = loaded_value
        && !matches!(value.object().as_expr(cx)?, Expr::Nil)
    {
        return Ok(vec![value]);
    }

    let searchers = table_field(cx, package, "searchers")?;
    let mut notes = Vec::new();
    for index in 1..=4 {
        let key = lua_integer_value(cx, index)?;
        let Some(searcher) = lua_rawget(cx, &searchers, &key)? else {
            continue;
        };
        let found = call_lua_value(
            cx,
            policy,
            searcher,
            vec![cx.factory().string(module.clone())?],
        )?;
        match found.as_slice() {
            [loader, ..] if loader.object().as_callable().is_some() => {
                let mut values = call_lua_value(
                    cx,
                    policy,
                    loader.clone(),
                    vec![cx.factory().string(module.clone())?],
                )?;
                let result = values
                    .drain(..1)
                    .next()
                    .filter(|value| !matches!(value.object().as_expr(cx), Ok(Expr::Nil)))
                    .unwrap_or_else(|| cx.factory().bool(true).unwrap());
                cache_loaded(cx, &loaded, module_key, result.clone())?;
                return Ok(vec![result]);
            }
            [nil, note, ..] if matches!(nil.object().as_expr(cx)?, Expr::Nil) => {
                notes.push(note.object().display(cx)?);
            }
            _ => {}
        }
    }
    Err(Error::Eval(format!(
        "module '{module}' not found{}",
        if notes.is_empty() {
            String::new()
        } else {
            format!(": {}", notes.join("; "))
        }
    )))
}

fn lua_preload_searcher(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    function: &LuaPackageFunction,
    args: Vec<Value>,
) -> Result<Vec<Value>> {
    let module = string_arg(cx, &args, 0, "package preload searcher module")?;
    let preload = function
        .preload_table
        .as_ref()
        .ok_or_else(|| Error::Eval("preload searcher is missing preload table".to_owned()))?;
    let key = cx.factory().string(module.clone())?;
    match lua_rawget(cx, preload, &key)? {
        Some(loader) => Ok(vec![loader]),
        None => Ok(vec![
            policy.kit().nil.clone(),
            cx.factory()
                .string(format!("no field package.preload['{module}']"))?,
        ]),
    }
}

fn lua_gap_searcher(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    kind: LuaPackageKind,
    args: Vec<Value>,
) -> Result<Vec<Value>> {
    let module = string_arg(cx, &args, 0, "package searcher module")?;
    let lane = kind.env_name();
    Ok(vec![
        policy.kit().nil.clone(),
        cx.factory().string(format!(
            "{lane} has no loadable package source for '{module}'"
        ))?,
    ])
}

fn lua_searchpath(cx: &mut Cx, policy: &LuaEvalPolicy, args: Vec<Value>) -> Result<Vec<Value>> {
    let module = string_arg(cx, &args, 0, "package.searchpath module")?;
    Ok(vec![
        policy.kit().nil.clone(),
        cx.factory()
            .string(format!("no Lua package path resolved for '{module}'"))?,
    ])
}

fn lua_loadlib_gap(cx: &mut Cx) -> Result<Vec<Value>> {
    lua_expected_gap_table(
        cx,
        "lua.c-api",
        "Lua C API package loading is outside the source runtime",
    )
    .map(|value| vec![value])
}

fn cache_loaded(cx: &mut Cx, loaded: &Value, key: Value, value: Value) -> Result<()> {
    match lua_rawset(cx, loaded, key, value) {
        Ok(()) | Err(Error::CapabilityDenied { .. }) => Ok(()),
        Err(err) => Err(err),
    }
}

fn table_field(cx: &mut Cx, table: &Value, field: &str) -> Result<Value> {
    let key = cx.factory().string(field.to_owned())?;
    lua_rawget(cx, table, &key)?.ok_or_else(|| Error::Eval(format!("package.{field} missing")))
}

fn string_arg(cx: &mut Cx, args: &[Value], index: usize, context: &str) -> Result<String> {
    let value = args
        .get(index)
        .ok_or_else(|| Error::Eval(format!("{context} requires a string")))?;
    match value.object().as_expr(cx)? {
        Expr::String(text) => Ok(text),
        _ => Err(Error::TypeMismatch {
            expected: "string",
            found: "non-string",
        }),
    }
}

fn define_or_assign(env: &mut crate::LuaEnv, name: Symbol, value: Value) -> Result<()> {
    if env.contains(&name) {
        env.assign(&name, value)?;
    } else {
        env.define(name, value)?;
    }
    Ok(())
}