use chumsky::prelude::*;
use crate::Span;
use crate::Spanned;
use crate::dsl::lexer::Token;
use super::AstItem;
use super::AstModule;
use super::ParserInput;
use super::annotation::comment;
use super::effect::def_effect;
use super::fn_def::def_fn;
use super::fn_def::def_pure_fn;
use super::import::import;
use super::test_def::def_test;
use super::ws::leading_ws;
use super::ws::newline;
const SENTINEL: Span = Span::new(0, 0);
fn module_item<'a>()
-> impl Parser<'a, ParserInput<'a>, Spanned<AstItem>, extra::Err<Rich<'a, Token<'a>>>> + Clone {
leading_ws().ignore_then(
choice((
import().map(|i| {
let span = i.span;
Spanned::new(
AstItem::Import {
import: i.node,
span,
},
span,
)
}),
def_pure_fn().map(|f| {
let span = f.span;
Spanned::new(AstItem::PureFn { def: f.node, span }, span)
}),
def_fn().map(|f| {
let span = f.span;
Spanned::new(AstItem::Fn { def: f.node, span }, span)
}),
def_effect().map(|e| {
let span = e.span;
Spanned::new(AstItem::Effect { def: e.node, span }, span)
}),
def_test().map(|t| {
let span = t.span;
Spanned::new(AstItem::Test { def: t.node, span }, span)
}),
comment().map_with(|c, e| {
let span = Span::from(e.span());
Spanned::new(AstItem::Comment { text: c, span }, span)
}),
))
.labelled("top-level item (import, fn, effect, test, or comment)"),
)
}
pub fn module<'a>()
-> impl Parser<'a, ParserInput<'a>, AstModule, extra::Err<Rich<'a, Token<'a>>>> + Clone {
module_item()
.or(newline().map_with(|_, _| {
Spanned::new(
AstItem::Comment { text: String::new(), span: SENTINEL },
SENTINEL,
)
}))
.repeated()
.collect::<Vec<_>>()
.map_with(|items, e| {
let items = items
.into_iter()
.filter(
|i| !matches!(&i.node, AstItem::Comment { text, .. } if text.is_empty() && i.span == SENTINEL),
)
.collect();
AstModule { items, span: Span::from(e.span()) }
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dsl::parser::lex_to_pairs;
use crate::dsl::parser::make_input;
fn parse_module(source: &str) -> AstModule {
let pairs = lex_to_pairs(source);
let input = make_input(&pairs, source.len());
module().parse(input).into_result().unwrap()
}
#[test]
fn empty_module() {
let m = parse_module("");
assert!(m.items.is_empty());
}
#[test]
fn module_with_import() {
let m = parse_module("import lib/greeter\n");
assert_eq!(m.items.len(), 1);
assert!(matches!(&m.items[0].node, AstItem::Import { .. }));
}
#[test]
fn module_with_fn() {
let m = parse_module(
r#"fn greet() {
> echo hello
}
"#,
);
assert_eq!(m.items.len(), 1);
assert!(matches!(&m.items[0].node, AstItem::Fn { .. }));
}
#[test]
fn module_with_comment() {
let m = parse_module("// this is a comment\n");
assert_eq!(m.items.len(), 1);
assert!(
matches!(&m.items[0].node, AstItem::Comment { text, .. } if text == "this is a comment")
);
}
#[test]
fn module_with_blank_lines() {
let m = parse_module(
r#"// comment
// another
"#,
);
assert_eq!(m.items.len(), 2);
}
#[test]
fn multi_item_module() {
let source = r#"import lib/greeter
fn greet() {
> echo hello
}
test "basic" {
shell main {
> echo hi
}
}
"#;
let m = parse_module(source);
assert_eq!(m.items.len(), 3);
assert!(matches!(&m.items[0].node, AstItem::Import { .. }));
assert!(matches!(&m.items[1].node, AstItem::Fn { .. }));
assert!(matches!(&m.items[2].node, AstItem::Test { .. }));
}
#[test]
fn module_with_effect() {
let source = r#"effect Db {
shell db {
> echo start
}
}
"#;
let m = parse_module(source);
assert_eq!(m.items.len(), 1);
assert!(matches!(&m.items[0].node, AstItem::Effect { .. }));
}
#[test]
fn module_with_pure_fn() {
let source = r#"pure fn concat(a, b) {
> echo hello
}
"#;
let m = parse_module(source);
assert_eq!(m.items.len(), 1);
assert!(matches!(&m.items[0].node, AstItem::PureFn { .. }));
}
#[test]
fn module_only_blank_lines() {
let m = parse_module("\n\n\n");
assert!(m.items.is_empty());
}
#[test]
fn module_all_item_types() {
let source = r#"import lib/greeter
// a comment
fn greet() {
> echo hello
}
pure fn concat(a, b) {
> echo hello
}
effect Db {
shell db {
> echo start
}
}
test "basic" {
shell main {
> echo hi
}
}
"#;
let m = parse_module(source);
assert!(
m.items
.iter()
.any(|i| matches!(&i.node, AstItem::Import { .. }))
);
assert!(
m.items
.iter()
.any(|i| matches!(&i.node, AstItem::Fn { .. }))
);
assert!(
m.items
.iter()
.any(|i| matches!(&i.node, AstItem::PureFn { .. }))
);
assert!(
m.items
.iter()
.any(|i| matches!(&i.node, AstItem::Effect { .. }))
);
assert!(
m.items
.iter()
.any(|i| matches!(&i.node, AstItem::Test { .. }))
);
}
#[test]
fn public_parse_api_success() {
let source = r#"test "basic" {
shell main {
> echo hi
}
}
"#;
let result = crate::dsl::parser::parse(source);
assert!(result.is_ok());
let m = result.unwrap();
assert_eq!(m.items.len(), 1);
assert!(matches!(&m.items[0].node, AstItem::Test { .. }));
}
#[test]
fn public_parse_api_error() {
let source = "this is not valid relux\n";
let result = crate::dsl::parser::parse(source);
assert!(result.is_err());
}
#[test]
fn module_with_only_comments() {
let m = parse_module(
r#"// first comment
// second comment
"#,
);
assert_eq!(m.items.len(), 2);
assert!(
m.items
.iter()
.all(|i| matches!(&i.node, AstItem::Comment { .. }))
);
}
#[test]
fn module_multiple_tests() {
let source = r#"test "first" {
shell main {
> echo a
}
}
test "second" {
shell main {
> echo b
}
}
"#;
let m = parse_module(source);
let test_count = m
.items
.iter()
.filter(|i| matches!(&i.node, AstItem::Test { .. }))
.count();
assert_eq!(test_count, 2);
}
}