#![cfg(test)]
use crate::ast::*;
use crate::ast_span::render_from_ast;
use crate::lexer::lex_with_trivia;
use crate::parse::parse_module;
fn parse(src: &str) -> Module {
parse_module(src).0
}
fn text(src: &str, span: Span) -> &str {
&src[span.start..span.end]
}
fn body_of(src: &str) -> Expr {
let m = parse(src);
let f = m
.decls
.iter()
.find_map(|d| match d {
Decl::Function(f) if f.name == "f" => Some(f),
_ => None,
})
.expect("function f");
f.equations[0].body.clone()
}
fn as_proj(e: &Expr) -> (&Expr, &Expr) {
match e {
Expr::BinOp { op, lhs, rhs, .. } if op == "." => (lhs, rhs),
other => panic!("expected projection BinOp, got {other:?}"),
}
}
#[test]
fn projection_binds_tighter_than_application() {
let src = "module M where\nf = length this.note\n";
let body = body_of(src);
match &body {
Expr::App { func, args, .. } => {
assert_eq!(text(src, func.span()), "length");
assert_eq!(args.len(), 1, "expected one argument, got {args:?}");
assert_eq!(text(src, args[0].span()), "this.note");
let (lhs, rhs) = as_proj(&args[0]);
assert!(matches!(lhs, Expr::Var { name, .. } if name == "this"));
assert!(matches!(rhs, Expr::Var { name, .. } if name == "note"));
}
other => panic!("expected application, got {other:?}"),
}
}
#[test]
fn bare_projection_is_a_projection() {
let src = "module M where\nf = this.note\n";
let body = body_of(src);
assert_eq!(text(src, body.span()), "this.note");
let (lhs, rhs) = as_proj(&body);
assert!(matches!(lhs, Expr::Var { name, .. } if name == "this"));
assert!(matches!(rhs, Expr::Var { name, .. } if name == "note"));
}
#[test]
fn chained_projection_left_nests() {
let src = "module M where\nf = a.b.c\n";
let body = body_of(src);
assert_eq!(text(src, body.span()), "a.b.c");
let (lhs, rhs) = as_proj(&body);
assert!(matches!(rhs, Expr::Var { name, .. } if name == "c"));
assert_eq!(text(src, lhs.span()), "a.b");
let (a, b) = as_proj(lhs);
assert!(matches!(a, Expr::Var { name, .. } if name == "a"));
assert!(matches!(b, Expr::Var { name, .. } if name == "b"));
}
#[test]
fn qualified_name_is_not_a_projection() {
let src = "module M where\nf = Map.lookup k\n";
let body = body_of(src);
match &body {
Expr::App { func, args, .. } => {
match func.as_ref() {
Expr::Var {
qualifier, name, ..
} => {
assert_eq!(qualifier.as_deref(), Some("Map"));
assert_eq!(name, "lookup");
}
other => panic!("expected qualified Var head, got {other:?}"),
}
assert_eq!(args.len(), 1);
assert!(matches!(&args[0], Expr::Var { name, .. } if name == "k"));
}
other => panic!("expected application, got {other:?}"),
}
}
#[test]
fn spaced_dot_stays_composition_not_projection() {
let src = "module M where\nf = compose g h\ncompose g h = g . h\n";
let m = parse(src);
let compose = m
.decls
.iter()
.find_map(|d| match d {
Decl::Function(f) if f.name == "compose" => Some(f),
_ => None,
})
.expect("compose");
let body = &compose.equations[0].body;
match body {
Expr::BinOp { op, lhs, rhs, .. } if op == "." => {
assert!(matches!(lhs.as_ref(), Expr::Var { name, .. } if name == "g"));
assert!(matches!(rhs.as_ref(), Expr::Var { name, .. } if name == "h"));
}
other => panic!("expected composition BinOp, got {other:?}"),
}
}
#[test]
fn newline_separated_dot_stays_composition() {
let src = "module M where\ncompose g h = g\n . h\n";
let m = parse(src);
let compose = m
.decls
.iter()
.find_map(|d| match d {
Decl::Function(f) if f.name == "compose" => Some(f),
_ => None,
})
.expect("compose");
let body = &compose.equations[0].body;
match body {
Expr::BinOp { op, lhs, rhs, .. } if op == "." => {
assert!(matches!(lhs.as_ref(), Expr::Var { name, .. } if name == "g"));
assert!(matches!(rhs.as_ref(), Expr::Var { name, .. } if name == "h"));
}
other => panic!("expected composition BinOp across newline, got {other:?}"),
}
let (_, trivia, _) = lex_with_trivia(src);
assert_eq!(render_from_ast(src, &m, &trivia).as_deref(), Ok(src));
}
#[test]
fn projection_inside_assertion_guard() {
let src = "module M where\nf x = assertMsg \"pos\" (x.amount > 0.0)\n";
let body = body_of(src);
let cmp = find_binop(&body, ">").expect("comparison > present");
let (lhs, _rhs) = match cmp {
Expr::BinOp { lhs, rhs, .. } => (lhs.as_ref(), rhs.as_ref()),
_ => unreachable!(),
};
assert_eq!(text(src, lhs.span()), "x.amount");
let (base, field) = as_proj(lhs);
assert!(matches!(base, Expr::Var { name, .. } if name == "x"));
assert!(matches!(field, Expr::Var { name, .. } if name == "amount"));
}
fn find_binop<'a>(e: &'a Expr, op: &str) -> Option<&'a Expr> {
if let Expr::BinOp { op: o, .. } = e {
if o == op {
return Some(e);
}
}
for child in children(e) {
if let Some(found) = find_binop(child, op) {
return Some(found);
}
}
None
}
fn children(e: &Expr) -> Vec<&Expr> {
match e {
Expr::App { func, args, .. } => {
let mut v = vec![func.as_ref()];
v.extend(args.iter());
v
}
Expr::BinOp { lhs, rhs, .. } => vec![lhs.as_ref(), rhs.as_ref()],
Expr::Neg { expr, .. } => vec![expr.as_ref()],
_ => Vec::new(),
}
}
#[test]
fn projection_span_is_tight() {
let src = "module M where\nf = length this.note\n";
let body = body_of(src);
let arg = match &body {
Expr::App { args, .. } => args[0].clone(),
other => panic!("expected app, got {other:?}"),
};
assert_eq!(text(src, arg.span()), "this.note");
}
#[test]
fn projection_roundtrips_through_oracle() {
let cases = [
"module M where\nf = length this.note\n",
"module M where\nf = a.b.c\n",
"module M where\nf = Map.lookup k\n",
"module M where\nf x = assertMsg \"pos\" (x.amount > 0.0)\n",
"module M where\nf = (g x).note\n",
];
for src in cases {
let (_, trivia, _) = lex_with_trivia(src);
match render_from_ast(src, &parse(src), &trivia) {
Ok(out) => assert_eq!(out, src, "roundtrip mismatch for {src:?}"),
Err(e) => panic!("oracle failed for {src:?}: {e}"),
}
}
}