sim-lib-lang-lua 0.1.4

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

use sim_kernel::{
    Args, Callable, ClassRef, Cx, Error, Object, ObjectCompat, Result, Symbol, Value,
};
use sim_lib_pattern::{TextLimits, TextOp, compile_lua_pattern, run_text_pattern};
use sim_lib_standard_core::Arity;

use crate::{
    LuaEvalPolicy, lua_integer_value,
    pattern_replace::{lua_gsub, replacement_from_value},
    stdlib_string::{integer_arg, string_arg},
};

pub(crate) struct LuaGMatchIterator {
    subject: String,
    ops: Vec<TextOp>,
    cursor: Mutex<usize>,
}

impl LuaGMatchIterator {
    fn new(subject: String, ops: Vec<TextOp>) -> Self {
        Self {
            subject,
            ops,
            cursor: Mutex::new(0),
        }
    }
}

impl Object for LuaGMatchIterator {
    fn display(&self, _cx: &mut Cx) -> Result<String> {
        Ok("#<lua-gmatch-iterator>".to_owned())
    }

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

impl ObjectCompat for LuaGMatchIterator {
    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 LuaGMatchIterator {
    fn call(&self, cx: &mut Cx, _args: Args) -> Result<Value> {
        let policy = LuaEvalPolicy::new(cx)?;
        let values = call_lua_gmatch_iterator(cx, &policy, self)?;
        Ok(policy
            .kit()
            .adjust_values(values, Arity::AtLeastOne)
            .into_iter()
            .next()
            .unwrap_or_else(|| policy.kit().nil.clone()))
    }
}

pub(crate) fn call_lua_gmatch_iterator(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    iterator: &LuaGMatchIterator,
) -> Result<Vec<Value>> {
    let mut cursor = iterator
        .cursor
        .lock()
        .map_err(|_| Error::PoisonedLock("lua gmatch iterator"))?;
    if *cursor > iterator.subject.len() {
        return Ok(vec![policy.kit().nil.clone()]);
    }
    let Some(matched) = run_text_pattern(
        &iterator.ops,
        &iterator.subject,
        *cursor,
        TextLimits { max_steps: 20_000 },
    ) else {
        *cursor = iterator.subject.len() + 1;
        return Ok(vec![policy.kit().nil.clone()]);
    };
    *cursor = if matched.end == matched.start {
        next_char_boundary(&iterator.subject, matched.end)
    } else {
        matched.end
    };
    capture_or_match_values(cx, &iterator.subject, &matched)
}

pub(crate) fn lua_string_find(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    args: Vec<Value>,
) -> Result<Vec<Value>> {
    let subject = string_arg(cx, &args, 0, "string.find")?;
    let pattern = string_arg(cx, &args, 1, "string.find")?;
    let init = args
        .get(2)
        .map(|value| integer_arg(cx, value, "string.find init"))
        .transpose()?
        .unwrap_or(1);
    let plain = args
        .get(3)
        .map(|value| policy.kit().is_truthy(cx, value))
        .transpose()?
        .unwrap_or(false);
    let start = lua_start_offset(&subject, init);
    let Some(matched) = (if plain {
        plain_find(&subject, &pattern, start)
    } else {
        let ops = compile_lua_pattern(&pattern)?;
        run_text_pattern(&ops, &subject, start, TextLimits { max_steps: 20_000 })
    }) else {
        return Ok(vec![policy.kit().nil.clone()]);
    };
    let mut values = vec![
        lua_integer_value(cx, matched.start as i64 + 1)?,
        lua_integer_value(cx, matched.end as i64)?,
    ];
    values.extend(capture_values(cx, &subject, &matched)?);
    Ok(values)
}

pub(crate) fn lua_string_gmatch(cx: &mut Cx, args: Vec<Value>) -> Result<Vec<Value>> {
    let subject = string_arg(cx, &args, 0, "string.gmatch")?;
    let pattern = string_arg(cx, &args, 1, "string.gmatch")?;
    let ops = compile_lua_pattern(&pattern)?;
    cx.factory()
        .opaque(Arc::new(LuaGMatchIterator::new(subject, ops)))
        .map(|value| vec![value])
}

pub(crate) fn lua_string_gsub(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    args: Vec<Value>,
) -> Result<Vec<Value>> {
    let subject = string_arg(cx, &args, 0, "string.gsub")?;
    let pattern = string_arg(cx, &args, 1, "string.gsub")?;
    let replacement = args
        .get(2)
        .cloned()
        .ok_or_else(|| Error::Eval("string.gsub requires a replacement".to_owned()))?;
    let limit = args
        .get(3)
        .map(|value| integer_arg(cx, value, "string.gsub limit"))
        .transpose()?;
    let ops = compile_lua_pattern(&pattern)?;
    let replacement = replacement_from_value(cx, replacement)?;
    let (text, count) = lua_gsub(cx, policy, &subject, &ops, &replacement, limit)?;
    Ok(vec![
        cx.factory().string(text)?,
        lua_integer_value(cx, count)?,
    ])
}

pub(crate) fn lua_string_match(
    cx: &mut Cx,
    policy: &LuaEvalPolicy,
    args: Vec<Value>,
) -> Result<Vec<Value>> {
    let subject = string_arg(cx, &args, 0, "string.match")?;
    let pattern = string_arg(cx, &args, 1, "string.match")?;
    let init = args
        .get(2)
        .map(|value| integer_arg(cx, value, "string.match init"))
        .transpose()?
        .unwrap_or(1);
    let ops = compile_lua_pattern(&pattern)?;
    let Some(matched) = run_text_pattern(
        &ops,
        &subject,
        lua_start_offset(&subject, init),
        TextLimits { max_steps: 20_000 },
    ) else {
        return Ok(vec![policy.kit().nil.clone()]);
    };
    capture_or_match_values(cx, &subject, &matched)
}

fn capture_or_match_values(
    cx: &mut Cx,
    subject: &str,
    matched: &sim_lib_pattern::TextMatch,
) -> Result<Vec<Value>> {
    let values = capture_values(cx, subject, matched)?;
    if values.is_empty() {
        return cx
            .factory()
            .string(subject[matched.start..matched.end].to_owned())
            .map(|value| vec![value]);
    }
    Ok(values)
}

fn capture_values(
    cx: &mut Cx,
    subject: &str,
    matched: &sim_lib_pattern::TextMatch,
) -> Result<Vec<Value>> {
    matched
        .captures
        .iter()
        .map(|(start, end)| {
            cx.factory()
                .string(subject.get(*start..*end).unwrap_or("").to_owned())
        })
        .collect()
}

fn plain_find(subject: &str, pattern: &str, start: usize) -> Option<sim_lib_pattern::TextMatch> {
    let offset = subject.get(start..)?.find(pattern)? + start;
    Some(sim_lib_pattern::TextMatch {
        start: offset,
        end: offset + pattern.len(),
        captures: Vec::new(),
    })
}

fn lua_start_offset(subject: &str, init: i64) -> usize {
    let raw = if init >= 0 {
        init - 1
    } else {
        subject.len() as i64 + init
    };
    clamp_char_boundary(subject, raw.max(0) as usize)
}

fn clamp_char_boundary(subject: &str, offset: usize) -> usize {
    let mut offset = offset.min(subject.len());
    while offset > 0 && !subject.is_char_boundary(offset) {
        offset -= 1;
    }
    offset
}

fn next_char_boundary(subject: &str, cursor: usize) -> usize {
    if cursor >= subject.len() {
        return subject.len() + 1;
    }
    let mut next = cursor + 1;
    while next < subject.len() && !subject.is_char_boundary(next) {
        next += 1;
    }
    next
}