Skip to main content

pnc_engine/
parse.rs

1use crate::error::PncError;
2use crate::ops;
3use crate::unit::{self, UnitId};
4use crate::value::Value;
5use num_bigint::BigInt;
6use num_traits::Num;
7
8fn parse_unit_symbol(s: &str) -> Option<UnitId> {
9    UnitId::from_symbol(s)
10}
11
12fn has_prefix(s: &str) -> bool {
13    let body = s.strip_prefix('-').unwrap_or(s);
14    body.starts_with("0b")
15        || body.starts_with("0B")
16        || body.starts_with("0o")
17        || body.starts_with("0O")
18        || body.starts_with("0x")
19        || body.starts_with("0X")
20}
21
22fn parse_integer(tok: &str) -> Option<Value> {
23    let (negative, body) = if let Some(rest) = tok.strip_prefix('-') {
24        (true, rest)
25    } else {
26        (false, tok)
27    };
28
29    // Reject leading +.
30    if body.starts_with('+') {
31        return None;
32    }
33
34    // Detect optional base prefix.
35    let (base, digits) =
36        if let Some(rest) = body.strip_prefix("0b").or_else(|| body.strip_prefix("0B")) {
37            (2u32, rest)
38        } else if let Some(rest) = body.strip_prefix("0o").or_else(|| body.strip_prefix("0O")) {
39            (8u32, rest)
40        } else if let Some(rest) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
41            (16u32, rest)
42        } else {
43            (10u32, body)
44        };
45
46    // Reject empty body.
47    if digits.is_empty() {
48        return None;
49    }
50
51    // Reject underscores and leading + in the digit body.
52    if digits.contains('_') || digits.starts_with('+') {
53        return None;
54    }
55
56    let n = if base == 10 {
57        digits.parse::<BigInt>().ok()?
58    } else {
59        // Lowercase hex digits for consistent parsing.
60        if base == 16 {
61            BigInt::from_str_radix(&digits.to_lowercase(), base).ok()?
62        } else {
63            BigInt::from_str_radix(digits, base).ok()?
64        }
65    };
66
67    if negative {
68        Some(Value::Int(-n))
69    } else {
70        Some(Value::Int(n))
71    }
72}
73
74fn parse_number(tok: &str) -> Option<Value> {
75    // Rational branch: split on '/'.
76    if let Some((a, b)) = tok.split_once('/') {
77        let a = a.trim();
78        let b = b.trim();
79
80        // Reject prefixed components.
81        if has_prefix(a) || has_prefix(b) {
82            return None;
83        }
84
85        // Numerator: optional leading -.
86        let (neg_n, n_body) = if let Some(rest) = a.strip_prefix('-') {
87            (true, rest)
88        } else {
89            (false, a)
90        };
91
92        // Reject +, underscores in numerator.
93        if n_body.starts_with('+') || n_body.contains('_') {
94            return None;
95        }
96
97        let n: BigInt = n_body.parse().ok()?;
98
99        // Denominator: must be positive, no sign, no prefix.
100        if b.starts_with('-') || b.starts_with('+') || b.contains('_') {
101            return None;
102        }
103        let d: BigInt = b.parse().ok()?;
104        if d == BigInt::from(0) {
105            return None;
106        }
107        if d < BigInt::from(0) {
108            return None;
109        }
110
111        let n = if neg_n { -n } else { n };
112        Some(Value::Rational(num_rational::BigRational::new(n, d)))
113    } else {
114        parse_integer(tok)
115    }
116}
117
118/// Evaluate a line of RPN tokens against a stack.
119///
120/// Mutates the stack in place. Returns `Err` on parse, arithmetic, or
121/// conversion failures; the stack reflects partial state on error.
122pub fn eval_line(stack: &mut Vec<Value>, line: &str) -> Result<(), PncError> {
123    let tokens: Vec<&str> = line.split_whitespace().collect();
124    let mut i = 0;
125    while i < tokens.len() {
126        let tok = tokens[i];
127        if tok == "to" {
128            if i + 1 >= tokens.len() {
129                return Err(PncError::Parse("missing unit after 'to'".to_string()));
130            }
131            let target = parse_unit_symbol(tokens[i + 1])
132                .ok_or_else(|| PncError::Parse(tokens[i + 1].to_string()))?;
133            let top = stack.last().cloned().ok_or(PncError::StackUnderflow)?;
134            let converted =
135                unit::convert(&top, target).map_err(|_| PncError::InvalidUnitConversion)?;
136            stack.pop();
137            stack.push(converted);
138            i += 2;
139            continue;
140        }
141        if let Some(num) = parse_number(tok) {
142            // check for unit-bearing number: number followed by unit symbol
143            if i + 1 < tokens.len()
144                && let Some(u) = parse_unit_symbol(tokens[i + 1])
145            {
146                stack.push(Value::Unit {
147                    number: Box::new(num),
148                    unit: u,
149                });
150                i += 2;
151                continue;
152            }
153            stack.push(num);
154            i += 1;
155            continue;
156        }
157        match tok {
158            "+" => ops::add(stack)?,
159            "-" => ops::sub(stack)?,
160            "*" => ops::mul(stack)?,
161            "/" => ops::div(stack)?,
162            "neg" => ops::neg(stack)?,
163            "abs" => ops::abs(stack)?,
164            "mod" => ops::rmod(stack)?,
165            "over" => ops::over(stack)?,
166            "rot" => ops::rot(stack)?,
167            "clear" => ops::clear(stack),
168            "to_rational" => ops::to_rational(stack)?,
169            "to_int" => ops::to_int(stack)?,
170            _ => {
171                if tok.chars().all(char::is_alphabetic) {
172                    return Err(PncError::UnknownCommand(tok.to_string()));
173                }
174                return Err(PncError::Parse(tok.to_string()));
175            }
176        }
177        i += 1;
178    }
179    Ok(())
180}
181
182/// Returns `true` if `tok` is a valid numeric literal (integer or rational).
183pub fn is_number_token(tok: &str) -> bool {
184    parse_number(tok).is_some()
185}