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
118pub fn eval_line(stack: &mut Vec<Value>, line: &str) -> Result<(), PncError> {
119    let tokens: Vec<&str> = line.split_whitespace().collect();
120    let mut i = 0;
121    while i < tokens.len() {
122        let tok = tokens[i];
123        if tok == "to" {
124            if i + 1 >= tokens.len() {
125                return Err(PncError::Parse("missing unit after 'to'".to_string()));
126            }
127            let target = parse_unit_symbol(tokens[i + 1])
128                .ok_or_else(|| PncError::Parse(tokens[i + 1].to_string()))?;
129            let top = stack.last().cloned().ok_or(PncError::StackUnderflow)?;
130            let converted =
131                unit::convert(&top, target).map_err(|_| PncError::InvalidUnitConversion)?;
132            stack.pop();
133            stack.push(converted);
134            i += 2;
135            continue;
136        }
137        if let Some(num) = parse_number(tok) {
138            // check for unit-bearing number: number followed by unit symbol
139            if i + 1 < tokens.len()
140                && let Some(u) = parse_unit_symbol(tokens[i + 1])
141            {
142                stack.push(Value::Unit {
143                    number: Box::new(num),
144                    unit: u,
145                });
146                i += 2;
147                continue;
148            }
149            stack.push(num);
150            i += 1;
151            continue;
152        }
153        match tok {
154            "+" => ops::add(stack)?,
155            "-" => ops::sub(stack)?,
156            "*" => ops::mul(stack)?,
157            "/" => ops::div(stack)?,
158            "neg" => ops::neg(stack)?,
159            "abs" => ops::abs(stack)?,
160            "mod" => ops::rmod(stack)?,
161            "over" => ops::over(stack)?,
162            "rot" => ops::rot(stack)?,
163            "clear" => ops::clear(stack),
164            "to_rational" => ops::to_rational(stack)?,
165            "to_int" => ops::to_int(stack)?,
166            _ => {
167                if tok.chars().all(char::is_alphabetic) {
168                    return Err(PncError::UnknownCommand(tok.to_string()));
169                }
170                return Err(PncError::Parse(tok.to_string()));
171            }
172        }
173        i += 1;
174    }
175    Ok(())
176}
177
178pub fn is_number_token(tok: &str) -> bool {
179    parse_number(tok).is_some()
180}