use daml_parser::ast::*;
use daml_parser::layout::resolve_layout;
use daml_parser::lexer::{lex_with_trivia, TriviaKind};
use daml_parser::parse::parse_module;
const INDENT: i64 = 2;
const MAX_STRUCTURAL_PASSES: usize = 6;
pub fn format_ast(src: &str) -> String {
if has_source_location_expectation(src) {
return src.to_string();
}
let mut base = src.to_string();
for _ in 0..MAX_STRUCTURAL_PASSES {
let mut next = base.clone();
next = gated_module_pass(&next);
next = gated_template_pass(&next);
next = gated_choice_pass(&next);
next = gated_type_def_pass(&next);
next = gated_guard_pass(&next);
next = gated_record_update_pass(&next);
next = gated_try_pass(&next);
next = gated_continuation_pass(&next);
next = gated_do_pass(&next);
next = gated_if_pass(&next);
next = gated_case_pass(&next);
next = gated_letin_pass(&next);
next = gated_con_with_pass(&next);
if next == base {
break;
}
base = next;
}
let full = crate::normalize_gaps(&base, true);
if same_tokens(&base, &full) {
return full;
}
let ws_only = crate::normalize_gaps(&base, false);
if same_tokens(&base, &ws_only) {
return ws_only;
}
base
}
fn gated_do_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_do_blocks(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_if_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_ifs(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_case_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_cases(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_letin_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_letins(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_con_with_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_con_with(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_template_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_templates(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_module_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_modules_and_imports(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_choice_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_choices(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_type_def_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_type_defs(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_guard_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_guards(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_record_update_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_record_updates(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_try_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_tries(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_continuation_pass(src: &str) -> String {
let (module, _) = parse_module(src);
let r = reindent_continuations(src, &module);
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
pub fn coverage(src: &str) -> (usize, usize) {
let (module, _diags) = parse_module(src);
let candidates = do_block_edits(src, &module).len()
+ module_edits(src, &module).len()
+ choice_edits(src, &module).len()
+ type_def_edits(src, &module).len()
+ guard_edits(src, &module).len()
+ record_update_edits(src, &module).len()
+ try_edits(src, &module).len()
+ continuation_edits(src, &module).len()
+ if_edits(src, &module).len()
+ case_edits(src, &module).len()
+ letin_edits(src, &module).len()
+ con_with_edits(src, &module).len()
+ template_edits(src, &module).len();
(candidates, modeled_construct_count(&module))
}
fn modeled_construct_count(module: &Module) -> usize {
let mut count = 0usize;
walk_module_expressions(module, &mut |e| match e {
Expr::Do { .. } | Expr::If { .. } | Expr::Case { .. } | Expr::LetIn { .. } => count += 1,
Expr::Record { base, fields, .. }
if matches!(base.as_ref(), Expr::Con { .. }) && !fields.is_empty() =>
{
count += 1
}
_ => {}
});
for d in &module.decls {
match d {
Decl::Template(t) if !t.fields.is_empty() || !t.body.is_empty() => count += 1,
Decl::Interface(i) if !i.methods.is_empty() || !i.choices.is_empty() => count += 1,
Decl::TypeDef { .. } => count += 1,
_ => {}
}
}
count += module.imports.iter().filter(|i| !i.span.is_empty()).count();
count
}
fn same_tokens(a: &str, b: &str) -> bool {
let la = resolve_layout(lex_with_trivia(a).0);
let lb = resolve_layout(lex_with_trivia(b).0);
la.len() == lb.len() && la.iter().zip(&lb).all(|(x, y)| x.tok == y.tok)
}
fn has_source_location_expectation(src: &str) -> bool {
src.lines()
.filter(|line| line.trim_start().starts_with("-- @"))
.any(|line| {
line.contains("range=")
|| line.contains(".location")
|| line.contains("start_line")
|| line.contains("start_col")
|| line.contains("end_line")
|| line.contains("end_col")
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Edit {
child_start: usize,
block_end: usize,
delta: i64,
}
fn reindent_do_blocks(src: &str, module: &Module) -> String {
let edits = do_block_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn do_block_edits(src: &str, module: &Module) -> Vec<Edit> {
let mut do_block_spans: Vec<Span> = Vec::new();
collect_do_block_spans(module, &mut do_block_spans);
do_block_spans.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for do_span in do_block_spans {
if accepted
.iter()
.any(|a| a.start <= do_span.start && do_span.end <= a.end && *a != do_span)
{
continue;
}
let do_line = line_of(&line_starts, do_span.start);
let do_indent = indent_of(src, &line_starts, do_line);
let Some(first_stmt_line) =
first_code_line_after(src, &line_starts, &comments, do_line, do_span.end)
else {
continue; };
accepted.push(do_span);
if leading_has_tab(src, line_starts[first_stmt_line]) {
continue;
}
let cur = indent_of(src, &line_starts, first_stmt_line);
let delta = (do_indent + INDENT) - cur;
if delta != 0 {
edits.push(Edit {
child_start: line_starts[first_stmt_line],
block_end: do_span.end,
delta,
});
}
}
edits
}
fn apply_shifts(src: &str, edits: &[Edit]) -> String {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut out = String::with_capacity(src.len());
for (li, &ls) in line_starts.iter().enumerate() {
let le = *line_starts.get(li + 1).unwrap_or(&src.len());
let line = &src[ls..le];
let trimmed = line.trim_start_matches(' ');
let cur = line.len() - trimmed.len();
let content_byte = ls + cur;
let delta = edits
.iter()
.find(|e| e.child_start <= content_byte && content_byte < e.block_end)
.map(|e| e.delta)
.unwrap_or(0);
if delta == 0
|| line.trim().is_empty()
|| is_comment_line(&comments, content_byte)
|| leading_has_tab(src, ls)
{
out.push_str(line);
continue;
}
let new = (cur as i64 + delta).max(0) as usize;
out.push_str(&" ".repeat(new));
out.push_str(&line[cur..]);
}
out
}
fn comment_spans(src: &str) -> Vec<(usize, usize)> {
let (_t, trivia, _e) = lex_with_trivia(src);
let mut v: Vec<(usize, usize)> = trivia
.iter()
.filter(|t| matches!(t.kind, TriviaKind::LineComment | TriviaKind::BlockComment))
.map(|t| (t.start, t.end))
.collect();
v.sort_by_key(|&(s, _)| s);
v
}
fn is_comment_line(comments: &[(usize, usize)], content_byte: usize) -> bool {
comments
.iter()
.any(|&(s, e)| s <= content_byte && content_byte < e)
}
fn line_start_table(src: &str) -> Vec<usize> {
std::iter::once(0)
.chain(src.match_indices('\n').map(|(i, _)| i + 1))
.collect()
}
fn line_of(line_starts: &[usize], byte: usize) -> usize {
match line_starts.binary_search(&byte) {
Ok(i) => i,
Err(i) => i - 1,
}
}
fn indent_of(src: &str, line_starts: &[usize], line: usize) -> i64 {
src[line_starts[line]..]
.chars()
.take_while(|&c| c == ' ')
.count() as i64
}
fn leading_has_tab(src: &str, line_start: usize) -> bool {
src[line_start..]
.chars()
.take_while(|&c| c == ' ' || c == '\t')
.any(|c| c == '\t')
}
fn first_code_line_after(
src: &str,
line_starts: &[usize],
comments: &[(usize, usize)],
do_line: usize,
block_end: usize,
) -> Option<usize> {
let mut l = do_line + 1;
while l < line_starts.len() && line_starts[l] < block_end {
let ls = line_starts[l];
let le = *line_starts.get(l + 1).unwrap_or(&src.len());
let line = &src[ls..le];
let cur = line.len() - line.trim_start_matches(' ').len();
if !line.trim().is_empty() && !is_comment_line(comments, ls + cur) {
return Some(l);
}
l += 1;
}
None
}
fn next_code_line_starts_with_keyword(
src: &str,
line_starts: &[usize],
comments: &[(usize, usize)],
after_line: usize,
keyword: &str,
) -> Option<usize> {
let mut l = after_line + 1;
while l < line_starts.len() {
let ls = line_starts[l];
let le = *line_starts.get(l + 1).unwrap_or(&src.len());
let line = &src[ls..le];
let cur = line.len() - line.trim_start_matches(' ').len();
let trimmed = line[cur..].trim_end();
if line.trim().is_empty() || is_comment_line(comments, ls + cur) {
l += 1;
continue;
}
return trimmed
.strip_prefix(keyword)
.is_some_and(|rest| {
rest.is_empty()
|| rest
.chars()
.next()
.is_some_and(|c| !c.is_ascii_alphanumeric() && c != '_')
})
.then_some(l);
}
None
}
fn collect_do_block_spans(module: &Module, do_block_spans: &mut Vec<Span>) {
walk_module_expressions(module, &mut |expr| {
if let Expr::Do { span, .. } = expr {
do_block_spans.push(*span);
}
});
}
fn walk_module_expressions(module: &Module, f: &mut impl FnMut(&Expr)) {
for decl in &module.decls {
match decl {
Decl::Function(fun) => {
for eq in &fun.equations {
walk_expression(&eq.body, f);
for (g, b) in &eq.guards {
walk_expression(g, f);
walk_expression(b, f);
}
for wb in &eq.where_bindings {
walk_expression(&wb.expr, f);
}
}
}
Decl::Template(t) => {
for b in &t.body {
match b {
TemplateBodyDecl::Choice(c) => {
if let Some(body) = &c.body {
walk_expression(body, f);
}
}
TemplateBodyDecl::Ensure { expr, .. }
| TemplateBodyDecl::Key { expr, .. }
| TemplateBodyDecl::Maintainer { expr, .. } => walk_expression(expr, f),
_ => {}
}
}
}
_ => {}
}
}
}
fn walk_expression(expr: &Expr, f: &mut impl FnMut(&Expr)) {
f(expr);
match expr {
Expr::App { func, args, .. } => {
walk_expression(func, f);
args.iter().for_each(|arg| walk_expression(arg, f));
}
Expr::BinOp { lhs, rhs, .. } => {
walk_expression(lhs, f);
walk_expression(rhs, f);
}
Expr::Neg { expr, .. } | Expr::Lambda { body: expr, .. } => walk_expression(expr, f),
Expr::If {
cond,
then_branch,
else_branch,
..
} => {
walk_expression(cond, f);
walk_expression(then_branch, f);
walk_expression(else_branch, f);
}
Expr::Case {
scrutinee, alts, ..
} => {
walk_expression(scrutinee, f);
alts.iter().for_each(|alt| walk_expression(&alt.body, f));
}
Expr::Do { stmts, .. } => {
for s in stmts {
match s {
DoStmt::Bind { expr, .. } | DoStmt::Expr { expr, .. } => {
walk_expression(expr, f)
}
DoStmt::Let { bindings, .. } => {
bindings.iter().for_each(|b| walk_expression(&b.expr, f))
}
}
}
}
Expr::LetIn { bindings, body, .. } => {
bindings.iter().for_each(|b| walk_expression(&b.expr, f));
walk_expression(body, f);
}
Expr::Record { base, fields, .. } => {
walk_expression(base, f);
for fa in fields {
if let Some(v) = &fa.value {
walk_expression(v, f);
}
}
}
Expr::Tuple { items, .. } | Expr::List { items, .. } => {
items.iter().for_each(|item| walk_expression(item, f))
}
Expr::Try { body, handlers, .. } => {
walk_expression(body, f);
handlers
.iter()
.for_each(|handler| walk_expression(&handler.body, f));
}
Expr::Section {
operand: Some(o), ..
} => walk_expression(o, f),
_ => {}
}
}
fn find_keyword(
src: &str,
from: usize,
to: usize,
kw: &str,
comments: &[(usize, usize)],
) -> Option<usize> {
let hay = &src[from..to.min(src.len())];
let bytes = hay.as_bytes();
let mut i = 0;
while let Some(rel) = hay[i..].find(kw) {
let at = i + rel;
let before_ok = at == 0 || !is_ident_byte(bytes[at - 1]);
let after = at + kw.len();
let after_ok = after >= hay.len() || !is_ident_byte(bytes[after]);
let abs = from + at;
if before_ok && after_ok && !is_comment_line(comments, abs) {
return Some(abs);
}
i = at + 1;
}
None
}
fn if_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut ifs: Vec<(Span, usize, usize, Span, Span)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::If {
span,
cond,
then_branch,
else_branch,
..
} = e
{
ifs.push((
*span,
span.start,
cond.span().end,
then_branch.span(),
else_branch.span(),
));
}
});
ifs.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (if_span, if_byte, cond_end, then_span, else_span) in ifs {
if accepted
.iter()
.any(|a| a.start <= if_span.start && if_span.end <= a.end && *a != if_span)
{
continue;
}
accepted.push(if_span);
let if_line = line_of(&line_starts, if_byte);
if leading_has_tab(src, line_starts[if_line]) {
continue;
}
let if_col = src[line_starts[if_line]..if_byte].chars().count() as i64;
let target = if_col + INDENT;
let then_byte = find_keyword(src, cond_end, then_span.start, "then", &comments);
let else_byte = find_keyword(src, then_span.end, else_span.start, "else", &comments);
for (kw_byte, branch_end) in [(then_byte, then_span.end), (else_byte, else_span.end)] {
let Some(kw_byte) = kw_byte else { continue };
let kw_line = line_of(&line_starts, kw_byte);
let ls = line_starts[kw_line];
if src[ls..kw_byte].chars().any(|c| c != ' ') {
continue;
}
if leading_has_tab(src, ls) {
continue;
}
let cur = indent_of(src, &line_starts, kw_line);
let delta = target - cur;
if delta != 0 {
edits.push(Edit {
child_start: ls,
block_end: branch_end,
delta,
});
}
}
}
edits
}
fn reindent_ifs(src: &str, module: &Module) -> String {
let edits = if_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn case_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let mut cases: Vec<(Span, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Case { span, alts, .. } = e {
if let (Some(first), Some(last)) = (alts.first(), alts.last()) {
cases.push((*span, first.span.start, last.span.end));
}
}
});
cases.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (case_span, first_alt, last_alt_end) in cases {
if accepted
.iter()
.any(|a| a.start <= case_span.start && case_span.end <= a.end && *a != case_span)
{
continue;
}
accepted.push(case_span);
let case_line = line_of(&line_starts, case_span.start);
let alt_line = line_of(&line_starts, first_alt);
if alt_line <= case_line {
continue;
}
if leading_has_tab(src, line_starts[case_line])
|| leading_has_tab(src, line_starts[alt_line])
{
continue;
}
let case_indent = indent_of(src, &line_starts, case_line);
let cur = indent_of(src, &line_starts, alt_line);
let delta = (case_indent + INDENT) - cur;
if delta != 0 {
edits.push(Edit {
child_start: line_starts[alt_line],
block_end: last_alt_end,
delta,
});
}
}
edits
}
fn reindent_cases(src: &str, module: &Module) -> String {
let edits = case_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn letin_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let mut lets: Vec<(Span, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::LetIn { span, bindings, .. } = e {
if let (Some(first), Some(last)) = (bindings.first(), bindings.last()) {
lets.push((*span, first.span.start, last.span.end));
}
}
});
lets.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (let_span, first_bind, last_bind_end) in lets {
if accepted
.iter()
.any(|a| a.start <= let_span.start && let_span.end <= a.end && *a != let_span)
{
continue;
}
accepted.push(let_span);
let let_line = line_of(&line_starts, let_span.start);
let bind_line = line_of(&line_starts, first_bind);
if bind_line <= let_line {
continue;
}
if src[line_starts[let_line]..let_span.start]
.chars()
.any(|c| c != ' ')
{
continue;
}
if leading_has_tab(src, line_starts[let_line])
|| leading_has_tab(src, line_starts[bind_line])
{
continue;
}
let let_indent = indent_of(src, &line_starts, let_line);
let cur = indent_of(src, &line_starts, bind_line);
let delta = (let_indent + INDENT) - cur;
if delta != 0 {
edits.push(Edit {
child_start: line_starts[bind_line],
block_end: last_bind_end,
delta,
});
}
}
edits
}
fn reindent_letins(src: &str, module: &Module) -> String {
let edits = letin_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn con_with_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut recs: Vec<(Span, usize, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Record {
span, base, fields, ..
} = e
{
if !matches!(base.as_ref(), Expr::Con { .. }) {
return;
}
if let (Some(first), Some(last)) = (fields.first(), fields.last()) {
recs.push((*span, base.span().end, first.span.start, last.span.end));
}
}
});
recs.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (rec_span, base_end, first_field, last_field_end) in recs {
if accepted
.iter()
.any(|a| a.start <= rec_span.start && rec_span.end <= a.end && *a != rec_span)
{
continue;
}
accepted.push(rec_span);
let rec_line = line_of(&line_starts, rec_span.start);
let field_line = line_of(&line_starts, first_field);
if field_line <= rec_line {
continue;
}
match find_keyword(src, base_end, first_field, "with", &comments) {
Some(w) if line_of(&line_starts, w) == rec_line => {}
_ => continue,
}
if leading_has_tab(src, line_starts[rec_line])
|| leading_has_tab(src, line_starts[field_line])
{
continue;
}
let rec_indent = indent_of(src, &line_starts, rec_line);
let cur = indent_of(src, &line_starts, field_line);
let target = rec_indent + INDENT;
if next_code_line_starts_with_keyword(
src,
&line_starts,
&comments,
line_of(&line_starts, last_field_end.saturating_sub(1)),
"where",
)
.is_some_and(|next_line| indent_of(src, &line_starts, next_line) <= target)
{
continue;
}
let delta = target - cur;
if delta != 0 {
edits.push(Edit {
child_start: line_starts[field_line],
block_end: last_field_end,
delta,
});
}
}
edits
}
fn reindent_con_with(src: &str, module: &Module) -> String {
let edits = con_with_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn push_line_edit(edits: &mut Vec<Edit>, ls: &[usize], src: &str, line: usize, target: i64) {
if leading_has_tab(src, ls[line]) {
return;
}
let delta = target - indent_of(src, ls, line);
if delta != 0 {
let end = *ls.get(line + 1).unwrap_or(&src.len());
edits.push(Edit {
child_start: ls[line],
block_end: end,
delta,
});
}
}
fn push_block_edit(
edits: &mut Vec<Edit>,
ls: &[usize],
src: &str,
first_byte: usize,
end_byte: usize,
target: i64,
head_line: usize,
) {
let first_line = line_of(ls, first_byte);
if first_line <= head_line {
return;
}
if src[ls[first_line]..first_byte].chars().any(|c| c != ' ') {
return;
}
if leading_has_tab(src, ls[first_line]) {
return;
}
let delta = target - indent_of(src, ls, first_line);
if delta != 0 {
edits.push(Edit {
child_start: ls[first_line],
block_end: end_byte,
delta,
});
}
}
const fn body_decl_span(d: &TemplateBodyDecl) -> Span {
match d {
TemplateBodyDecl::Signatory { span, .. }
| TemplateBodyDecl::Observer { span, .. }
| TemplateBodyDecl::Ensure { span, .. }
| TemplateBodyDecl::Key { span, .. }
| TemplateBodyDecl::Maintainer { span, .. }
| TemplateBodyDecl::Other { span, .. } => *span,
TemplateBodyDecl::Choice(c) => c.span,
TemplateBodyDecl::InterfaceInstance(i) => i.span,
}
}
#[allow(clippy::too_many_arguments)]
fn reindent_keyword_block(
edits: &mut Vec<Edit>,
ls: &[usize],
src: &str,
comments: &[(usize, usize)],
head_line: usize,
kw_target: i64,
body_target: i64,
kw: &str,
kw_from: usize,
block_first: usize,
block_last_end: usize,
) {
if let Some(w) = find_keyword(src, kw_from, block_first, kw, comments) {
if line_of(ls, w) > head_line {
push_line_edit(edits, ls, src, line_of(ls, w), kw_target);
}
}
push_block_edit(
edits,
ls,
src,
block_first,
block_last_end,
body_target,
head_line,
);
}
fn template_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
for d in &module.decls {
let head_byte = match d {
Decl::Template(t) => t.span.start,
Decl::Interface(i) => i.span.start,
_ => continue,
};
let head_line = line_of(&line_starts, head_byte);
if leading_has_tab(src, line_starts[head_line]) {
continue;
}
let head_indent = indent_of(src, &line_starts, head_line);
let kw_target = head_indent + INDENT;
match d {
Decl::Template(t) => {
let body_target = head_indent + 2 * INDENT;
if let (Some(f0), Some(fl)) = (t.fields.first(), t.fields.last()) {
reindent_keyword_block(
&mut edits,
&line_starts,
src,
&comments,
head_line,
kw_target,
body_target,
"with",
t.span.start,
f0.span.start,
fl.span.end,
);
}
if let (Some(b0), Some(bl)) = (t.body.first(), t.body.last()) {
let b0_start = body_decl_span(b0).start;
let bl_end = body_decl_span(bl).end;
let where_from = t.fields.last().map(|f| f.span.end).unwrap_or(t.span.start);
reindent_keyword_block(
&mut edits,
&line_starts,
src,
&comments,
head_line,
kw_target,
body_target,
"where",
where_from,
b0_start,
bl_end,
);
}
}
Decl::Interface(i) => {
let mut last_end = None;
for s in i
.methods
.iter()
.map(|m| m.span)
.chain(i.choices.iter().map(|c| c.span))
{
last_end = Some(last_end.map_or(s.end, |e: usize| e.max(s.end)));
}
if let Some(last_end) = last_end {
if let Some(fbl) =
first_code_line_after(src, &line_starts, &comments, head_line, last_end)
{
reindent_keyword_block(
&mut edits,
&line_starts,
src,
&comments,
head_line,
kw_target,
head_indent + INDENT,
"where",
i.span.start,
line_starts[fbl],
last_end,
);
}
}
}
_ => {}
}
}
edits
}
fn reindent_templates(src: &str, module: &Module) -> String {
let edits = template_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn module_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
if !module.header.is_empty() {
push_continuation_lines(
&mut edits,
src,
&line_starts,
&comments,
module.header,
INDENT,
);
}
for imp in &module.imports {
push_continuation_lines(&mut edits, src, &line_starts, &comments, imp.span, INDENT);
}
edits
}
fn reindent_modules_and_imports(src: &str, module: &Module) -> String {
let edits = module_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn push_continuation_lines(
edits: &mut Vec<Edit>,
src: &str,
ls: &[usize],
comments: &[(usize, usize)],
span: Span,
offset: i64,
) {
let head_line = line_of(ls, span.start);
let target = indent_of(src, ls, head_line) + offset;
let mut line = head_line + 1;
while line < ls.len() && ls[line] < span.end {
push_code_line_edit(edits, src, ls, comments, line, target);
line += 1;
}
}
fn collect_choices<'a>(module: &'a Module, choices: &mut Vec<&'a ChoiceDecl>) {
for decl in &module.decls {
match decl {
Decl::Template(t) => {
for body in &t.body {
if let TemplateBodyDecl::Choice(c) = body {
choices.push(c);
}
}
}
Decl::Interface(i) => choices.extend(i.choices.iter()),
_ => {}
}
}
choices.sort_by(|a, b| {
a.span
.start
.cmp(&b.span.start)
.then(b.span.end.cmp(&a.span.end))
});
}
fn choice_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut choices = Vec::new();
collect_choices(module, &mut choices);
let mut edits = Vec::new();
for c in choices {
let choice_line = line_of(&line_starts, c.span.start);
if leading_has_tab(src, line_starts[choice_line]) {
continue;
}
let choice_indent = indent_of(src, &line_starts, choice_line);
let clause_target = choice_indent + INDENT;
let nested_target = choice_indent + 2 * INDENT;
if let Some(ty) = &c.return_ty {
if let Some(colon) = find_symbol(src, c.span.start, ty.span().start, ":", &comments) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
colon,
ty.span().end,
clause_target,
);
}
}
if let (Some(first), Some(last)) = (c.params.first(), c.params.last()) {
let kw_from = c.return_ty.as_ref().map_or(c.span.start, |t| t.span().end);
if let Some(w) = find_keyword(src, kw_from, first.span.start, "with", &comments) {
let with_line = line_of(&line_starts, w);
let first_param_line = line_of(&line_starts, first.span.start);
if first_param_line == with_line {
push_span_block_edit(
&mut edits,
&line_starts,
src,
w,
last.span.end,
clause_target,
);
} else {
push_line_edit(&mut edits, &line_starts, src, with_line, clause_target);
push_block_edit(
&mut edits,
&line_starts,
src,
first.span.start,
last.span.end,
nested_target,
choice_line,
);
}
}
}
if let (Some(first), Some(last)) = (c.observers.first(), c.observers.last()) {
if let Some(k) =
find_keyword(src, c.span.start, first.span().start, "observer", &comments)
{
push_span_block_edit(
&mut edits,
&line_starts,
src,
k,
last.span().end,
clause_target,
);
}
}
if let (Some(first), Some(last)) = (c.controllers.first(), c.controllers.last()) {
if let Some(k) = find_keyword(
src,
c.span.start,
first.span().start,
"controller",
&comments,
) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
k,
last.span().end,
clause_target,
);
}
}
if let Some(body) = &c.body {
push_span_block_edit(
&mut edits,
&line_starts,
src,
body.span().start,
body.span().end,
clause_target,
);
}
}
edits
}
fn reindent_choices(src: &str, module: &Module) -> String {
let edits = choice_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn type_def_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
for decl in &module.decls {
let Decl::TypeDef { span, .. } = decl else {
continue;
};
let head_line = line_of(&line_starts, span.start);
if leading_has_tab(src, line_starts[head_line]) {
continue;
}
let head_indent = indent_of(src, &line_starts, head_line);
let head_has_with = line_contains_word(src, &line_starts, head_line, "with");
let mut in_with = head_has_with;
let mut with_body_target = if head_has_with {
first_body_anchor_indent_after(src, &line_starts, &comments, head_line, span.end)
.unwrap_or(head_indent + INDENT)
} else {
head_indent + 2 * INDENT
};
let head_has_where = line_contains_word(src, &line_starts, head_line, "where");
let mut in_where = head_has_where;
let mut where_body_target = if head_has_where {
first_body_anchor_indent_after(src, &line_starts, &comments, head_line, span.end)
.unwrap_or(head_indent + INDENT)
} else {
head_indent + INDENT
};
let mut after_variant = line_contains_symbol(src, &line_starts, head_line, "=");
let mut after_bar_variant = false;
let mut line = head_line + 1;
while line < line_starts.len() && line_starts[line] < span.end {
let Some(trimmed) = code_line_trimmed(src, &line_starts, &comments, line) else {
line += 1;
continue;
};
let target = if starts_with_word(trimmed, "where") {
in_with = false;
in_where = true;
where_body_target = head_indent + 2 * INDENT;
after_variant = false;
Some(head_indent + INDENT)
} else if starts_with_word(trimmed, "with") {
let target = if after_bar_variant {
head_indent + 2 * INDENT
} else {
head_indent + INDENT
};
in_with = true;
in_where = false;
with_body_target = target + INDENT;
after_variant = false;
after_bar_variant = false;
Some(target)
} else if trimmed.starts_with('=') || trimmed.starts_with('|') {
in_with = false;
in_where = false;
after_variant = true;
after_bar_variant = trimmed.starts_with('|');
Some(head_indent + INDENT)
} else if starts_with_word(trimmed, "deriving") {
in_with = false;
in_where = false;
after_variant = false;
after_bar_variant = false;
Some(head_indent + INDENT)
} else if in_with {
Some(with_body_target)
} else if in_where {
Some(where_body_target)
} else if after_variant {
Some(head_indent + INDENT)
} else {
None
};
if let Some(target) = target {
push_code_line_edit(&mut edits, src, &line_starts, &comments, line, target);
}
line += 1;
}
}
edits
}
fn first_body_anchor_indent_after(
src: &str,
ls: &[usize],
comments: &[(usize, usize)],
after_line: usize,
block_end: usize,
) -> Option<i64> {
let mut line = after_line + 1;
while line < ls.len() && ls[line] < block_end {
if leading_has_tab(src, ls[line]) {
return None;
}
let end = *ls.get(line + 1).unwrap_or(&src.len());
let text = &src[ls[line]..end];
let cur = text.len() - text.trim_start_matches(' ').len();
let trimmed = text[cur..].trim_end();
if trimmed.is_empty() {
line += 1;
continue;
}
if is_comment_line(comments, ls[line] + cur) && !trimmed.starts_with("{-#") {
line += 1;
continue;
}
return Some(indent_of(src, ls, line));
}
None
}
fn reindent_type_defs(src: &str, module: &Module) -> String {
let edits = type_def_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn guard_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
for decl in &module.decls {
let Decl::Function(fun) = decl else {
continue;
};
for eq in &fun.equations {
let eq_line = line_of(&line_starts, eq.span.start);
if leading_has_tab(src, line_starts[eq_line]) {
continue;
}
let guard_target = indent_of(src, &line_starts, eq_line) + INDENT;
let mut cursor = eq.span.start;
for (guard, body) in &eq.guards {
if let Some(pipe) = find_symbol(src, cursor, guard.span().start, "|", &comments) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
pipe,
body.span().end,
guard_target,
);
}
cursor = body.span().end;
}
if let (Some(first), Some(last)) = (eq.where_bindings.first(), eq.where_bindings.last())
{
if let Some(w) = find_keyword(src, cursor, first.span.start, "where", &comments) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
w,
w + "where".len(),
guard_target,
);
push_block_edit(
&mut edits,
&line_starts,
src,
first.span.start,
last.span.end,
guard_target + INDENT,
eq_line,
);
}
}
}
}
edits
}
fn reindent_guards(src: &str, module: &Module) -> String {
let edits = guard_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn record_update_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut recs: Vec<(Span, usize, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Record {
span, base, fields, ..
} = e
{
if matches!(base.as_ref(), Expr::Con { .. }) {
return;
}
if let (Some(first), Some(last)) = (fields.first(), fields.last()) {
recs.push((*span, base.span().end, first.span.start, last.span.end));
}
}
});
recs.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits = Vec::new();
for (rec_span, base_end, first_field, last_field_end) in recs {
let rec_line = line_of(&line_starts, rec_span.start);
let field_line = line_of(&line_starts, first_field);
if field_line <= rec_line || leading_has_tab(src, line_starts[rec_line]) {
continue;
}
let Some(w) = find_keyword(src, base_end, first_field, "with", &comments) else {
continue;
};
let with_line = line_of(&line_starts, w);
let rec_indent = indent_of(src, &line_starts, rec_line);
let field_target = if with_line > rec_line {
push_line_edit(
&mut edits,
&line_starts,
src,
with_line,
rec_indent + INDENT,
);
rec_indent + 2 * INDENT
} else {
rec_indent + INDENT
};
push_block_edit(
&mut edits,
&line_starts,
src,
first_field,
last_field_end,
field_target,
rec_line,
);
}
edits
}
fn reindent_record_updates(src: &str, module: &Module) -> String {
let edits = record_update_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn try_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut tries: Vec<(Span, Span, Vec<Span>)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Try {
span,
body,
handlers,
..
} = e
{
tries.push((
*span,
body.span(),
handlers.iter().map(|h| h.span).collect(),
));
}
});
tries.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits = Vec::new();
for (try_span, body_span, handlers) in tries {
let try_line = line_of(&line_starts, try_span.start);
if leading_has_tab(src, line_starts[try_line]) {
continue;
}
let try_col = src[line_starts[try_line]..try_span.start].chars().count() as i64;
let nested_target = try_col + INDENT;
push_block_edit(
&mut edits,
&line_starts,
src,
body_span.start,
body_span.end,
nested_target,
try_line,
);
if let Some(first_handler) = handlers.first() {
if let Some(catch) =
find_keyword(src, body_span.end, first_handler.start, "catch", &comments)
{
push_span_block_edit(
&mut edits,
&line_starts,
src,
catch,
catch + "catch".len(),
try_col,
);
}
}
if let (Some(first), Some(last)) = (handlers.first(), handlers.last()) {
push_block_edit(
&mut edits,
&line_starts,
src,
first.start,
last.end,
nested_target,
try_line,
);
}
}
edits
}
fn reindent_tries(src: &str, module: &Module) -> String {
let edits = try_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn continuation_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
walk_module_expressions(module, &mut |e| match e {
Expr::Tuple { span, items, .. } | Expr::List { span, items, .. } => {
push_item_continuations(
&mut edits,
src,
&line_starts,
&comments,
*span,
items.iter().map(Expr::span),
);
}
_ => {}
});
edits
}
fn push_item_continuations(
edits: &mut Vec<Edit>,
src: &str,
ls: &[usize],
comments: &[(usize, usize)],
span: Span,
items: impl Iterator<Item = Span>,
) {
let head_line = line_of(ls, span.start);
let target = indent_of(src, ls, head_line) + INDENT;
for item in items {
let item_line = line_of(ls, item.start);
if item_line <= head_line {
continue;
}
let mut first = item.start;
let line_start = ls[item_line];
if let Some(comma) = src[line_start..item.start].rfind(',') {
first = line_start + comma;
}
if is_comment_line(comments, first) {
continue;
}
push_span_block_edit(edits, ls, src, first, item.end, target);
}
}
fn reindent_continuations(src: &str, module: &Module) -> String {
let edits = continuation_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn push_span_block_edit(
edits: &mut Vec<Edit>,
ls: &[usize],
src: &str,
first_byte: usize,
end_byte: usize,
target: i64,
) {
let first_line = line_of(ls, first_byte);
if src[ls[first_line]..first_byte].chars().any(|c| c != ' ') {
return;
}
if leading_has_tab(src, ls[first_line]) {
return;
}
let delta = target - indent_of(src, ls, first_line);
if delta != 0 {
edits.push(Edit {
child_start: ls[first_line],
block_end: end_byte,
delta,
});
}
}
fn push_code_line_edit(
edits: &mut Vec<Edit>,
src: &str,
ls: &[usize],
comments: &[(usize, usize)],
line: usize,
target: i64,
) {
if code_line_trimmed(src, ls, comments, line).is_none() || leading_has_tab(src, ls[line]) {
return;
}
push_line_edit(edits, ls, src, line, target);
}
fn code_line_trimmed<'a>(
src: &'a str,
ls: &[usize],
comments: &[(usize, usize)],
line: usize,
) -> Option<&'a str> {
let start = *ls.get(line)?;
let end = *ls.get(line + 1).unwrap_or(&src.len());
let text = &src[start..end];
let cur = text.len() - text.trim_start_matches(' ').len();
let trimmed = text[cur..].trim_end();
if trimmed.is_empty() || is_comment_line(comments, start + cur) {
None
} else {
Some(trimmed)
}
}
fn starts_with_word(s: &str, word: &str) -> bool {
s.strip_prefix(word).is_some_and(|rest| {
rest.is_empty()
|| rest
.chars()
.next()
.is_some_and(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '\'')
})
}
fn line_contains_word(src: &str, ls: &[usize], line: usize, word: &str) -> bool {
let end = *ls.get(line + 1).unwrap_or(&src.len());
let line_text = &src[ls[line]..end];
let mut i = 0;
while let Some(rel) = line_text[i..].find(word) {
let at = i + rel;
let before_ok = at == 0 || !is_ident_byte(line_text.as_bytes()[at - 1]);
let after = at + word.len();
let after_ok = after >= line_text.len() || !is_ident_byte(line_text.as_bytes()[after]);
if before_ok && after_ok {
return true;
}
i = at + 1;
}
false
}
const fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'\''
}
fn line_contains_symbol(src: &str, ls: &[usize], line: usize, symbol: &str) -> bool {
let end = *ls.get(line + 1).unwrap_or(&src.len());
src[ls[line]..end].contains(symbol)
}
fn find_symbol(
src: &str,
from: usize,
to: usize,
symbol: &str,
comments: &[(usize, usize)],
) -> Option<usize> {
let hay = &src[from..to.min(src.len())];
let mut i = 0;
while let Some(rel) = hay[i..].find(symbol) {
let abs = from + i + rel;
if !is_comment_line(comments, abs) {
return Some(abs);
}
i += rel + symbol.len();
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comment_line_detection() {
let src = "x\n-- hi\ny\n";
let comments = comment_spans(src);
assert!(is_comment_line(&comments, 2));
assert!(!is_comment_line(&comments, 0)); assert!(!is_comment_line(&comments, 7)); }
#[test]
fn indent_and_line_helpers() {
let src = "a\n b\n";
let ls = line_start_table(src);
assert_eq!(indent_of(src, &ls, 0), 0);
assert_eq!(indent_of(src, &ls, 1), 4);
assert_eq!(line_of(&ls, 0), 0);
assert_eq!(line_of(&ls, 6), 1);
}
#[test]
fn do_body_reindented_to_anchor_plus_two() {
let src = "f = do\n pure ()\n";
let out = format_ast(src);
assert_eq!(out, "f = do\n pure ()\n");
}
#[test]
fn source_range_expectation_files_stay_byte_exact() {
let src = "module M where\n-- @ WARN range=3:8-3:9; x\nfoo : Int\nfoo = 1\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn source_location_query_files_stay_byte_exact() {
let src = "-- @QUERY-LF .location.range | (.start_line == 8 and .start_col == 9)\n\n\nmodule Locations where\nfoo : Int\nfoo = 1\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn idempotent_on_reindent() {
let src = "f = do\n pure ()\n";
let once = format_ast(src);
let twice = format_ast(&once);
assert_eq!(once, twice);
}
#[test]
fn leading_comment_not_measured_or_moved() {
let src = "f = do\n-- note\n pure ()\n";
let out = format_ast(src);
assert_eq!(out, "f = do\n-- note\n pure ()\n");
assert_eq!(format_ast(&out), out); }
#[test]
fn inline_do_left_alone() {
let src = "f = do pure ()\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn tab_indented_body_left_verbatim() {
let src = "f = do\n\t\tpure ()\n";
assert_eq!(format_ast(src), src);
assert_eq!(format_ast(&format_ast(src)), format_ast(src));
}
#[test]
fn do_block_starting_with_let_is_reindented() {
let src = "f = do\n let x = 1\n y = 2\n pure (x + y)\n";
let out = format_ast(src);
assert_eq!(out, "f = do\n let x = 1\n y = 2\n pure (x + y)\n");
assert_eq!(format_ast(&out), out); }
#[test]
fn do_block_with_try_is_reindented() {
let src = "f = do\n _ <- try foo catch _ -> bar\n pure ()\n";
let out = format_ast(src);
assert_eq!(out, "f = do\n _ <- try foo catch _ -> bar\n pure ()\n");
assert_eq!(format_ast(&out), out);
}
#[test]
fn if_then_else_reindented_to_if_col_plus_two() {
let src = "f x =\n if x > 0\n then 1\n else 2\n";
let out = format_ast(src);
assert_eq!(out, "f x =\n if x > 0\n then 1\n else 2\n");
assert_eq!(format_ast(&out), out); }
#[test]
fn if_then_else_already_aligned_is_a_fixpoint() {
let src = "f x =\n if x > 0\n then 1\n else 2\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn single_line_if_is_untouched() {
let src = "g x = if x then 1 else 2\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn do_then_if_passes_reach_a_single_call_fixpoint() {
let src = "f =\n if c\n then do\n a\n b\n else d\n";
let once = format_ast(src);
let twice = format_ast(&once);
assert_eq!(once, twice, "single-call output must be a fixpoint");
}
#[test]
fn if_then_else_multiline_branch_rides_uniform_shift() {
let src = "f x =\n if x > 0\n then g\n a\n else h\n";
let out = format_ast(src);
assert_eq!(
out,
"f x =\n if x > 0\n then g\n a\n else h\n"
);
assert_eq!(format_ast(&out), out); }
#[test]
fn case_alts_reindented_to_case_indent_plus_two() {
let src = "f x = case x of\n None -> 1\n Some y -> y\n";
let out = format_ast(src);
assert_eq!(out, "f x = case x of\n None -> 1\n Some y -> y\n");
assert_eq!(format_ast(&out), out); }
#[test]
fn case_alts_already_aligned_is_a_fixpoint() {
let src = "f x = case x of\n None -> 1\n Some y -> y\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn inline_case_is_untouched() {
let src = "f x = case x of None -> 1; Some y -> y\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn nested_case_rides_outer_shift() {
let src = "f x = case x of\n A -> case y of\n P -> 1\n Q -> 2\n B -> 0\n";
let out = format_ast(src);
let want =
"f x = case x of\n A -> case y of\n P -> 1\n Q -> 2\n B -> 0\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out); }
#[test]
fn letin_bindings_reindented_to_let_indent_plus_two() {
let src = "f =\n let\n x = 1\n y = 2\n in x + y\n";
let out = format_ast(src);
assert_eq!(out, "f =\n let\n x = 1\n y = 2\n in x + y\n");
assert_eq!(format_ast(&out), out); }
#[test]
fn letin_already_aligned_is_a_fixpoint() {
let src = "f =\n let\n x = 1\n y = 2\n in x + y\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn inline_letin_is_untouched() {
let src = "f = let x = 1 in x\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn con_with_fields_reindented_to_indent_plus_two() {
let src = "f = create Asset with\n issuer = a\n owner = b\n";
let out = format_ast(src);
assert_eq!(out, "f = create Asset with\n issuer = a\n owner = b\n");
assert_eq!(format_ast(&out), out); }
#[test]
fn record_update_fields_are_reindented() {
let src = "f this p = this with\n owner = p\n";
let out = format_ast(src);
assert_eq!(out, "f this p = this with\n owner = p\n");
assert_eq!(format_ast(&out), out);
}
#[test]
fn split_with_on_own_line_stays_verbatim() {
let src = "f = WithField\n with\n f1 = 10\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn inline_con_with_is_untouched() {
let src = "f = Asset with issuer = a; owner = b\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn con_with_before_where_keeps_fields_inside_expression() {
let src = "module M where\nquery : T\nquery = lift $ QueryACS with\n parties = p\n tplId = t\n where\n convert = x\n";
let out = format_ast(src);
assert_eq!(
out,
"module M where\nquery: T\nquery = lift $ QueryACS with\n parties = p\n tplId = t\n where\n convert = x\n"
);
}
#[test]
fn template_four_space_ladder_canonicalized_to_two() {
let src = "template Coin\n with\n issuer : Party\n where\n signatory issuer\n choice Burn : ()\n controller issuer\n do pure ()\n";
let out = format_ast(src);
let want = "template Coin\n with\n issuer: Party\n where\n signatory issuer\n choice Burn: ()\n controller issuer\n do pure ()\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out); }
#[test]
fn interface_body_canonicalized_to_two() {
let src = "interface Asset where\n viewtype V\n getOwner : Party\n choice Xfer : ()\n controller getOwner this\n do pure ()\n";
let out = format_ast(src);
let want = "interface Asset where\n viewtype V\n getOwner: Party\n choice Xfer: ()\n controller getOwner this\n do pure ()\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out); }
#[test]
fn inline_with_template_keeps_fields_at_head_plus_four() {
let src = "template T with\n p: Party\n where\n signatory p\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn canonical_template_is_a_fixpoint() {
let src = "template Coin\n with\n issuer: Party\n where\n signatory issuer\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn under_indented_template_body_canonicalized() {
let src = "template Coin\n with\n issuer: Party\n where\n signatory issuer\n";
let out = format_ast(src);
assert_eq!(
out,
"template Coin\n with\n issuer: Party\n where\n signatory issuer\n"
);
assert_eq!(format_ast(&out), out); }
#[test]
fn mid_line_let_is_left_verbatim() {
let src = "f x = let\n a = 1\n b = 2\n in a + b\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn choice_internal_ladder_is_canonicalized() {
let src = "template T\n with\n p: Party\n where\n choice C\n : ()\n with\n arg: Text\n observer p\n controller p\n do\n pure ()\n";
let out = format_ast(src);
let want = "template T\n with\n p: Party\n where\n choice C\n : ()\n with\n arg: Text\n observer p\n controller p\n do\n pure ()\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn choice_keyword_scan_ignores_identifier_fragments() {
let src = "template T\n with\n p: Party\n where\n choice C\n : ()\n with\n observer_name: Party\n observer p\n controller p\n do\n pure ()\n";
let out = format_ast(src);
let want = "template T\n with\n p: Party\n where\n choice C\n : ()\n with\n observer_name: Party\n observer p\n controller p\n do\n pure ()\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn type_def_ladders_are_canonicalized() {
let src = "data Color = Grey\n | RGB\n with r: Int\n deriving (Eq, Show)\n\nexception E\n with\n msg: Text\n where\n message msg\n";
let out = format_ast(src);
let want = "data Color = Grey\n | RGB\n with r: Int\n deriving (Eq, Show)\n\nexception E\n with\n msg: Text\n where\n message msg\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn data_record_with_ladder_keeps_with_above_fields() {
let src =
"data ReceiverAmount = ReceiverAmount\n with\n receiver : Party\n amount : Decimal\n";
let out = format_ast(src);
let want =
"data ReceiverAmount = ReceiverAmount\n with\n receiver: Party\n amount: Decimal\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn inline_data_record_with_braces_keeps_body_column() {
let src = "data Data = Data with\n { dummy : ()\n , srcLoc : SrcLoc\n }\n";
assert_eq!(format_ast(src), src);
}
#[test]
fn class_where_body_with_comments_keeps_body_indent() {
let src = "class ActionState s m | m -> s where\n {-# MINIMAL get, (put | modify) #-}\n -- | Fetch the current value.\n get : m s\n\n -- | Set the value.\n put : s -> m ()\n put = modify . const\n";
let out = format_ast(src);
let want = "class ActionState s m | m -> s where\n {-# MINIMAL get, (put | modify) #-}\n -- | Fetch the current value.\n get: m s\n\n -- | Set the value.\n put: s -> m ()\n put = modify . const\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn class_where_body_with_indented_pragma_keeps_pragma_indent() {
let src = "class Foo t where\n {-# MINIMAL foo1 | foo2 #-}\n\n foo1 : t -> Int\n foo1 x = foo1 x + 1\n";
let out = format_ast(src);
let want = "class Foo t where\n {-# MINIMAL foo1 | foo2 #-}\n\n foo1: t -> Int\n foo1 x = foo1 x + 1\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn guards_and_where_bindings_are_canonicalized() {
let src =
"f x\n | x > 0 = g\n x\n | otherwise = 0\n where\n g y = y\n";
let out = format_ast(src);
let want = "f x\n | x > 0 = g\n x\n | otherwise = 0\n where\n g y = y\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn multiline_try_catch_is_canonicalized() {
let src = "f = try\n foo\n catch\n _ -> bar\n";
let out = format_ast(src);
let want = "f = try\n foo\n catch\n _ -> bar\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn explicit_list_continuations_are_canonicalized() {
let src = "x = [ 1\n , 2\n , 3 ]\n";
let out = format_ast(src);
let want = "x = [ 1\n , 2\n , 3 ]\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn module_and_import_continuations_are_canonicalized() {
let src = "module M\n ( f\n , g\n ) where\n\nimport DA.Map\n ( Map\n )\n";
let out = format_ast(src);
let want = "module M\n ( f\n , g\n ) where\n\nimport DA.Map\n ( Map\n )\n";
assert_eq!(out, want);
assert_eq!(format_ast(&out), out);
}
#[test]
fn duplicate_space_after_colon_collapsed() {
let src = "module M where\nfoo: Int -> Int\nfoo x = x\n";
let out = format_ast(src);
assert_eq!(out, "module M where\nfoo: Int -> Int\nfoo x = x\n");
assert_eq!(format_ast(&out), out); }
#[test]
fn space_around_colon_canonicalized_both_sides() {
let src = "module M where\nfoo : Int\nfoo = 1\n";
assert_eq!(format_ast(src), "module M where\nfoo: Int\nfoo = 1\n");
}
#[test]
fn after_colon_collapse_skips_braces_and_parens() {
let braced = "module M where\nx = { a : Int }\n";
assert_eq!(format_ast(braced), braced);
let parened = "module M where\nf (n : Int) = n\n";
assert_eq!(format_ast(parened), parened);
}
#[test]
fn crlf_final_newline_not_mixed() {
let src = "module M where\r\nx = 1 \r\n";
let out = format_ast(src);
assert!(out.ends_with("\r\n"), "got: {:?}", out);
assert!(!out.ends_with("\n\n"));
assert_eq!(format_ast(&out), out); }
}