chronlang-parser 0.1.7

A parser for Chronlang, a computer language for crafting constructed languages.
Documentation
use chumsky::{prelude::*, text::{ident, whitespace}};
use crate::parser::common::*;
use crate::ast::{
  Stmt,
  TraitMember,
};

pub fn parser() -> impl Parser<char, Stmt, Error = Simple<char>> {
  let start = just("trait")
    .padded();

  let sequence = || word_chars()
    .or_not()
    .map(|opt| match opt {
      Some(ch) => ch,
      None => "".to_string(),
    });

  let notation = just("=")
    .padded()
    .ignore_then(
      sequence()
        .then_ignore(just("_"))
        .then(sequence())
        .map_with_span(|(before, after), span| (span, before + "_" + &after))
    )
    .or_not();

  let member = just("default")
    .padded()
    .or_not()
    .map(|d| d.is_some())
    .then(
      ident()
        .map_with_span(|id, span| (span, id))
        .separated_by(just("|").padded())
        .at_least(1)
    )
    .then(notation)
    .map(|((default, labels), notation)| TraitMember { default, labels, notation });

  let body = member
    .map_with_span(|m, span| (span, m))
    .separated_by(just(",").padded())
    .allow_trailing()
    .at_least(1)
    .then_ignore(whitespace())
    .delimited_by(just("{").padded(), just("}"));

  start
    .ignore_then(ident().map_with_span(|id, span| (span, id)))
    .then(body)
    .map(|(label, members)| Stmt::Trait { label, members })
}

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

  #[test]
  fn it_parses_a_trait() {
    let src = "
      trait Place {
        labial,
        alveolar,
        velar,
      }
    ";
    assert_eq!(
      parser().parse(src.to_string()),
      Ok(
        Stmt::Trait {
          label: (13..18, "Place".into()),
          members: vec![
            (29..35, TraitMember { labels: vec![(29..35, "labial".into())], notation: None, default: false }),
            (45..53, TraitMember { labels: vec![(45..53, "alveolar".into())], notation: None, default: false }),
            (63..68, TraitMember { labels: vec![(63..68, "velar".into())], notation: None, default: false }),
          ],
        },
      )
    )
  }

  #[test]
  fn it_parses_a_trait_with_annotations() {
    let src = "
      trait Stress {
        primary = ˈ_,
        secondary = ˌ_,
      }
    ";
    assert_eq!(
      parser().parse(src.to_string()),
      Ok(
        Stmt::Trait {
          label: (13..19, "Stress".into()),
          members: vec![
            (30..42, TraitMember { labels: vec![(30..37, "primary".into())], notation: Some((40..42, "ˈ_".into())), default: false }),
            (52..66, TraitMember { labels: vec![(52..61, "secondary".into())], notation: Some((64..66, "ˌ_".into())), default: false }),
          ],
        },
      )
    )
  }

  #[test]
  fn it_parses_a_trait_with_a_default() {
    let src = "
      trait Length {
        default short,
        long = _:,
        overlong = _::,
      }
    ";
    assert_eq!(
      parser().parse(src.to_string()),
      Ok(
        Stmt::Trait {
          label: (13..19, "Length".into()),
          members: vec![
            (30..43, TraitMember { labels: vec![(38..43, "short".into())], notation: None, default: true }),
            (53..62, TraitMember { labels: vec![(53..57, "long".into())], notation: Some((60..62, "_:".into())), default: false }),
            (72..86, TraitMember { labels: vec![(72..80, "overlong".into())], notation: Some((83..86, "_::".into())), default: false }),
          ],
        },
      )
    )
  }
}