#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Word(String),
Equals,
Value(String),
LeftBracket,
RightBracket,
Comma,
Newline,
Continuation,
}
pub fn tokenize(input: &str) -> Vec<Token> {
let logical_lines = preprocess_lines(input);
let mut tokens = Vec::new();
for line in logical_lines {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.starts_with('~') {
tokens.push(Token::Continuation);
let rest = line.strip_prefix('~').map(str::trim).unwrap_or("");
if !rest.is_empty() {
tokenize_property_list(rest, &mut tokens);
}
} else {
let first_space = line.find(char::is_whitespace).unwrap_or(line.len());
let verb = &line[..first_space];
tokens.push(Token::Word(verb.to_string()));
let rest = line[first_space..].trim();
if !rest.is_empty() {
tokenize_property_list(rest, &mut tokens);
}
}
tokens.push(Token::Newline);
}
tokens
}
fn preprocess_lines(input: &str) -> Vec<String> {
let mut result: Vec<String> = Vec::new();
for raw_line in input.lines() {
let line = strip_comment(raw_line);
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('~') {
result.push(trimmed.to_string());
} else {
result.push(trimmed.to_string());
}
}
result
}
fn strip_comment(line: &str) -> &str {
let mut in_quote: Option<char> = None;
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if let Some(q) = in_quote {
if c == q {
in_quote = None;
}
i += 1;
continue;
}
match c {
'"' | '\'' => {
in_quote = Some(c);
i += 1;
}
'!' => {
return &line[..byte_offset(line, i)];
}
'/' if i + 1 < chars.len() && chars[i + 1] == '/' => {
return &line[..byte_offset(line, i)];
}
_ => {
i += 1;
}
}
}
line
}
fn byte_offset(s: &str, ci: usize) -> usize {
s.char_indices().nth(ci).map(|(b, _)| b).unwrap_or(s.len())
}
fn eval_rpn(s: &str) -> Option<f64> {
let tokens: Vec<&str> = s.split_whitespace().collect();
if tokens.is_empty() {
return None;
}
let has_operator = tokens.iter().any(|t| {
matches!(
t.to_lowercase().as_str(),
"+" | "-" | "*" | "/" | "sqrt" | "sqr" | "inv" | "abs" | "neg"
)
});
if !has_operator {
return None;
}
let mut stack: Vec<f64> = Vec::new();
for tok in &tokens {
if let Ok(num) = tok.parse::<f64>() {
stack.push(num);
} else {
match tok.to_lowercase().as_str() {
"+" => {
let b = stack.pop()?;
let a = stack.pop()?;
stack.push(a + b);
}
"-" => {
let b = stack.pop()?;
let a = stack.pop()?;
stack.push(a - b);
}
"*" => {
let b = stack.pop()?;
let a = stack.pop()?;
stack.push(a * b);
}
"/" => {
let b = stack.pop()?;
let a = stack.pop()?;
if b.abs() < 1e-30 {
return None;
}
stack.push(a / b);
}
"sqrt" => {
let a = stack.pop()?;
stack.push(a.sqrt());
}
"sqr" => {
let a = stack.pop()?;
stack.push(a * a);
}
"inv" => {
let a = stack.pop()?;
if a.abs() < 1e-30 {
return None;
}
stack.push(1.0 / a);
}
"abs" => {
let a = stack.pop()?;
stack.push(a.abs());
}
"neg" => {
let a = stack.pop()?;
stack.push(-a);
}
_ => return None, }
}
}
if stack.len() == 1 {
Some(stack[0])
} else {
None }
}
fn tokenize_property_list(s: &str, tokens: &mut Vec<Token>) {
let mut chars = s.char_indices().peekable();
while let Some(&(start, c)) = chars.peek() {
match c {
' ' | '\t' | ';' => {
chars.next();
}
'=' => {
tokens.push(Token::Equals);
chars.next();
}
'(' => {
chars.next(); let inner_start = chars.peek().map(|&(b, _)| b).unwrap_or(s.len());
let mut inner_end = s.len();
while let Some(&(i, ch)) = chars.peek() {
if ch == ')' {
inner_end = i;
chars.next(); break;
}
chars.next();
}
let inner = &s[inner_start..inner_end];
if let Some(val) = eval_rpn(inner) {
tokens.push(Token::Word(val.to_string()));
} else {
tokens.push(Token::LeftBracket);
tokenize_property_list(inner, tokens);
tokens.push(Token::RightBracket);
}
}
'[' => {
tokens.push(Token::LeftBracket);
chars.next();
}
']' | ')' => {
tokens.push(Token::RightBracket);
chars.next();
}
',' | '|' => {
tokens.push(Token::Comma);
chars.next();
}
'"' | '\'' => {
let quote = c;
chars.next(); let inner_start = chars.peek().map(|&(b, _)| b).unwrap_or(s.len());
let mut inner_end = s.len();
while let Some(&(i, ch)) = chars.peek() {
if ch == quote {
inner_end = i;
chars.next(); break;
}
chars.next();
}
let value = s[inner_start..inner_end].to_string();
tokens.push(Token::Value(value));
}
_ => {
let word_start = start;
let mut word_end = s.len();
chars.next();
while let Some(&(i, ch)) = chars.peek() {
if " \t=[](),|;".contains(ch) {
word_end = i;
break;
}
chars.next();
}
let word = s[word_start..word_end].to_string();
if !word.is_empty() {
tokens.push(Token::Word(word));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_bang_comment() {
let tokens = tokenize("New Line.L1 bus1=A bus2=B ! this is a comment");
assert!(
!tokens
.iter()
.any(|t| matches!(t, Token::Word(w) if w.contains("comment")))
);
}
#[test]
fn strips_slash_comment() {
let tokens = tokenize("New Line.L1 // skip");
assert!(
!tokens
.iter()
.any(|t| matches!(t, Token::Word(w) if w == "skip"))
);
}
#[test]
fn continuation_produces_continuation_token() {
let tokens = tokenize("New Line.L1 bus1=A\n~ bus2=B");
assert!(tokens.contains(&Token::Continuation));
}
#[test]
fn array_brackets_parsed() {
let tokens = tokenize("New Line.L1 rmatrix=[1 2 3]");
assert!(tokens.contains(&Token::LeftBracket));
assert!(tokens.contains(&Token::RightBracket));
}
#[test]
fn rpn_division() {
assert!((eval_rpn("8 1000 /").unwrap() - 0.008).abs() < 1e-12);
}
#[test]
fn rpn_multiplication() {
assert!((eval_rpn("3 4 *").unwrap() - 12.0).abs() < 1e-12);
}
#[test]
fn rpn_sqrt() {
assert!((eval_rpn("16 sqrt").unwrap() - 4.0).abs() < 1e-12);
}
#[test]
fn rpn_complex_expression() {
assert!((eval_rpn("2 3 + 4 *").unwrap() - 20.0).abs() < 1e-12);
}
#[test]
fn rpn_single_number_is_not_rpn() {
assert!(eval_rpn("8").is_none());
}
#[test]
fn rpn_inline_in_tokenizer() {
let tokens = tokenize("New Transformer.Sub XHL=(8 1000 /)");
assert!(
!tokens.contains(&Token::LeftBracket),
"RPN expression should not produce brackets: {:?}",
tokens
);
let has_value = tokens.iter().any(|t| {
if let Token::Word(w) = t {
if let Ok(v) = w.parse::<f64>() {
(v - 0.008).abs() < 1e-10
} else {
false
}
} else {
false
}
});
assert!(
has_value,
"Should contain evaluated RPN value 0.008: {:?}",
tokens
);
}
}