#![cfg(test)]
use crate::ast::*;
use crate::parse::parse_module;
use std::path::PathBuf;
fn parse(src: &str) -> Module {
parse_module(src).0
}
fn wrap(src: &str) -> Module {
parse(&format!("module M where\n{src}\n"))
}
fn typedef<'a>(m: &'a Module, name: &str) -> &'a Decl {
m.decls
.iter()
.find(|d| matches!(d, Decl::TypeDef { name: n, .. } if n == name))
.unwrap_or_else(|| panic!("typedef {name} not found"))
}
fn con(name: &str) -> Type {
Type::Con {
qualifier: None,
name: name.to_string(),
span: Span::default(),
}
}
fn app(head: Type, args: Vec<Type>) -> Type {
Type::App(Box::new(head), args, Span::default())
}
fn var(name: &str) -> Type {
Type::Var(name.to_string(), Span::default())
}
#[test]
fn record_data_decl_exposes_fields() {
let m = wrap("data Asset = Asset with\n amount : Decimal\n owner : Party\n");
let Decl::TypeDef {
constructors,
synonym,
deriving,
..
} = typedef(&m, "Asset")
else {
panic!("expected TypeDef");
};
assert!(synonym.is_none());
assert!(deriving.is_empty());
assert_eq!(constructors.len(), 1);
let c = &constructors[0];
assert_eq!(c.name, "Asset");
assert!(c.arg_types.is_empty());
assert_eq!(c.fields.len(), 2);
assert_eq!(c.fields[0].name, "amount");
assert_eq!(c.fields[0].ty, Some(con("Decimal")));
assert_eq!(c.fields[1].name, "owner");
assert_eq!(c.fields[1].ty, Some(con("Party")));
}
#[test]
fn single_line_record() {
let m = wrap("data P = P with amount : Decimal");
let Decl::TypeDef { constructors, .. } = typedef(&m, "P") else {
panic!()
};
assert_eq!(constructors.len(), 1);
assert_eq!(constructors[0].fields.len(), 1);
assert_eq!(constructors[0].fields[0].name, "amount");
assert_eq!(constructors[0].fields[0].ty, Some(con("Decimal")));
}
#[test]
fn enum_constructors_and_deriving() {
let m = wrap("data MyEnum = MyEnum1 | MyEnum2 deriving (Show, Eq)");
let Decl::TypeDef {
constructors,
deriving,
..
} = typedef(&m, "MyEnum")
else {
panic!()
};
let names: Vec<&str> = constructors.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, ["MyEnum1", "MyEnum2"]);
assert!(constructors
.iter()
.all(|c| c.fields.is_empty() && c.arg_types.is_empty()));
assert_eq!(deriving, &["Show".to_string(), "Eq".to_string()]);
}
#[test]
fn multiline_enum_constructors() {
let m = wrap("data Lockers\n = CustodianOnly\n | RegulatorOnly\n | CustodianAndRegulator\n");
let Decl::TypeDef { constructors, .. } = typedef(&m, "Lockers") else {
panic!()
};
let names: Vec<&str> = constructors.iter().map(|c| c.name.as_str()).collect();
assert_eq!(
names,
["CustodianOnly", "RegulatorOnly", "CustodianAndRegulator"]
);
}
#[test]
fn sum_with_positional_args() {
let m = wrap("data Tree = Leaf | Node Int Text");
let Decl::TypeDef { constructors, .. } = typedef(&m, "Tree") else {
panic!()
};
assert_eq!(constructors.len(), 2);
assert_eq!(constructors[0].name, "Leaf");
assert!(constructors[0].arg_types.is_empty());
assert_eq!(constructors[1].name, "Node");
assert_eq!(constructors[1].arg_types, vec![con("Int"), con("Text")]);
}
#[test]
fn newtype_wraps_a_type() {
let m = wrap("newtype Money = Money Decimal");
let Decl::TypeDef {
keyword,
constructors,
..
} = typedef(&m, "Money")
else {
panic!()
};
assert_eq!(keyword, "newtype");
assert_eq!(constructors.len(), 1);
assert_eq!(constructors[0].name, "Money");
assert_eq!(constructors[0].arg_types, vec![con("Decimal")]);
}
#[test]
fn type_synonym_keeps_aliased_type() {
let m = wrap("type Name = Text");
let Decl::TypeDef {
keyword,
synonym,
constructors,
..
} = typedef(&m, "Name")
else {
panic!()
};
assert_eq!(keyword, "type");
assert!(constructors.is_empty());
assert_eq!(*synonym, Some(con("Text")));
}
#[test]
fn type_synonym_application() {
let m = wrap("type Amounts = Map Party Decimal");
let Decl::TypeDef { synonym, .. } = typedef(&m, "Amounts") else {
panic!()
};
assert_eq!(
*synonym,
Some(app(con("Map"), vec![con("Party"), con("Decimal")]))
);
}
#[test]
fn type_parameters_are_skipped() {
let m = wrap("data Box a = Box with\n value : a\n");
let Decl::TypeDef { constructors, .. } = typedef(&m, "Box") else {
panic!()
};
assert_eq!(constructors[0].name, "Box");
assert_eq!(constructors[0].fields.len(), 1);
assert_eq!(constructors[0].fields[0].name, "value");
assert_eq!(constructors[0].fields[0].ty, Some(var("a")));
}
#[test]
fn multiline_record_with_dedented_deriving() {
let m = wrap("data P = P\n with\n x : Int\n deriving (Eq)\n");
let Decl::TypeDef {
constructors,
deriving,
..
} = typedef(&m, "P")
else {
panic!()
};
assert_eq!(constructors.len(), 1);
assert_eq!(constructors[0].fields.len(), 1);
assert_eq!(constructors[0].fields[0].name, "x");
assert_eq!(deriving, &["Eq".to_string()]);
}
#[test]
fn class_and_instance_stay_opaque() {
let m = wrap("class Foo a where\n bar : a -> Int\n");
let Decl::TypeDef {
keyword,
constructors,
synonym,
..
} = typedef(&m, "Foo")
else {
panic!()
};
assert_eq!(keyword, "class");
assert!(constructors.is_empty());
assert!(synonym.is_none());
}
#[test]
fn deriving_strategy_keyword_still_captures_classes() {
let m = wrap("data T = A | B deriving stock (Show, Eq)");
let Decl::TypeDef { deriving, .. } = typedef(&m, "T") else {
panic!()
};
assert_eq!(deriving, &["Show".to_string(), "Eq".to_string()]);
}
#[test]
fn multiple_deriving_clauses_are_all_captured() {
let m = wrap("data T = A | B deriving (Show) deriving (Eq, Ord)");
let Decl::TypeDef { deriving, .. } = typedef(&m, "T") else {
panic!()
};
assert_eq!(
deriving,
&["Show".to_string(), "Eq".to_string(), "Ord".to_string()]
);
}
#[test]
fn infix_constructor_stays_opaque_not_fabricated() {
let m = wrap("data T = Int :+: Int");
let Decl::TypeDef { constructors, .. } = typedef(&m, "T") else {
panic!()
};
assert!(
constructors.is_empty(),
"infix constructor must not fabricate a name, got {constructors:?}"
);
}
#[test]
fn strictness_bang_constructor_stays_opaque_not_understated() {
let m = wrap("data T = T !Int !Text");
let Decl::TypeDef { constructors, .. } = typedef(&m, "T") else {
panic!()
};
assert!(constructors.is_empty());
}
#[test]
fn empty_with_block_does_not_swallow_next_decl() {
let m = wrap(
"data E\n = First with\n x : Int\n | Empty with\n -- all fields commented out\n | Last with\n y : Int\n\nafter : Int\nafter = 1\n",
);
assert!(m
.decls
.iter()
.any(|d| matches!(d, Decl::TypeDef { name, .. } if name == "E")));
assert!(m
.decls
.iter()
.any(|d| matches!(d, Decl::Function(f) if f.name == "after")));
}
#[test]
fn finance_corpus_exposes_record_fields() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../corpus/daml-finance/daml");
if !root.exists() {
assert!(
std::env::var_os("CI").is_none(),
"vendored corpus missing under CI (was it committed?): {}",
root.display()
);
eprintln!("corpus absent (published crate?), skipping");
return;
}
let mut files = Vec::new();
collect(&root, &mut files);
assert!(
files.len() > 600,
"finance corpus incomplete: {}",
files.len()
);
let mut decls_with_fields = 0usize;
let mut decls_with_ctors = 0usize;
let mut lock_lockers_seen = false;
for f in &files {
let Ok(src) = std::fs::read_to_string(f) else {
continue;
};
for d in &parse(&src).decls {
let Decl::TypeDef {
name, constructors, ..
} = d
else {
continue;
};
if !constructors.is_empty() {
decls_with_ctors += 1;
}
if constructors.iter().any(|c| !c.fields.is_empty()) {
decls_with_fields += 1;
}
if name == "Lock"
&& constructors
.iter()
.any(|c| c.fields.iter().any(|fl| fl.name == "lockers"))
{
lock_lockers_seen = true;
}
}
}
assert!(
decls_with_ctors > 100,
"expected many constructor-bearing decls, got {decls_with_ctors}"
);
assert!(
decls_with_fields > 50,
"expected many record-field-bearing decls, got {decls_with_fields}"
);
assert!(
lock_lockers_seen,
"expected a `Lock` record exposing a `lockers` field"
);
}
fn collect(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
collect(&p, out);
} else if p.extension().is_some_and(|x| x == "daml") {
out.push(p);
}
}
}