~jojo/microcrisp

c984afed29a3c42095c37e6102340a65f148e152 — JoJo 8 months ago 39ef00c
add match_expr! to match&destructure lisp exprs more conveniently
2 files changed, 159 insertions(+), 21 deletions(-)

M src/lib.rs
A src/matching.rs
M src/lib.rs => src/lib.rs +30 -21
@@ 1,7 1,9 @@
mod matching;

use std::{
    borrow::Cow,
    cmp::{max, min},
    fmt, str,
    str,
};

const SYMBOL_CHARS: &str = "\0abcdefghijklmnopqrstuvxyz+-*?0123456789";


@@ 94,7 96,7 @@ fn parse_(src: &[u8]) -> PResult<Lisp, &'static str> {
        }
        [b, ..] if SYMBOL_CHARS.contains(*b as char) => {
            let s = src[i..].split(|b| !SYMBOL_CHARS.contains(*b as char)).next().unwrap();
            match Symbol::pack(s) {
            match Symbol::pack_slice(s) {
                Some(symbol) => Ok((i + s.len(), Lisp::Symbol(symbol))),
                None => Err((i, "valid symbol")),
            }


@@ 168,23 170,31 @@ fn map_ok<T, U, E>(r: PResult<T, E>, f: impl FnOnce(T) -> U) -> PResult<U, E> {

impl Symbol {
    pub fn pack_str(s: &str) -> Option<Self> {
        Self::pack(s.as_bytes())
        Self::pack(s.bytes())
    }

    pub fn pack_slice(s: &[u8]) -> Option<Self> {
        Self::pack(s.iter().cloned())
    }

    pub fn pack(s: &[u8]) -> Option<Self> {
        if s.is_empty() || s.len() > SYMBOL_MAX_LEN || s[0].is_ascii_digit() || s[0] == 0 {
            None
        } else {
            let mut acc = SYMBOL_CHARS.find(s[0] as char)? as u128;
            let mut multiplier = (SYMBOL_CHARS.len() - 10) as u128;
            for &c in &s[1..] {
                if c == 0 {
                    return None;
    pub fn pack(s: impl IntoIterator<Item = u8>) -> Option<Self> {
        let mut s = s.into_iter();
        match s.next() {
            None | Some(b'0'..=b'9' | 0) => None,
            Some(c0) => {
                let mut i = 1;
                let mut acc = SYMBOL_CHARS.find(c0 as char)? as u128;
                let mut multiplier = (SYMBOL_CHARS.len() - 10) as u128;
                for c in s {
                    if i == SYMBOL_MAX_LEN || c == 0 {
                        return None;
                    }
                    acc += multiplier * SYMBOL_CHARS.find(c as char)? as u128;
                    multiplier *= SYMBOL_CHARS.len() as u128;
                    i += 1;
                }
                acc += multiplier * SYMBOL_CHARS.find(c as char)? as u128;
                multiplier *= SYMBOL_CHARS.len() as u128;
                Some(Symbol(acc))
            }
            Some(Symbol(acc))
        }
    }



@@ 226,12 236,11 @@ impl Lisp {
    }
}

impl fmt::Display for Lisp {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
impl PartialEq<i64> for Lisp {
    fn eq(&self, rhs: &i64) -> bool {
        match self {
            Lisp::Int(x) => x.fmt(f),
            Lisp::Float(x) => x.fmt(f),
            _ => todo!(),
            Lisp::Int(lhs) => lhs == rhs,
            _ => false,
        }
    }
}


@@ 250,7 259,7 @@ mod test {
        assert_eq!(Symbol::pack_str("a"), Some(Symbol(1)));
        assert_eq!(Symbol::pack_str(""), None);
        assert_eq!(Symbol::pack_str("0"), None);
        assert_eq!(Symbol::pack(&[0]), None);
        assert_eq!(Symbol::pack_slice(&[0]), None);
    }

    #[test]

A src/matching.rs => src/matching.rs +129 -0
@@ 0,0 1,129 @@
#[allow(unused_macros)]
macro_rules! match_case {
    // ==== GO! ====
    ($e:expr, @go {_}, $body:expr, $else:expr) => {
        { let _ = $e; $body }
    };
    ($e:expr, @go {$lhs:ident}, $body:expr, $else:expr) => {{
        let $lhs = $e;
        $body
    }};
    ($e:expr, @go {$lhs:literal}, $body:expr, $else:expr) => {
        if $e == &$lhs { $body } else { $else }
    };
    ($e:expr, @go {i($lhs:pat)}, $body:expr, $else:expr) => {
        match $e {
            $crate::Lisp::Int($lhs) => $body,
            _ => $else
        }
    };
    ($e:expr, @go {$symbol:lifetime}, $body:expr, $else:expr) => {
        match $e {
            $crate::Lisp::Symbol(ref s) if s == &$crate::Symbol::pack(stringify!($symbol)[1..].bytes().map(|c| if c == b'_' { b'-' } else { c })).unwrap() =>
                $body,
            _ => $else,
        }
    };
    ($e:expr, @go {sy($lhs:pat)}, $body:expr, $else:expr) => {
        match $e {
            $crate::Lisp::Symbol($lhs) => $body,
            _ => $else
        }
    };
    ($e:expr, @go {($($lhs:tt)*)}, $body:expr, $else:expr) => {{
        let r = match $e {
            $crate::Lisp::List(xs) => match_case!(xs, @munch {} {$($lhs)*}, $body, None),
            _ => None,
        };
        if r.is_some() {
            r
        } else {
            $else
        }
    }};

    // ==== MUNCH! ====
    ($es:expr, @munch {} {}, $body:expr, $else:expr) => {
        if $es.is_empty() { $body } else { $else }
    };
    ($es:expr, @munch {$($lhs:tt)+} {}, $body:expr, $else:expr) => {
        match_case!($es, @munch {$($lhs)+} {,}, $body, $else)
    };
    ($es:expr, @munch {$($lhs:tt)+} {, $($lhss:tt)*}, $body:expr, $else:expr) => {{
        let r = if let Some((first, rest)) = $es.split_first() {
            match_case!(first, @go {$($lhs)+}, match_case!(rest, @munch {} {$($lhss)*}, $body, None), None)
        } else {
            None
        };
        if r.is_some() {
            r
        } else {
            $else
        }
    }};
    ($es:expr, @munch {$($lhs:tt)*} {$t:tt $($tts:tt)*}, $body:expr, $else:expr) => {
        match_case!($es, @munch {$($lhs)* $t} {$($tts)*}, $body, $else)
    };
}

#[macro_export]
macro_rules! match_expr {
    // ==== MUNCH! ====
    ($e:expr, @munch {$($lhs:tt)*} => $body:expr, $($clauses:tt)*) => {
        match_case!($e, @go {$($lhs)*}, Some($body), match_expr!($e, @munch {} $($clauses)*))
    };
    ($e:expr, @munch {$($lhs:tt)*} => $body:expr) => {
        match_expr!($e, @munch {$($lhs)*} => $body ,)
    };
    ($e:expr, @munch {}) => {
        None
    };
    ($e:expr, @munch {$($lhs:tt)*} $t:tt $($tts:tt)*) => {
        match_expr!($e, @munch {$($lhs)* $t} $($tts)*)
    };

    // ==== ENTRY ====
    ($e:expr, { $($clauses:tt)* }) => {{
        let e = &$e;
        match_expr!(e, @munch {} $($clauses)*)
    }};
}

#[cfg(test)]
mod test {
    use crate::*;

    #[test]
    fn match_expr() {
        assert_eq!(match_expr!((), { _ => "hello" }), Some("hello"));

        assert_eq!(match_expr!(Lisp::List(vec![]), { () => "yo", _ => "hello" }), Some("yo"));

        assert_eq!(
            match_expr!(parse(r#" ("hello" "there") "#.as_bytes()).unwrap(), { (_, s) => s.clone() }),
            Some(Lisp::String("there".to_owned()))
        );

        assert_eq!(match_expr!(Lisp::Int(0),   { 0 => 666, 123 => 456 }), Some(666));
        assert_eq!(match_expr!(Lisp::Int(123), { 0 => 666, 123 => 456 }), Some(456));
        assert_eq!(match_expr!(Lisp::Int(123), { i(x) => *x }), Some(123));

        assert_eq!(match_expr!(parse(b"foo").unwrap(), { sy(s) => *s }), Symbol::pack_str("foo"));
        assert_eq!(match_expr!(parse(b"foo").unwrap(), { sy(_) => () }), Some(()));
        assert_eq!(match_expr!(parse(b"foo-bar").unwrap(), { 'foo_bar => () }), Some(()));

        assert_eq!(match_expr!(parse(b"(foo 123)").unwrap(), { ('foo, i(x)) => *x }), Some(123));

        #[derive(Debug, PartialEq)]
        enum Update {
            Version(i64, i64, i64),
            Force,
        }
        use Update::*;
        let v = match_expr!(parse(b"(version 1 2 3)").unwrap(), {
            ('version, i(major), i(minor), i(patch)) => Version(*major, *minor, *patch),
            'force => Force
        });
        assert_eq!(v, Some(Version(1, 2, 3)));
    }
}