#![cfg(test)]
use crate::ast::*;
use crate::ast_span::render_from_ast;
use crate::lexer::lex_with_trivia;
use crate::parse::parse_module;
use std::path::{Path, PathBuf};
fn render(src: &str) -> Result<String, String> {
let (_, trivia, _) = lex_with_trivia(src);
render_from_ast(src, &parse_module(src).0, &trivia)
}
fn parse(src: &str) -> Module {
parse_module(src).0
}
fn text(src: &str, span: Span) -> &str {
&src[span.start..span.end]
}
fn first_function<'a>(m: &'a Module, name: &str) -> &'a FunctionDecl {
m.decls
.iter()
.find_map(|d| match d {
Decl::Function(f) if f.name == name => Some(f),
_ => None,
})
.unwrap_or_else(|| panic!("function {} not found", name))
}
#[test]
fn leaf_and_composite_spans_are_tight() {
let src = "module M where\nf = g (a, b)\n";
let m = parse(src);
let f = first_function(&m, "f");
let body = &f.equations[0].body;
assert_eq!(text(src, body.span()), "g (a, b)");
match body {
Expr::App { func, args, .. } => {
assert_eq!(text(src, func.span()), "g");
assert_eq!(text(src, args[0].span()), "(a, b)");
match &args[0] {
Expr::Tuple { items, .. } => {
assert_eq!(text(src, items[0].span()), "a");
assert_eq!(text(src, items[1].span()), "b");
}
other => panic!("expected tuple, got {:?}", other),
}
}
other => panic!("expected app, got {:?}", other),
}
}
#[test]
fn do_block_span_covers_whole_block() {
let src = "module M where\nf = do\n a\n b\n";
let m = parse(src);
let f = first_function(&m, "f");
let body = &f.equations[0].body;
assert_eq!(text(src, body.span()), "do\n a\n b");
}
#[test]
fn typedef_span_covers_whole_construct() {
let src = "module M where\ndata Foo = Bar | Baz\n";
let m = parse(src);
let td = m
.decls
.iter()
.find(|d| matches!(d, Decl::TypeDef { .. }))
.expect("typedef");
let Decl::TypeDef { span, .. } = td else {
unreachable!()
};
assert_eq!(text(src, *span), "data Foo = Bar | Baz");
}
#[test]
fn template_field_and_signatory_spans() {
let src =
"module M where\ntemplate T\n with\n owner : Party\n where\n signatory owner\n";
let m = parse(src);
let Decl::Template(t) = m
.decls
.iter()
.find(|d| matches!(d, Decl::Template(_)))
.unwrap()
else {
unreachable!()
};
assert_eq!(text(src, t.fields[0].span), "owner : Party");
let sig = t
.body
.iter()
.find(|b| matches!(b, TemplateBodyDecl::Signatory { .. }))
.unwrap();
let TemplateBodyDecl::Signatory { span, .. } = sig else {
unreachable!()
};
assert_eq!(text(src, *span), "signatory owner");
}
#[test]
fn type_node_spans_are_tight() {
let src = r#"module M where
template T
with
owner : Party
where
signatory owner
key owner : Party
maintainer owner
choice Go : Optional (ContractId T)
controller owner
do pure None
interface I where
method : Numeric 10
f : ContractId T -> Script ()
f cid = pure ()
"#;
let m = parse(src);
let Decl::Template(t) = m
.decls
.iter()
.find(|d| matches!(d, Decl::Template(_)))
.unwrap()
else {
unreachable!()
};
let field_ty = t.fields[0].ty.as_ref().expect("field type");
assert_eq!(text(src, field_ty.span()), "Party");
let key_ty = t
.body
.iter()
.find_map(|b| match b {
TemplateBodyDecl::Key { ty, .. } => ty.as_ref(),
_ => None,
})
.expect("key type");
assert_eq!(text(src, key_ty.span()), "Party");
let choice_ty = t
.body
.iter()
.find_map(|b| match b {
TemplateBodyDecl::Choice(c) => c.return_ty.as_ref(),
_ => None,
})
.expect("choice return type");
assert_eq!(text(src, choice_ty.span()), "Optional (ContractId T)");
let Decl::Interface(i) = m
.decls
.iter()
.find(|d| matches!(d, Decl::Interface(_)))
.unwrap()
else {
unreachable!()
};
let method_ty = i.methods[0].ty.as_ref().expect("method type");
assert_eq!(text(src, method_ty.span()), "Numeric 10");
let f = first_function(&m, "f");
let fn_ty = f.ty.as_ref().expect("function signature type");
assert_eq!(text(src, fn_ty.span()), "ContractId T -> Script ()");
}
#[test]
fn render_from_ast_roundtrips_small_programs() {
let cases = [
"module M where\nf = 1\n",
"module M where\nf x = if x then 1 else 2\n",
"module M where\nf = do\n a <- step\n pure a\n",
"module M where\n-- a comment\nf = g (a, b) -- trailing\n",
"module M where\ndata Foo = Bar with x : Int\nf = 1\n",
"f = 1\ng = 2\n", ];
for src in cases {
match render(src) {
Ok(out) => assert_eq!(out, src, "roundtrip mismatch for {:?}", src),
Err(e) => panic!("render_from_ast failed for {:?}: {}", src, e),
}
}
}
#[test]
fn multi_name_field_spans_are_disjoint() {
let src = "module M where\ntemplate T\n with\n a, b : Int\n where\n signatory a\n";
let m = parse(src);
let Decl::Template(t) = m
.decls
.iter()
.find(|d| matches!(d, Decl::Template(_)))
.unwrap()
else {
unreachable!()
};
assert_eq!(text(src, t.fields[0].span), "a");
assert_eq!(text(src, t.fields[1].span), "b : Int");
assert!(t.fields[0].span.end <= t.fields[1].span.start);
}
#[test]
fn separated_signature_does_not_straddle_sibling() {
let src = "module M where\nf : Int\ng = 2\nf = 1\n";
let m = parse(src);
assert_eq!(render(src).as_deref(), Ok(src));
let f = first_function(&m, "f");
let g = first_function(&m, "g");
assert_eq!(text(src, f.span), "f = 1");
assert!(!f.span.contains(&g.span));
}
fn run_corpus(root: &Path) -> (usize, Vec<String>) {
let mut files = Vec::new();
collect(root, &mut files);
let mut failures = Vec::new();
for f in &files {
let Ok(src) = std::fs::read_to_string(f) else {
continue;
};
if let Err(e) = render(&src) {
failures.push(format!("{}: {}", f.display(), e));
}
}
(files.len(), failures)
}
#[test]
fn span_oracle_over_finance_corpus() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../corpus/daml-finance/daml");
if !root.exists() {
eprintln!("corpus absent (published crate?), skipping");
return;
}
let (n, failures) = run_corpus(&root);
assert!(n > 600, "finance corpus incomplete: {} files", n);
if !failures.is_empty() {
let shown: Vec<_> = failures.iter().take(20).cloned().collect();
panic!(
"{} / {} files failed render_from_ast:\n{}",
failures.len(),
n,
shown.join("\n")
);
}
}
#[test]
fn render_lossless_over_finance_corpus() {
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,
"corpus incomplete: {} files",
files.len()
);
let mut checked = 0usize;
for f in &files {
let Ok(src) = std::fs::read_to_string(f) else {
continue;
};
let (tokens, trivia, errors) = lex_with_trivia(&src);
if !errors.is_empty() {
continue; }
checked += 1;
if let Err(e) = crate::lexer::render_lossless(&src, &tokens, &trivia) {
panic!("round trip failed for {}: {}", f.display(), e);
}
}
assert!(checked > 600, "too few files round-tripped: {}", checked);
}
fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
collect(&p, out);
} else if p.extension().is_some_and(|e| e == "daml") {
out.push(p);
}
}
}