makers 0.7.0

a POSIX-compatible make implemented in Rust
use eyre::{bail, Result};

use super::token::TokenString;

#[derive(Debug)]
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<Line>),
    /// 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<State>) {
        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 {
        // TODO see if i really need to implement potentially-mixed-quoted args
        return None;
    };
    arg1.trim_end();
    arg2.trim_start();
    Some((arg1, arg2))
}

impl Line {
    pub fn from(
        line: &str,
        expand_macro: impl Fn(&TokenString) -> Result<String>,
    ) -> Result<Option<Self>> {
        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<String>,
    ) -> Result<StateAction> {
        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"),
            },
        })
    }
}