mumu 0.9.1

Lava Mumu is a language for those in the now and that know
Documentation
// FILE: src/modules/extend.rs

use crate::parser::interpreter::eval::eval_expression;  // <-- Changed here
use crate::parser::tokens::tokenize_pick_args;
use crate::parser::interpreter::Interpreter;
use crate::parser::types::Value;
use libloading::{Library, Symbol};
use std::env::consts::{DLL_PREFIX, DLL_SUFFIX};
use std::ffi::c_void;
use std::path::{Path, PathBuf};

/// The user calls `extend("somePlugin")` or `extend("plugins/net", "someArg")`.
/// We build the platform‐specific name (like 'liblavanet.so') then load it.
/// After the plugin's Cargo.lock symbol runs, we return `StrArray` of all *new* function names
/// that the plugin introduced (or empty if none).
pub fn handle_extend_call(interp: &mut Interpreter, expr: &str) -> Result<Value, String> {
    // 1) Parse out arguments from `expr`, e.g. extend("plugins/net", "someArg")
    //    by removing `extend(` and `)`, then tokenizing
    let inside = &expr["extend(".len()..expr.len() - 1].trim();
    let parts = tokenize_pick_args(inside, interp.is_verbose())?;

    if parts.is_empty() {
        return Err("extend(...) requires at least one argument (the library name/path)".to_string());
    }
    if parts.len() > 2 {
        return Err("extend(...) => at most 2 arguments: (libBaseName[, extraString])".to_string());
    }

    // Evaluate first argument => base plugin name/path
    let so_path_val = eval_expression(interp, &parts[0])?;
    let so_path_raw = match so_path_val {
        Value::StrArray(ref ss) if ss.len() == 1 => ss[0].clone(),
        _ => {
            return Err(
                "extend(...) => first arg must be a single string for the plugin name/path".to_string()
            );
        }
    };

    // Evaluate optional second argument => if provided
    let maybe_extra: Option<String> = if parts.len() == 2 {
        let val2 = eval_expression(interp, &parts[1])?;
        match val2 {
            Value::StrArray(ref ss) if ss.len() == 1 => Some(ss[0].clone()),
            Value::IntArray(ref ia) if ia.len() == 1 => Some(ia[0].to_string()),
            Value::Int(i) => Some(i.to_string()),
            _ => None,
        }
    } else {
        None
    };

    // 2) For "myPlugin", build the actual library file name => "liblavamyPlugin.so" (Linux) or platform variant.
    //    We no longer remove trailing letters from the plugin name. Instead, we only remove literal
    //    known file extensions if present, e.g. ".so", ".dll", ".dylib".
    let user_path = Path::new(&so_path_raw);
    let filename = match user_path.file_name() {
        Some(osfn) => osfn.to_string_lossy().into_owned(),
        None => return Err(format!("extend => cannot extract filename from '{}'", so_path_raw)),
    };
    let parent_dir = user_path.parent().unwrap_or_else(|| Path::new(""));

    // Instead of trimming characters like 's' or 'o', we only remove known extensions:
    let base_str = filename
        .trim_end_matches(".so")
        .trim_end_matches(".dll")
        .trim_end_matches(".dylib");
    let actual_filename = format!("{}lava{}{}", DLL_PREFIX, base_str, DLL_SUFFIX);

    // Recombine parent directory + actual_filename
    let full_path: PathBuf = parent_dir.join(&actual_filename);

    if interp.is_verbose() {
        eprintln!("[extend] Attempting to load library => '{}'", full_path.display());
    }
    // 3) Before loading, track which dynamic function names exist so we can see what's newly imported
    let old_names = interp
        .get_dynamic_functions_for_clone()
        .keys()
        .map(|k| k.clone())
        .collect::<Vec<_>>();

    // 4) Attempt to load library
    let lib_res = unsafe { Library::new(&full_path) };
    let lib = match lib_res {
        Ok(l) => l,
        Err(e) => {
            return Err(format!("extend => could not load '{}': {}", full_path.display(), e));
        }
    };

    // 5) If it has a symbol named "Cargo_lock", call it with (interp_ptr, extra_cstr?)
    unsafe {
        let maybe_init: std::result::Result<
            Symbol<unsafe extern "C" fn(*mut c_void, *const c_void) -> i32>,
            libloading::Error,
        > = lib.get(b"Cargo_lock");

        if let Ok(init_fn) = maybe_init {
            let interp_ptr = interp as *mut Interpreter as *mut c_void;
            let extra_ptr: *const c_void = if let Some(ref extra_str) = maybe_extra {
                extra_str.as_ptr() as *const c_void
            } else {
                std::ptr::null()
            };

            if interp.is_verbose() {
                eprintln!("[extend] Found Cargo_lock => calling it now...");
            }

            let rc = init_fn(interp_ptr, extra_ptr);
            if rc != 0 {
                return Err(format!(
                    "extend => plugin '{}' returned error code {} from Cargo_lock()",
                    full_path.display(),
                    rc
                ));
            }

            if interp.is_verbose() {
                eprintln!(
                    "[extend] plugin '{}' => Cargo_lock() success. Extra arg = {:?}",
                    full_path.display(),
                    maybe_extra
                );
            }
        } else if interp.is_verbose() {
            eprintln!(
                "[extend] plugin '{}' => no Cargo_lock symbol found. Kept loaded.",
                full_path.display()
            );
        }
    }

    // 6) Keep the Library loaded by storing it in the Interpreter
    interp.add_library_handle(lib);

    // 7) Now gather the new dynamic function names
    let new_set = interp
        .get_dynamic_functions_for_clone()
        .keys()
        .map(|k| k.clone())
        .collect::<Vec<_>>();
    let mut newly_imported = Vec::new();
    for nm in new_set {
        if !old_names.contains(&nm) {
            newly_imported.push(nm);
        }
    }
    newly_imported.sort();

    // 8) Return StrArray of newly imported function names
    Ok(Value::StrArray(newly_imported))
}