use std::collections::HashMap;
use std::io::{self, BufRead};
use mathlex_eval::{EvalInput, NumericResult, compile, eval};
fn main() {
let stdin = io::stdin();
let mut block = Block::default();
for (line_num, line) in stdin.lock().lines().enumerate() {
let line = match line {
Ok(l) => l,
Err(e) => {
eprintln!("error: reading stdin line {}: {}", line_num + 1, e);
std::process::exit(1);
}
};
let trimmed = line.trim();
if trimmed.starts_with('#') {
continue;
}
if trimmed.is_empty() {
if block.has_expr() {
execute_block(&block);
block = Block::default();
}
continue;
}
if let Some(rest) = strip_prefix(trimmed, "expr:") {
block.expr = Some(rest.to_string());
} else if let Some(rest) = strip_prefix(trimmed, "latex:") {
block.latex = Some(rest.to_string());
} else if let Some(rest) = strip_prefix(trimmed, "subst:") {
block.subst = Some(rest.to_string());
} else if let Some(rest) = strip_prefix(trimmed, "var:") {
block.var = Some(rest.to_string());
} else {
eprintln!("error: line {}: unknown prefix: {}", line_num + 1, trimmed);
}
}
if block.has_expr() {
execute_block(&block);
}
}
#[derive(Default)]
struct Block {
expr: Option<String>,
latex: Option<String>,
subst: Option<String>,
var: Option<String>,
}
impl Block {
fn has_expr(&self) -> bool {
self.expr.is_some() || self.latex.is_some()
}
}
fn execute_block(block: &Block) {
let ast = if let Some(ref expr_str) = block.expr {
match mathlex::parse(expr_str) {
Ok(ast) => ast,
Err(e) => {
eprintln!("error: parse failed: {}", e);
return;
}
}
} else if let Some(ref latex_str) = block.latex {
match mathlex::parse_latex(latex_str) {
Ok(ast) => ast,
Err(e) => {
eprintln!("error: latex parse failed: {}", e);
return;
}
}
} else {
return;
};
let constants = match &block.subst {
Some(s) => match parse_dict(s) {
Ok(dict) => dict
.into_iter()
.map(|(k, v)| (k, scalar_to_numeric(&v)))
.collect::<Vec<_>>(),
Err(e) => {
eprintln!("error: subst parse failed: {}", e);
return;
}
},
None => Vec::new(),
};
let constants_map: HashMap<&str, NumericResult> =
constants.iter().map(|(k, v)| (k.as_str(), *v)).collect();
let compiled = match compile(&ast, &constants_map) {
Ok(c) => c,
Err(e) => {
eprintln!("error: compile failed: {}", e);
return;
}
};
let arg_names = compiled.argument_names();
let args: HashMap<&str, EvalInput> = match &block.var {
Some(var_str) => match parse_var(var_str.trim(), arg_names) {
Ok(a) => a,
Err(e) => {
eprintln!("error: var parse failed: {}", e);
return;
}
},
None => HashMap::new(),
};
let handle = match eval(&compiled, args) {
Ok(h) => h,
Err(e) => {
eprintln!("error: eval failed: {}", e);
return;
}
};
let expr_label = block
.expr
.as_deref()
.or(block.latex.as_deref())
.unwrap_or("?");
if handle.shape().is_empty() {
match handle.scalar() {
Ok(v) => println!("{} = {}", expr_label, fmt_result(&v)),
Err(e) => eprintln!("error: {}", e),
}
} else {
println!("{} [shape: {:?}]", expr_label, handle.shape());
for result in handle.iter() {
match result {
Ok(v) => println!(" {}", fmt_result(&v)),
Err(e) => println!(" ERROR: {}", e),
}
}
}
}
fn fmt_result(r: &NumericResult) -> String {
match r {
NumericResult::Real(v) => format!("{v}"),
NumericResult::Complex(c) => {
if c.im >= 0.0 {
format!("{} + {}i", c.re, c.im)
} else {
format!("{} - {}i", c.re, -c.im)
}
}
}
}
fn strip_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
s.strip_prefix(prefix).map(|r| r.trim())
}
fn parse_dict(s: &str) -> Result<Vec<(String, Value)>, String> {
let s = s.trim();
let s = s
.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
.ok_or("expected {key: value, ...}")?
.trim();
if s.is_empty() {
return Ok(Vec::new());
}
let mut result = Vec::new();
let mut rest = s;
while !rest.is_empty() {
let colon_pos = rest
.find(':')
.ok_or_else(|| format!("expected ':' after key in '{rest}'"))?;
let key = rest[..colon_pos].trim().to_string();
rest = rest[colon_pos + 1..].trim();
let (value, consumed) = parse_value(rest)?;
result.push((key, value));
rest = rest[consumed..].trim();
if let Some(r) = rest.strip_prefix(',') {
rest = r.trim();
}
}
Ok(result)
}
#[derive(Debug)]
enum Value {
Scalar(f64),
Array(Vec<f64>),
}
fn scalar_to_numeric(v: &Value) -> NumericResult {
match v {
Value::Scalar(f) => NumericResult::Real(*f),
Value::Array(a) => NumericResult::Real(a[0]), }
}
fn parse_value(s: &str) -> Result<(Value, usize), String> {
let s = s.trim_start();
if s.starts_with('[') {
let bracket_end = s.find(']').ok_or("unclosed '[' in array value")?;
let inner = s[1..bracket_end].trim();
let values: Result<Vec<f64>, String> = inner
.split(',')
.filter(|p| !p.trim().is_empty())
.map(|p| {
p.trim()
.parse::<f64>()
.map_err(|e| format!("invalid number '{}': {}", p.trim(), e))
})
.collect();
Ok((Value::Array(values?), bracket_end + 1))
} else {
let end = s.find([',', '}']).unwrap_or(s.len());
let num_str = s[..end].trim();
let val = num_str
.parse::<f64>()
.map_err(|e| format!("invalid number '{}': {}", num_str, e))?;
Ok((Value::Scalar(val), end))
}
}
fn parse_var<'a>(s: &str, arg_names: &'a [String]) -> Result<HashMap<&'a str, EvalInput>, String> {
let s = s.trim();
let mut args = HashMap::new();
if s.starts_with('{') {
let dict = parse_dict(s)?;
for (key, value) in dict {
let name = arg_names
.iter()
.find(|n| **n == key)
.ok_or_else(|| format!("unknown variable '{key}'"))?;
args.insert(name.as_str(), value_to_input(value));
}
} else if arg_names.len() == 1 {
let (value, _) = parse_value(s)?;
args.insert(arg_names[0].as_str(), value_to_input(value));
} else if arg_names.is_empty() {
if !s.is_empty() {
eprintln!("warning: var provided but expression has no free variables");
}
} else {
return Err(format!(
"multiple variables ({}) require dict syntax: {{x: ..., y: ...}}",
arg_names.join(", ")
));
}
Ok(args)
}
fn value_to_input(v: Value) -> EvalInput {
match v {
Value::Scalar(f) => EvalInput::Scalar(f),
Value::Array(a) => EvalInput::from(a),
}
}