mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
use super::*;
use crate::ast::Range;
use crate::parser::arithm::parse_arithm_expr_strict;

const MAX_ARITHMETIC_VALUE_REPARSE_DEPTH: usize = 1024;

#[derive(Default)]
struct ArithmeticEvalContext {
    reparsed_variables: Vec<String>,
}

impl ArithmeticEvalContext {
    fn enter_reparsed_variable(&mut self, name: &str) -> Result<(), String> {
        if self.reparsed_variables.iter().any(|active| active == name) {
            return Err(format!("recursive arithmetic variable: {name}"));
        }
        if self.reparsed_variables.len() >= MAX_ARITHMETIC_VALUE_REPARSE_DEPTH {
            return Err("arithmetic variable recursion limit exceeded".to_string());
        }
        self.reparsed_variables.push(name.to_string());
        Ok(())
    }

    fn leave_reparsed_variable(&mut self) {
        self.reparsed_variables.pop();
    }
}

pub(super) fn eval_arithm(state: &mut ShellState, expr: &ArithmExpr) -> Result<i64, String> {
    let mut context = ArithmeticEvalContext::default();
    eval_arithm_with_context(state, expr, &mut context)
}

fn parse_arithmetic_variable_value(value: &str) -> Result<ArithmExpr, String> {
    match parse_arithm_expr_strict(value) {
        Ok(parsed) => Ok(parsed),
        Err(_) if value == i64::MIN.to_string() => {
            Ok(ArithmExpr::literal(i64::MIN, Range::unknown()))
        }
        Err(_) => Err("Illegal number".to_string()),
    }
}

fn eval_variable_arithm_value(
    state: &mut ShellState,
    context: &mut ArithmeticEvalContext,
    name: &str,
) -> Result<i64, String> {
    let Some(value) = state.env_get(name).map(str::to_owned) else {
        return Ok(0);
    };
    if value.is_empty() {
        return Ok(0);
    }
    let parsed = parse_arithmetic_variable_value(&value)?;
    context.enter_reparsed_variable(name)?;
    let result = eval_arithm_with_context(state, &parsed, context);
    context.leave_reparsed_variable();
    result
}

fn eval_assignment_current_value(
    state: &mut ShellState,
    context: &mut ArithmeticEvalContext,
    name: &str,
    op: ArithmAssignOp,
) -> Result<i64, String> {
    if assign_op_as_binop(op).is_some() {
        eval_variable_arithm_value(state, context, name)
    } else {
        Ok(0)
    }
}

fn eval_arithm_with_context(
    state: &mut ShellState,
    expr: &ArithmExpr,
    context: &mut ArithmeticEvalContext,
) -> Result<i64, String> {
    match expr {
        ArithmExpr::Literal(n) => Ok(n.value()),
        ArithmExpr::Raw(expr) => parse_arithm_expr_strict(expr.expr())
            .map_err(|err| {
                let expr = expr.expr();
                format!(
                    "{}: \"{}\"",
                    err.message,
                    expr[err.position.min(expr.len())..].trim()
                )
            })
            .and_then(|parsed| eval_arithm_with_context(state, &parsed, context)),
        ArithmExpr::Variable(name) => {
            let var = name.name().strip_prefix('$').unwrap_or(name.name());
            eval_variable_arithm_value(state, context, var)
        }
        ArithmExpr::BinOp(binary) => {
            let l = eval_arithm_with_context(state, binary.left(), context)?;
            match binary.op() {
                ArithmBinOp::LogAnd if l == 0 => return Ok(0),
                ArithmBinOp::LogOr if l != 0 => return Ok(1),
                _ => {}
            }
            let r = eval_arithm_with_context(state, binary.right(), context)?;
            eval_arithm_binop(binary.op(), l, r).map_err(str::to_string)
        }
        ArithmExpr::UnOp(unary) => {
            let v = eval_arithm_with_context(state, unary.operand(), context)?;
            Ok(match unary.op() {
                ArithmUnOp::Plus => v,
                ArithmUnOp::Minus => v.wrapping_neg(),
                ArithmUnOp::BitNot => !v,
                ArithmUnOp::LogNot => i64::from(v == 0),
            })
        }
        ArithmExpr::Cond(cond) => {
            if eval_arithm_with_context(state, cond.cond(), context)? != 0 {
                eval_arithm_with_context(state, cond.then_branch(), context)
            } else {
                eval_arithm_with_context(state, cond.else_branch(), context)
            }
        }
        ArithmExpr::Assign(assign) => {
            let rhs = eval_arithm_with_context(state, assign.value(), context)?;
            let current =
                eval_assignment_current_value(state, context, assign.name(), assign.op())?;
            let result = eval_assign_op(assign.op(), current, rhs).map_err(str::to_string)?;
            let attrib = if state.has_option(OPT_ALLEXPORT) {
                VAR_EXPORT
            } else {
                0
            };
            if !state.env_set(assign.name(), result.to_string(), attrib) {
                readonly_assignment_status(state, assign.name(), "", true);
                return Err("readonly variable".to_string());
            }
            Ok(result)
        }
    }
}

fn checked_shift(rhs: i64) -> Result<u32, &'static str> {
    if !(0..i64::BITS as i64).contains(&rhs) {
        return Err("invalid shift count");
    }
    Ok(rhs as u32)
}

fn eval_checked_arithm_binop(op: ArithmBinOp, l: i64, r: i64) -> Result<i64, &'static str> {
    Ok(match op {
        ArithmBinOp::Add => l.wrapping_add(r),
        ArithmBinOp::Sub => l.wrapping_sub(r),
        ArithmBinOp::Mul => l.wrapping_mul(r),
        ArithmBinOp::Div => {
            if r == 0 {
                return Err("division by zero");
            }
            l.wrapping_div(r)
        }
        ArithmBinOp::Mod => {
            if r == 0 {
                return Err("division by zero");
            }
            l.wrapping_rem(r)
        }
        ArithmBinOp::Shl => l.wrapping_shl(checked_shift(r)?),
        ArithmBinOp::Shr => l.wrapping_shr(checked_shift(r)?),
        ArithmBinOp::LessThan => i64::from(l < r),
        ArithmBinOp::LessEq => i64::from(l <= r),
        ArithmBinOp::GreaterThan => i64::from(l > r),
        ArithmBinOp::GreaterEq => i64::from(l >= r),
        ArithmBinOp::Equal => i64::from(l == r),
        ArithmBinOp::NotEqual => i64::from(l != r),
        ArithmBinOp::BitAnd => l & r,
        ArithmBinOp::BitXor => l ^ r,
        ArithmBinOp::BitOr => l | r,
        ArithmBinOp::LogAnd => i64::from(l != 0 && r != 0),
        ArithmBinOp::LogOr => i64::from(l != 0 || r != 0),
    })
}

pub(super) fn eval_arithm_binop(op: ArithmBinOp, l: i64, r: i64) -> Result<i64, &'static str> {
    eval_checked_arithm_binop(op, l, r)
}

fn assign_op_as_binop(op: ArithmAssignOp) -> Option<ArithmBinOp> {
    match op {
        ArithmAssignOp::Equal => None,
        ArithmAssignOp::MulEq => Some(ArithmBinOp::Mul),
        ArithmAssignOp::DivEq => Some(ArithmBinOp::Div),
        ArithmAssignOp::ModEq => Some(ArithmBinOp::Mod),
        ArithmAssignOp::AddEq => Some(ArithmBinOp::Add),
        ArithmAssignOp::SubEq => Some(ArithmBinOp::Sub),
        ArithmAssignOp::ShlEq => Some(ArithmBinOp::Shl),
        ArithmAssignOp::ShrEq => Some(ArithmBinOp::Shr),
        ArithmAssignOp::AndEq => Some(ArithmBinOp::BitAnd),
        ArithmAssignOp::XorEq => Some(ArithmBinOp::BitXor),
        ArithmAssignOp::OrEq => Some(ArithmBinOp::BitOr),
    }
}

pub(super) fn eval_assign_op(
    op: ArithmAssignOp,
    current: i64,
    rhs: i64,
) -> Result<i64, &'static str> {
    match assign_op_as_binop(op) {
        Some(binop) => eval_checked_arithm_binop(binop, current, rhs),
        None => Ok(rhs),
    }
}