pnc-engine 0.2.0

Core engine for PNC: arbitrary-precision RPN evaluation, units, and persistence
Documentation
use crate::error::PncError;
use crate::ops;
use crate::unit::{self, UnitId};
use crate::value::Value;
use num_bigint::BigInt;
use num_traits::Num;

fn parse_unit_symbol(s: &str) -> Option<UnitId> {
    UnitId::from_symbol(s)
}

fn has_prefix(s: &str) -> bool {
    let body = s.strip_prefix('-').unwrap_or(s);
    body.starts_with("0b")
        || body.starts_with("0B")
        || body.starts_with("0o")
        || body.starts_with("0O")
        || body.starts_with("0x")
        || body.starts_with("0X")
}

fn parse_integer(tok: &str) -> Option<Value> {
    let (negative, body) = if let Some(rest) = tok.strip_prefix('-') {
        (true, rest)
    } else {
        (false, tok)
    };

    // Reject leading +.
    if body.starts_with('+') {
        return None;
    }

    // Detect optional base prefix.
    let (base, digits) =
        if let Some(rest) = body.strip_prefix("0b").or_else(|| body.strip_prefix("0B")) {
            (2u32, rest)
        } else if let Some(rest) = body.strip_prefix("0o").or_else(|| body.strip_prefix("0O")) {
            (8u32, rest)
        } else if let Some(rest) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
            (16u32, rest)
        } else {
            (10u32, body)
        };

    // Reject empty body.
    if digits.is_empty() {
        return None;
    }

    // Reject underscores and leading + in the digit body.
    if digits.contains('_') || digits.starts_with('+') {
        return None;
    }

    let n = if base == 10 {
        digits.parse::<BigInt>().ok()?
    } else {
        // Lowercase hex digits for consistent parsing.
        if base == 16 {
            BigInt::from_str_radix(&digits.to_lowercase(), base).ok()?
        } else {
            BigInt::from_str_radix(digits, base).ok()?
        }
    };

    if negative {
        Some(Value::Int(-n))
    } else {
        Some(Value::Int(n))
    }
}

fn parse_number(tok: &str) -> Option<Value> {
    // Rational branch: split on '/'.
    if let Some((a, b)) = tok.split_once('/') {
        let a = a.trim();
        let b = b.trim();

        // Reject prefixed components.
        if has_prefix(a) || has_prefix(b) {
            return None;
        }

        // Numerator: optional leading -.
        let (neg_n, n_body) = if let Some(rest) = a.strip_prefix('-') {
            (true, rest)
        } else {
            (false, a)
        };

        // Reject +, underscores in numerator.
        if n_body.starts_with('+') || n_body.contains('_') {
            return None;
        }

        let n: BigInt = n_body.parse().ok()?;

        // Denominator: must be positive, no sign, no prefix.
        if b.starts_with('-') || b.starts_with('+') || b.contains('_') {
            return None;
        }
        let d: BigInt = b.parse().ok()?;
        if d == BigInt::from(0) {
            return None;
        }
        if d < BigInt::from(0) {
            return None;
        }

        let n = if neg_n { -n } else { n };
        Some(Value::Rational(num_rational::BigRational::new(n, d)))
    } else {
        parse_integer(tok)
    }
}

pub fn eval_line(stack: &mut Vec<Value>, line: &str) -> Result<(), PncError> {
    let tokens: Vec<&str> = line.split_whitespace().collect();
    let mut i = 0;
    while i < tokens.len() {
        let tok = tokens[i];
        if tok == "to" {
            if i + 1 >= tokens.len() {
                return Err(PncError::Parse("missing unit after 'to'".to_string()));
            }
            let target = parse_unit_symbol(tokens[i + 1])
                .ok_or_else(|| PncError::Parse(tokens[i + 1].to_string()))?;
            let top = stack.last().cloned().ok_or(PncError::StackUnderflow)?;
            let converted =
                unit::convert(&top, target).map_err(|_| PncError::InvalidUnitConversion)?;
            stack.pop();
            stack.push(converted);
            i += 2;
            continue;
        }
        if let Some(num) = parse_number(tok) {
            // check for unit-bearing number: number followed by unit symbol
            if i + 1 < tokens.len()
                && let Some(u) = parse_unit_symbol(tokens[i + 1])
            {
                stack.push(Value::Unit {
                    number: Box::new(num),
                    unit: u,
                });
                i += 2;
                continue;
            }
            stack.push(num);
            i += 1;
            continue;
        }
        match tok {
            "+" => ops::add(stack)?,
            "-" => ops::sub(stack)?,
            "*" => ops::mul(stack)?,
            "/" => ops::div(stack)?,
            "neg" => ops::neg(stack)?,
            "abs" => ops::abs(stack)?,
            "mod" => ops::rmod(stack)?,
            "over" => ops::over(stack)?,
            "rot" => ops::rot(stack)?,
            "clear" => ops::clear(stack),
            "to_rational" => ops::to_rational(stack)?,
            "to_int" => ops::to_int(stack)?,
            _ => {
                if tok.chars().all(char::is_alphabetic) {
                    return Err(PncError::UnknownCommand(tok.to_string()));
                }
                return Err(PncError::Parse(tok.to_string()));
            }
        }
        i += 1;
    }
    Ok(())
}

pub fn is_number_token(tok: &str) -> bool {
    parse_number(tok).is_some()
}