use eyre::{bail, Result}; use super::token::TokenString; #[derive(Debug, Eq, PartialEq)] pub enum Line { /// spelled "ifeq" IfEqual(TokenString, TokenString), /// spelled "ifneq" IfNotEqual(TokenString, TokenString), /// spelled "ifdef" IfDefined(String), /// spelled "ifndef" IfNotDefined(String), /// spelled "else" Else, /// spelled "else condition" ElseIf(Box), /// spelled "endif" EndIf, } #[derive(Debug)] pub enum State { /// we saw a conditional, the condition was true, we're executing now /// and if we hit an else we will start SkippingUntilEndIf Executing, /// we saw a conditional, the condition was false, we're ignoring now /// and if we hit an else we'll start executing /// (or if it's an else if we'll check the condition) SkippingUntilElseOrEndIf, /// we saw a conditional, the condition was true, we executed, and now we hit an else /// so we don't need to stop and evaluate new conditions, because we straight up do /// not care SkippingUntilEndIf, } impl State { pub const fn skipping(&self) -> bool { match self { Self::Executing => false, Self::SkippingUntilElseOrEndIf | Self::SkippingUntilEndIf => true, } } } #[derive(Debug)] pub enum StateAction { Push(State), Replace(State), Pop, } impl StateAction { #[allow(clippy::panic)] pub fn apply_to(self, stack: &mut Vec) { match self { Self::Push(state) => stack.push(state), Self::Replace(state) => match stack.last_mut() { Some(x) => *x = state, None => panic!("internal error: applying Replace on an empty condition stack"), }, Self::Pop => { stack.pop(); } } } } fn decode_condition_args(line_body: &str) -> Option<(TokenString, TokenString)> { let tokens: TokenString = line_body.parse().ok()?; let (mut arg1, mut arg2) = if tokens.starts_with("(") && tokens.ends_with(")") { let mut tokens = tokens; tokens.strip_prefix("("); tokens.strip_suffix(")"); tokens.split_once(",")? } else { let quotes = &["\"", "'"]; let mut split_pair = None; for (left_quote, right_quote) in quotes .iter() .flat_map(|left| quotes.iter().map(move |right| (left, right))) { if tokens.starts_with(left_quote) && tokens.ends_with(right_quote) { let mut tokens = tokens; tokens.strip_prefix(left_quote); tokens.strip_suffix(right_quote); split_pair = Some(tokens.split_once(&format!("{} {}", left_quote, right_quote))?); break; } } split_pair? }; arg1.trim_end(); arg2.trim_start(); Some((arg1, arg2)) } impl Line { pub fn from( line: &str, expand_macro: impl Fn(&TokenString) -> Result, ) -> Result> { let line = line.trim_start(); Ok(Some(if let Some(line) = line.strip_prefix("ifeq ") { match decode_condition_args(line) { Some((arg1, arg2)) => Self::IfEqual(arg1, arg2), None => return Ok(None), } } else if let Some(line) = line.strip_prefix("ifneq ") { match decode_condition_args(line) { Some((arg1, arg2)) => Self::IfNotEqual(arg1, arg2), None => return Ok(None), } } else if let Some(line) = line.strip_prefix("ifdef ") { Self::IfDefined(expand_macro(&line.parse()?)?) } else if let Some(line) = line.strip_prefix("ifndef ") { Self::IfNotDefined(expand_macro(&line.parse()?)?) } else if line == "else" { Self::Else } else if let Some(line) = line.strip_prefix("else ") { match Self::from(line, expand_macro)? { Some(sub_condition) => Self::ElseIf(Box::new(sub_condition)), None => return Ok(None), } } else if line == "endif" { Self::EndIf } else { return Ok(None); })) } pub fn action( &self, current_state: Option<&State>, is_macro_defined: impl Fn(&str) -> bool, expand_macro: impl Fn(&TokenString) -> Result, ) -> Result { Ok(match self { Self::IfEqual(arg1, arg2) => { let arg1 = expand_macro(arg1)?; let arg2 = expand_macro(arg2)?; if arg1 == arg2 { StateAction::Push(State::Executing) } else { StateAction::Push(State::SkippingUntilElseOrEndIf) } } Self::IfNotEqual(arg1, arg2) => { let arg1 = expand_macro(arg1)?; let arg2 = expand_macro(arg2)?; if arg1 == arg2 { StateAction::Push(State::SkippingUntilElseOrEndIf) } else { StateAction::Push(State::Executing) } } Self::IfDefined(name) => { if is_macro_defined(name) { StateAction::Push(State::Executing) } else { StateAction::Push(State::SkippingUntilElseOrEndIf) } } Self::IfNotDefined(name) => { if is_macro_defined(name) { StateAction::Push(State::SkippingUntilElseOrEndIf) } else { StateAction::Push(State::Executing) } } Self::Else => StateAction::Replace(match current_state { Some(State::Executing) | Some(State::SkippingUntilEndIf) => { State::SkippingUntilEndIf } Some(State::SkippingUntilElseOrEndIf) => State::Executing, None => bail!("got an Else but not in a conditional"), }), Self::ElseIf(inner_condition) => match current_state { Some(State::Executing) | Some(State::SkippingUntilEndIf) => { StateAction::Replace(State::SkippingUntilEndIf) } Some(State::SkippingUntilElseOrEndIf) => { match inner_condition.action(current_state, is_macro_defined, expand_macro)? { StateAction::Push(x) => StateAction::Replace(x), x => x, } } None => bail!("got an ElseIf but not in a conditional"), }, Self::EndIf => match current_state { Some(_) => StateAction::Pop, None => bail!("got an EndIf but not in a conditional"), }, }) } } #[cfg(test)] mod test { use super::*; type R = Result<()>; #[test] fn quotes() -> R { let line = r#"ifeq 'x' "3""#; assert_eq!( Line::from(line, |_| panic!())?, Some(Line::IfEqual( TokenString::text("x"), TokenString::text("3") )) ); Ok(()) } }