use std::collections::HashMap;
const MAX_EXPR_LEN: usize = 512;
#[derive(Debug, Default)]
pub struct ParamEnv {
map: HashMap<String, f64>,
}
impl ParamEnv {
pub fn define(&mut self, name: &str, value: f64) {
self.map.insert(name.to_ascii_lowercase(), value);
}
pub fn get(&self, name: &str) -> Option<f64> {
self.map.get(name).copied()
}
pub fn eval_brace(&self, token: &str) -> Result<f64, String> {
let inner = token
.strip_prefix('{')
.and_then(|t| t.strip_suffix('}'))
.ok_or_else(|| {
format!(
"malformed parameter expression `{token}` (note: basic .param \
expressions cannot contain spaces, commas or parentheses)"
)
})?;
self.eval_expr(inner)
}
pub fn eval_expr(&self, expr: &str) -> Result<f64, String> {
if expr.len() > MAX_EXPR_LEN {
return Err("parameter expression too long".into());
}
let mut lx = Lexer {
bytes: expr.as_bytes(),
pos: 0,
};
let v = self.sum(&mut lx)?;
match lx.peek() {
None => Ok(v),
Some(_) => Err(format!("trailing garbage in expression `{expr}`")),
}
}
fn sum(&self, lx: &mut Lexer) -> Result<f64, String> {
let mut acc = self.product(lx)?;
while let Some(op @ (b'+' | b'-')) = lx.peek() {
lx.bump();
let rhs = self.product(lx)?;
acc = if op == b'+' { acc + rhs } else { acc - rhs };
}
Ok(acc)
}
fn product(&self, lx: &mut Lexer) -> Result<f64, String> {
let mut acc = self.power(lx)?;
while let Some(op @ (b'*' | b'/')) = lx.peek() {
lx.bump();
let rhs = self.power(lx)?;
acc = if op == b'*' { acc * rhs } else { acc / rhs };
}
Ok(acc)
}
fn power(&self, lx: &mut Lexer) -> Result<f64, String> {
match lx.peek() {
Some(b'-') => {
lx.bump();
return Ok(-self.power(lx)?);
}
Some(b'+') => {
lx.bump();
return self.power(lx);
}
_ => {}
}
let base = self.atom(lx)?;
if lx.peek() == Some(b'^') {
lx.bump();
let exp = self.power(lx)?;
return Ok(base.powf(exp));
}
Ok(base)
}
fn atom(&self, lx: &mut Lexer) -> Result<f64, String> {
match lx.peek() {
Some(b) if b.is_ascii_digit() || b == b'.' => {
let text = lx.take_number();
crate::scan::evaluate(text).ok_or_else(|| format!("bad number `{text}`"))
}
Some(b) if b.is_ascii_alphabetic() || b == b'_' => {
let name = lx.take_ident().to_ascii_lowercase();
self.get(&name)
.ok_or_else(|| format!("unknown parameter `{name}`"))
}
Some(b) => Err(format!("unexpected `{}` in expression", b as char)),
None => Err("expression ends where a value was expected".into()),
}
}
}
struct Lexer<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Lexer<'a> {
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn bump(&mut self) {
self.pos += 1;
}
fn take_number(&mut self) -> &'a str {
let start = self.pos;
while let Some(b) = self.peek() {
if b.is_ascii_digit() || b == b'.' {
self.bump();
} else {
break;
}
}
if let Some(b'e' | b'E' | b'd' | b'D') = self.peek() {
let mark = self.pos;
self.bump();
if let Some(b'+' | b'-') = self.peek() {
self.bump();
}
if self.peek().is_some_and(|b| b.is_ascii_digit()) {
while self.peek().is_some_and(|b| b.is_ascii_digit()) {
self.bump();
}
} else {
self.pos = mark;
}
}
while self.peek().is_some_and(|b| b.is_ascii_alphabetic()) {
self.bump();
}
std::str::from_utf8(&self.bytes[start..self.pos]).unwrap_or("")
}
fn take_ident(&mut self) -> &'a str {
let start = self.pos;
while self
.peek()
.is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_')
{
self.bump();
}
std::str::from_utf8(&self.bytes[start..self.pos]).unwrap_or("")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env() -> ParamEnv {
let mut e = ParamEnv::default();
e.define("rload", 10e3);
e.define("two", 2.0);
e
}
#[test]
fn numbers_and_names() {
let e = env();
assert_eq!(e.eval_expr("3"), Ok(3.0));
assert_eq!(e.eval_expr("rload"), Ok(1e4));
assert_eq!(e.eval_expr("2k"), Ok(2000.0));
assert!((e.eval_expr("1.5e-3").unwrap() - 1.5e-3).abs() < 1e-18);
}
#[test]
fn arithmetic_and_precedence() {
let e = env();
assert_eq!(e.eval_expr("1+two*3"), Ok(7.0));
assert_eq!(e.eval_expr("rload/two"), Ok(5e3));
assert_eq!(e.eval_expr("-two^two"), Ok(-4.0)); assert_eq!(e.eval_expr("two^two^two"), Ok(16.0)); assert_eq!(e.eval_expr("1-two-two"), Ok(-3.0)); }
#[test]
fn braces_and_errors() {
let e = env();
assert_eq!(e.eval_brace("{rload*two}"), Ok(2e4));
assert!(
e.eval_brace("{oops}")
.unwrap_err()
.contains("unknown parameter")
);
assert!(e.eval_brace("no-braces").is_err());
assert!(e.eval_expr("1+)").is_err());
assert!(e.eval_expr("").is_err());
let long = "1+".repeat(600) + "1";
assert!(e.eval_expr(&long).is_err());
}
#[test]
fn exponent_vs_unit_suffix() {
let e = env();
assert_eq!(e.eval_expr("2e3"), Ok(2000.0));
assert_eq!(e.eval_expr("2e"), Ok(2.0));
assert_eq!(e.eval_expr("10kohm+1"), Ok(10001.0));
}
}