jlrs-macros 0.1.1

jlrs-macros contains the custom derives offered by jlrs.
Documentation
use std::ops::RangeInclusive;

use proc_macro::{Delimiter, TokenStream, TokenTree};

const MAJOR_VERSION: usize = 1;
const LTS_MINOR_VERSION: usize = 6;
const NIGHTLY_MINOR_VERSION: usize = 10;

#[cfg(feature = "julia-1-6")]
const SELECTED_MINOR_VERSION: usize = 6;
#[cfg(feature = "julia-1-7")]
const SELECTED_MINOR_VERSION: usize = 7;
#[cfg(feature = "julia-1-8")]
const SELECTED_MINOR_VERSION: usize = 8;
#[cfg(feature = "julia-1-9")]
const SELECTED_MINOR_VERSION: usize = 9;
#[cfg(feature = "julia-1-10")]
const SELECTED_MINOR_VERSION: usize = 10;

pub fn emit_if_compatible(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut tts = attr.into_iter();
    let mut since = None;
    let mut until = None;
    let mut except = None;
    let mut windows_lts = None;

    loop {
        match tts.next() {
            Some(TokenTree::Ident(ident)) => match ident.to_string().as_ref() {
                "since" => {
                    expect_punt_eq(&mut tts);
                    since = Some(unwrap_version(&mut tts));
                    if !expect_comma_or_end(&mut tts) {
                        break;
                    }
                }
                "until" => {
                    expect_punt_eq(&mut tts);
                    until = Some(unwrap_version(&mut tts));
                    if !expect_comma_or_end(&mut tts) {
                        break;
                    }
                }
                "windows_lts" => {
                    expect_punt_eq(&mut tts);
                    windows_lts = Some(unwrap_bool(&mut tts));
                    if !expect_comma_or_end(&mut tts) {
                        break;
                    }
                }
                "except" => {
                    expect_punt_eq(&mut tts);
                    except = Some(unwrap_version_group(&mut tts));
                    if !expect_comma_or_end(&mut tts) {
                        break;
                    }
                }
                ident => panic!("Unexpected identifier {}", ident),
            },
            None => break,
            Some(tt) => panic!("Unexpected tokens {}", tt),
        }
    }

    let since = since.unwrap_or(Version::new(MAJOR_VERSION, LTS_MINOR_VERSION));
    let until = until.unwrap_or(Version::new(MAJOR_VERSION, NIGHTLY_MINOR_VERSION));
    let except = except.unwrap_or_default();

    if should_emit(since, until, windows_lts, &except) {
        item
    } else {
        TokenStream::new()
    }
}

fn expect_punt_eq<T: Iterator<Item = TokenTree>>(iter: &mut T) {
    match iter.next() {
        Some(TokenTree::Punct(punct)) => {
            let punct = punct.as_char();
            if punct != '=' {
                panic!("Expected =, got {}", punct)
            }
        }
        Some(other) => panic!("Expected =, got {}", other),
        None => panic!("Expected =, got nothing"),
    }
}

fn expect_comma_or_end<T: Iterator<Item = TokenTree>>(iter: &mut T) -> bool {
    match iter.next() {
        Some(TokenTree::Punct(punct)) => {
            let punct = punct.as_char();
            if punct != ',' {
                panic!("Expected ,, got {}", punct)
            }
            true
        }
        Some(other) => panic!("Expected =, got {}", other),
        None => false,
    }
}

fn unwrap_bool<T: Iterator<Item = TokenTree>>(iter: &mut T) -> bool {
    match iter.next() {
        Some(TokenTree::Ident(ident)) => {
            let ident = ident.to_string();
            match ident.as_ref() {
                "true" => true,
                "false" => false,
                other => panic!("Expected true or false, got {}", other),
            }
        }
        Some(other) => panic!("Expected true or false, got {}", other),
        None => panic!("Expected true or false, got nothing"),
    }
}

#[derive(PartialOrd, PartialEq)]
struct Version {
    major: usize,
    minor: usize,
}

impl Version {
    const fn new(major: usize, minor: usize) -> Self {
        Version { major, minor }
    }

    fn assert_valid(&self, major: usize, minor: RangeInclusive<usize>) {
        if major != self.major {
            panic!("Unsupported major version");
        }

        if !minor.contains(&self.minor) {
            panic!("Unsupported minor version");
        }
    }
}

fn unwrap_version<T: Iterator<Item = TokenTree>>(iter: &mut T) -> Version {
    match iter.next() {
        Some(TokenTree::Literal(lit)) => {
            let lit = lit.to_string();
            let lit = &lit[1..lit.len() - 1];
            let mut iter = lit.split(".");
            let major = iter
                .next()
                .expect("Expected of the form major.minor")
                .parse::<usize>()
                .expect("Not a number");
            let minor = iter
                .next()
                .expect("Expected of the form major.minor")
                .parse::<usize>()
                .expect("Not a number");
            assert!(iter.next().is_none(), "Expected of the form major.minor");

            let version = Version::new(major, minor);
            version.assert_valid(MAJOR_VERSION, LTS_MINOR_VERSION..=NIGHTLY_MINOR_VERSION);

            version
        }
        Some(other) => panic!(
            "Expected a version in the form major.minor, got {:?}",
            other
        ),
        None => panic!("Expected a version in the form major.minor, got nothing"),
    }
}

fn unwrap_version_group<T: Iterator<Item = TokenTree>>(iter: &mut T) -> Vec<Version> {
    match iter.next() {
        Some(TokenTree::Group(group)) => {
            let mut versions = vec![];
            if group.delimiter() != Delimiter::Bracket {
                panic!("Expected a bracketed group of versions")
            }

            let mut group_tts = group.stream().into_iter();
            loop {
                let version = unwrap_version(&mut group_tts);
                versions.push(version);
                if !expect_comma_or_end(&mut group_tts) {
                    break;
                }
            }

            versions
        }
        Some(other) => panic!("Expected a bracketed group of versions, got {:?}", other),
        None => panic!("Expected a bracketed group of versions, got nothing"),
    }
}

fn selected_version() -> Version {
    Version::new(1, SELECTED_MINOR_VERSION)
}

#[allow(unused_variables)]
fn should_emit(
    since: Version,
    until: Version,
    windows_lts: Option<bool>,
    except: &[Version],
) -> bool {
    let selected = selected_version();
    if since > selected {
        return false;
    }

    if selected > until {
        return false;
    }

    if except.contains(&selected) {
        return false;
    }

    if let Some(windows_lts) = windows_lts {
        #[cfg(any(feature = "windows", target_os = "windows"))]
        if selected.minor == LTS_MINOR_VERSION && !windows_lts {
            return false;
        }

        #[cfg(any(feature = "windows", target_os = "windows"))]
        if selected.minor != LTS_MINOR_VERSION && windows_lts {
            return false;
        }

        #[cfg(not(any(feature = "windows", target_os = "windows")))]
        if windows_lts {
            return false;
        }
    }

    true
}