Skip to main content

aver/main/
format_cmd.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process;
5
6use aver::ast::TopLevel;
7use aver::lexer::Lexer;
8use aver::parser::Parser;
9use aver::types::{Type, parse_type_str_strict};
10use colored::Colorize;
11
12#[allow(dead_code)]
13pub(super) fn cmd_format(path: &str, check: bool) {
14    let root = Path::new(path);
15    let mut files = Vec::new();
16    if let Err(e) = collect_av_files(root, &mut files) {
17        eprintln!("{}", e.red());
18        process::exit(1);
19    }
20    files.sort();
21
22    if files.is_empty() {
23        eprintln!(
24            "{}",
25            format!("No .av files found under '{}'", root.display()).red()
26        );
27        process::exit(1);
28    }
29
30    let mut changed = Vec::new();
31    for file in &files {
32        let src = match fs::read_to_string(file) {
33            Ok(s) => s,
34            Err(e) => {
35                eprintln!(
36                    "{}",
37                    format!("Cannot read '{}': {}", file.display(), e).red()
38                );
39                process::exit(1);
40            }
41        };
42        let formatted = match try_format_source(&src) {
43            Ok(s) => s,
44            Err(e) => {
45                eprintln!(
46                    "{}",
47                    format!("Cannot format '{}': {}", file.display(), e).red()
48                );
49                process::exit(1);
50            }
51        };
52        if formatted != src {
53            changed.push(file.clone());
54            if !check && let Err(e) = fs::write(file, formatted) {
55                eprintln!(
56                    "{}",
57                    format!("Cannot write '{}': {}", file.display(), e).red()
58                );
59                process::exit(1);
60            }
61        }
62    }
63
64    if check {
65        if changed.is_empty() {
66            println!("{}", "Format check passed".green());
67            return;
68        }
69        println!("{}", "Format check failed".red());
70        println!("Files that need formatting:");
71        for f in &changed {
72            println!("  {}", f.display());
73        }
74        process::exit(1);
75    }
76
77    if changed.is_empty() {
78        println!("{}", "Already formatted".green());
79    } else {
80        for f in &changed {
81            println!("{} {}", "formatted".green(), f.display());
82        }
83        println!("{}", format!("Formatted {} file(s)", changed.len()).green());
84    }
85}
86
87#[allow(dead_code)]
88fn collect_av_files(path: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> {
89    if !path.exists() {
90        return Err(format!("Path '{}' does not exist", path.display()));
91    }
92
93    if path.is_file() {
94        if is_av_file(path) {
95            out.push(path.to_path_buf());
96            return Ok(());
97        }
98        return Err(format!("'{}' is not an .av file", path.display()));
99    }
100
101    let entries = fs::read_dir(path)
102        .map_err(|e| format!("Cannot read directory '{}': {}", path.display(), e))?;
103    for entry_res in entries {
104        let entry = entry_res
105            .map_err(|e| format!("Cannot read directory entry in '{}': {}", path.display(), e))?;
106        let p = entry.path();
107        if p.is_dir() {
108            collect_av_files(&p, out)?;
109        } else if is_av_file(&p) {
110            out.push(p);
111        }
112    }
113    Ok(())
114}
115
116#[allow(dead_code)]
117fn is_av_file(path: &Path) -> bool {
118    path.extension().and_then(|e| e.to_str()) == Some("av")
119}
120
121fn normalize_leading_indent(line: &str) -> String {
122    let mut end = 0usize;
123    for (idx, ch) in line.char_indices() {
124        if ch == ' ' || ch == '\t' {
125            end = idx + ch.len_utf8();
126        } else {
127            break;
128        }
129    }
130
131    let (indent, rest) = line.split_at(end);
132    if rest.is_empty() {
133        return String::new();
134    }
135
136    let mut out = String::new();
137    for ch in indent.chars() {
138        if ch == '\t' {
139            out.push_str("    ");
140        } else {
141            out.push(ch);
142        }
143    }
144    out.push_str(rest);
145    out
146}
147
148fn effect_namespace(effect: &str) -> &str {
149    match effect.split_once('.') {
150        Some((namespace, _)) => namespace,
151        None => effect,
152    }
153}
154
155fn sorted_effects(effects: &[String]) -> Vec<String> {
156    let mut sorted = effects.to_vec();
157    sorted.sort();
158    sorted
159}
160
161fn format_block_effect_declaration(indent: &str, effects: &[String]) -> Vec<String> {
162    let effects = sorted_effects(effects);
163    if effects.len() <= 4 {
164        return vec![format!("{}! [{}]", indent, effects.join(", "))];
165    }
166
167    let mut out = vec![format!("{}! [", indent)];
168    let mut start = 0usize;
169    while start < effects.len() {
170        let namespace = effect_namespace(&effects[start]);
171        let mut end = start + 1;
172        while end < effects.len() && effect_namespace(&effects[end]) == namespace {
173            end += 1;
174        }
175        out.push(format!("{}    {},", indent, effects[start..end].join(", ")));
176        start = end;
177    }
178    out.push(format!("{}]", indent));
179    out
180}
181
182fn split_top_level(src: &str, delimiter: char) -> Option<Vec<String>> {
183    let mut parts = Vec::new();
184    let mut start = 0usize;
185    let mut paren_depth = 0usize;
186    let mut bracket_depth = 0usize;
187    let mut angle_depth = 0usize;
188    let mut prev = None;
189
190    for (idx, ch) in src.char_indices() {
191        match ch {
192            '(' => paren_depth += 1,
193            ')' => paren_depth = paren_depth.checked_sub(1)?,
194            '[' => bracket_depth += 1,
195            ']' => bracket_depth = bracket_depth.checked_sub(1)?,
196            '<' => angle_depth += 1,
197            '>' if prev != Some('-') && angle_depth > 0 => angle_depth -= 1,
198            _ => {}
199        }
200
201        if ch == delimiter && paren_depth == 0 && bracket_depth == 0 && angle_depth == 0 {
202            parts.push(src[start..idx].to_string());
203            start = idx + ch.len_utf8();
204        }
205        prev = Some(ch);
206    }
207
208    if paren_depth != 0 || bracket_depth != 0 || angle_depth != 0 {
209        return None;
210    }
211
212    parts.push(src[start..].to_string());
213    Some(parts)
214}
215
216fn find_matching_paren(src: &str, open_idx: usize) -> Option<usize> {
217    let mut depth = 0usize;
218    for (idx, ch) in src.char_indices().skip_while(|(idx, _)| *idx < open_idx) {
219        match ch {
220            '(' => depth += 1,
221            ')' => {
222                depth = depth.checked_sub(1)?;
223                if depth == 0 {
224                    return Some(idx);
225                }
226            }
227            _ => {}
228        }
229    }
230    None
231}
232
233fn format_type_for_source(ty: &Type) -> String {
234    match ty {
235        Type::Int => "Int".to_string(),
236        Type::Float => "Float".to_string(),
237        Type::Str => "String".to_string(),
238        Type::Bool => "Bool".to_string(),
239        Type::Unit => "Unit".to_string(),
240        Type::Result(ok, err) => format!(
241            "Result<{}, {}>",
242            format_type_for_source(ok),
243            format_type_for_source(err)
244        ),
245        Type::Option(inner) => format!("Option<{}>", format_type_for_source(inner)),
246        Type::List(inner) => format!("List<{}>", format_type_for_source(inner)),
247        Type::Tuple(items) => format!(
248            "({})",
249            items
250                .iter()
251                .map(format_type_for_source)
252                .collect::<Vec<_>>()
253                .join(", ")
254        ),
255        Type::Map(key, value) => format!(
256            "Map<{}, {}>",
257            format_type_for_source(key),
258            format_type_for_source(value)
259        ),
260        Type::Fn(params, ret, effects) => {
261            let params = params
262                .iter()
263                .map(format_type_for_source)
264                .collect::<Vec<_>>()
265                .join(", ");
266            let ret = format_type_for_source(ret);
267            let effects = sorted_effects(effects);
268            if effects.is_empty() {
269                format!("Fn({params}) -> {ret}")
270            } else {
271                format!("Fn({params}) -> {ret} ! [{}]", effects.join(", "))
272            }
273        }
274        Type::Unknown => "Unknown".to_string(),
275        Type::Named(name) => name.clone(),
276    }
277}
278
279fn normalize_type_annotation(type_src: &str) -> String {
280    let trimmed = type_src.trim();
281    match parse_type_str_strict(trimmed) {
282        Ok(ty) => format_type_for_source(&ty),
283        Err(_) => trimmed.to_string(),
284    }
285}
286
287fn normalize_function_header_effects_line(line: &str) -> String {
288    let indent_len = line.chars().take_while(|c| *c == ' ').count();
289    let indent = " ".repeat(indent_len);
290    let trimmed = line.trim();
291    if !trimmed.starts_with("fn ") {
292        return line.to_string();
293    }
294
295    let open_idx = match trimmed.find('(') {
296        Some(idx) => idx,
297        None => return line.to_string(),
298    };
299    let close_idx = match find_matching_paren(trimmed, open_idx) {
300        Some(idx) => idx,
301        None => return line.to_string(),
302    };
303
304    let params_src = &trimmed[open_idx + 1..close_idx];
305    let params = match split_top_level(params_src, ',') {
306        Some(parts) => parts,
307        None => return line.to_string(),
308    };
309    let formatted_params = params
310        .into_iter()
311        .filter(|part| !part.trim().is_empty())
312        .map(|param| {
313            let (name, ty) = match param.split_once(':') {
314                Some(parts) => parts,
315                None => return param.trim().to_string(),
316            };
317            format!("{}: {}", name.trim(), normalize_type_annotation(ty))
318        })
319        .collect::<Vec<_>>()
320        .join(", ");
321
322    let mut formatted = format!(
323        "{}{}{})",
324        indent,
325        &trimmed[..open_idx + 1],
326        formatted_params
327    );
328    let remainder = trimmed[close_idx + 1..].trim();
329    if let Some(return_type) = remainder.strip_prefix("->") {
330        formatted.push_str(" -> ");
331        formatted.push_str(&normalize_type_annotation(return_type));
332    } else if !remainder.is_empty() {
333        formatted.push(' ');
334        formatted.push_str(remainder);
335    }
336
337    formatted
338}
339
340fn normalize_function_header_effects(lines: Vec<String>) -> Vec<String> {
341    lines
342        .into_iter()
343        .map(|line| normalize_function_header_effects_line(&line))
344        .collect()
345}
346
347fn normalize_effect_declaration_blocks(lines: Vec<String>) -> Vec<String> {
348    let mut out = Vec::with_capacity(lines.len());
349    let mut i = 0usize;
350
351    while i < lines.len() {
352        let line = &lines[i];
353        let trimmed = line.trim();
354        if !trimmed.starts_with("! [") {
355            out.push(line.clone());
356            i += 1;
357            continue;
358        }
359
360        let indent_len = line.chars().take_while(|c| *c == ' ').count();
361        let indent = " ".repeat(indent_len);
362        let mut inner = String::new();
363        let mut consumed = 0usize;
364        let mut found_close = false;
365
366        while i + consumed < lines.len() {
367            let current = &lines[i + consumed];
368            let current_trimmed = current.trim();
369            let segment = if consumed == 0 {
370                current_trimmed.trim_start_matches("! [")
371            } else {
372                current_trimmed
373            };
374
375            if let Some(before_close) = segment.strip_suffix(']') {
376                if !inner.is_empty() && !before_close.trim().is_empty() {
377                    inner.push(' ');
378                }
379                inner.push_str(before_close.trim());
380                found_close = true;
381                consumed += 1;
382                break;
383            }
384
385            if !inner.is_empty() && !segment.trim().is_empty() {
386                inner.push(' ');
387            }
388            inner.push_str(segment.trim());
389            consumed += 1;
390        }
391
392        if !found_close {
393            out.push(line.clone());
394            i += 1;
395            continue;
396        }
397
398        let effects: Vec<String> = if inner.trim().is_empty() {
399            vec![]
400        } else {
401            inner
402                .split(',')
403                .map(str::trim)
404                .filter(|part| !part.is_empty())
405                .map(ToString::to_string)
406                .collect()
407        };
408
409        out.extend(format_block_effect_declaration(&indent, &effects));
410        i += consumed;
411    }
412
413    out
414}
415
416#[derive(Clone, Debug, PartialEq, Eq)]
417enum BlockKind {
418    Fn(String),
419    Verify(String),
420    Other,
421}
422
423#[derive(Clone, Debug, PartialEq, Eq)]
424struct TopBlock {
425    text: String,
426    kind: BlockKind,
427    start_line: usize,
428}
429
430#[derive(Default)]
431struct FormatAstInfo {
432    kind_by_line: HashMap<usize, BlockKind>,
433}
434
435fn classify_block(header_line: &str) -> BlockKind {
436    let trimmed = header_line.trim();
437    if let Some(rest) = trimmed.strip_prefix("fn ") {
438        let name = rest
439            .split(['(', ' ', '\t'])
440            .next()
441            .unwrap_or_default()
442            .to_string();
443        if !name.is_empty() {
444            return BlockKind::Fn(name);
445        }
446    }
447    if let Some(rest) = trimmed.strip_prefix("verify ") {
448        let name = rest
449            .split([' ', '\t'])
450            .next()
451            .unwrap_or_default()
452            .to_string();
453        if !name.is_empty() {
454            return BlockKind::Verify(name);
455        }
456    }
457    BlockKind::Other
458}
459
460fn is_top_level_start(line: &str) -> bool {
461    if line.is_empty() {
462        return false;
463    }
464    if line.starts_with(' ') || line.starts_with('\t') {
465        return false;
466    }
467    !line.trim_start().starts_with("//")
468}
469
470fn split_top_level_blocks(lines: &[String], ast_info: Option<&FormatAstInfo>) -> Vec<TopBlock> {
471    if lines.is_empty() {
472        return Vec::new();
473    }
474
475    let starts: Vec<usize> = lines
476        .iter()
477        .enumerate()
478        .filter_map(|(idx, line)| is_top_level_start(line).then_some(idx))
479        .collect();
480
481    if starts.is_empty() {
482        let text = lines.join("\n").trim_end_matches('\n').to_string();
483        if text.is_empty() {
484            return Vec::new();
485        }
486        return vec![TopBlock {
487            text,
488            kind: BlockKind::Other,
489            start_line: 1,
490        }];
491    }
492
493    let mut blocks = Vec::new();
494
495    // Preserve preamble comments/metadata before first top-level declaration.
496    let first = starts[0];
497    if first > 0 {
498        let mut pre = lines[..first].to_vec();
499        while pre.last().is_some_and(|l| l.is_empty()) {
500            pre.pop();
501        }
502        if !pre.is_empty() {
503            blocks.push(TopBlock {
504                text: pre.join("\n"),
505                kind: BlockKind::Other,
506                start_line: 1,
507            });
508        }
509    }
510
511    for (i, start) in starts.iter().enumerate() {
512        let end = starts.get(i + 1).copied().unwrap_or(lines.len());
513        let mut segment = lines[*start..end].to_vec();
514        while segment.last().is_some_and(|l| l.is_empty()) {
515            segment.pop();
516        }
517        if segment.is_empty() {
518            continue;
519        }
520        let header = segment[0].clone();
521        let start_line = *start + 1;
522        let kind = ast_info
523            .and_then(|info| info.kind_by_line.get(&start_line).cloned())
524            .unwrap_or_else(|| classify_block(&header));
525        blocks.push(TopBlock {
526            text: segment.join("\n"),
527            kind,
528            start_line,
529        });
530    }
531
532    blocks
533}
534
535fn reorder_verify_blocks(blocks: Vec<TopBlock>) -> Vec<TopBlock> {
536    let verify_blocks: Vec<TopBlock> = blocks
537        .iter()
538        .filter(|b| matches!(b.kind, BlockKind::Verify(_)))
539        .cloned()
540        .collect();
541
542    if verify_blocks.is_empty() {
543        return blocks;
544    }
545
546    let mut by_fn: HashMap<String, Vec<usize>> = HashMap::new();
547    for (idx, block) in verify_blocks.iter().enumerate() {
548        if let BlockKind::Verify(name) = &block.kind {
549            by_fn.entry(name.clone()).or_default().push(idx);
550        }
551    }
552
553    let mut used = vec![false; verify_blocks.len()];
554    let mut out = Vec::new();
555
556    for block in blocks {
557        match block.kind.clone() {
558            BlockKind::Verify(_) => {}
559            BlockKind::Fn(name) => {
560                out.push(block);
561                if let Some(indices) = by_fn.remove(&name) {
562                    for idx in indices {
563                        used[idx] = true;
564                        out.push(verify_blocks[idx].clone());
565                    }
566                }
567            }
568            BlockKind::Other => out.push(block),
569        }
570    }
571
572    for (idx, block) in verify_blocks.iter().enumerate() {
573        if !used[idx] {
574            out.push(block.clone());
575        }
576    }
577
578    out
579}
580
581fn parse_ast_info_checked(source: &str) -> Result<FormatAstInfo, String> {
582    let mut lexer = Lexer::new(source);
583    let tokens = lexer.tokenize().map_err(|e| e.to_string())?;
584    let mut parser = Parser::new(tokens);
585    let items = parser.parse().map_err(|e| e.to_string())?;
586
587    let mut info = FormatAstInfo::default();
588    for item in items {
589        match item {
590            TopLevel::FnDef(fd) => {
591                info.kind_by_line
592                    .insert(fd.line, BlockKind::Fn(fd.name.clone()));
593            }
594            TopLevel::Verify(vb) => {
595                info.kind_by_line
596                    .insert(vb.line, BlockKind::Verify(vb.fn_name.clone()));
597            }
598            _ => {}
599        }
600    }
601    Ok(info)
602}
603
604fn normalize_source_lines(source: &str) -> Vec<String> {
605    let normalized = source.replace("\r\n", "\n").replace('\r', "\n");
606
607    let mut lines = Vec::new();
608    for raw in normalized.split('\n') {
609        let trimmed = raw.trim_end_matches([' ', '\t']);
610        let line = normalize_leading_indent(trimmed);
611        lines.push(line);
612    }
613
614    let lines = normalize_effect_declaration_blocks(lines);
615    let lines = normalize_function_header_effects(lines);
616    let lines = normalize_module_intent_blocks(lines);
617    normalize_inline_decision_fields(lines)
618}
619
620fn normalize_module_intent_blocks(lines: Vec<String>) -> Vec<String> {
621    let mut out = Vec::with_capacity(lines.len());
622    let mut in_module_header = false;
623    let mut i = 0usize;
624
625    while i < lines.len() {
626        let line = &lines[i];
627        let trimmed = line.trim();
628        let indent = line.chars().take_while(|c| *c == ' ').count();
629
630        if indent == 0 && trimmed.starts_with("module ") {
631            in_module_header = true;
632            out.push(line.clone());
633            i += 1;
634            continue;
635        }
636
637        if in_module_header && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
638            in_module_header = false;
639        }
640
641        if in_module_header && indent > 0 {
642            let head = &line[indent..];
643            if let Some(rhs) = head.strip_prefix("intent =") {
644                let rhs_trimmed = rhs.trim_start();
645                if rhs_trimmed.starts_with('"') {
646                    let mut parts = vec![rhs_trimmed.to_string()];
647                    let mut consumed = 1usize;
648
649                    while i + consumed < lines.len() {
650                        let next = &lines[i + consumed];
651                        let next_indent = next.chars().take_while(|c| *c == ' ').count();
652                        let next_trimmed = next.trim();
653
654                        if next_indent <= indent || next_trimmed.is_empty() {
655                            break;
656                        }
657                        if !next_trimmed.starts_with('"') {
658                            break;
659                        }
660
661                        parts.push(next_trimmed.to_string());
662                        consumed += 1;
663                    }
664
665                    if parts.len() > 1 {
666                        out.push(format!("{}intent =", " ".repeat(indent)));
667                        for part in parts {
668                            out.push(format!("{}{}", " ".repeat(indent + 4), part));
669                        }
670                        i += consumed;
671                        continue;
672                    }
673                }
674            }
675        }
676
677        out.push(line.clone());
678        i += 1;
679    }
680
681    out
682}
683
684fn normalize_internal_blank_runs(text: &str) -> String {
685    let mut out = Vec::new();
686    let mut blank_run = 0usize;
687    for raw in text.split('\n') {
688        if raw.is_empty() {
689            blank_run += 1;
690            if blank_run <= 2 {
691                out.push(String::new());
692            }
693        } else {
694            blank_run = 0;
695            out.push(raw.to_string());
696        }
697    }
698    while out.first().is_some_and(|l| l.is_empty()) {
699        out.remove(0);
700    }
701    while out.last().is_some_and(|l| l.is_empty()) {
702        out.pop();
703    }
704    out.join("\n")
705}
706
707const DECISION_FIELDS: [&str; 6] = ["date", "author", "reason", "chosen", "rejected", "impacts"];
708
709fn starts_with_decision_field(content: &str) -> bool {
710    DECISION_FIELDS
711        .iter()
712        .any(|field| content.starts_with(&format!("{field} =")))
713}
714
715fn find_next_decision_field_boundary(s: &str) -> Option<usize> {
716    let mut best: Option<usize> = None;
717    for field in DECISION_FIELDS {
718        let needle = format!(" {field} =");
719        let mut search_from = 0usize;
720        while let Some(rel) = s[search_from..].find(&needle) {
721            let idx = search_from + rel;
722            // Require at least two spaces before the next field marker, so
723            // normal single-space tokens don't split accidentally.
724            let spaces_before = s[..idx].chars().rev().take_while(|c| *c == ' ').count();
725            // `needle` starts at one of the separating spaces, so include it.
726            let total_separator_spaces = spaces_before + 1;
727            if total_separator_spaces >= 2 {
728                let field_start = idx + 1;
729                best = Some(best.map_or(field_start, |cur| cur.min(field_start)));
730                break;
731            }
732            search_from = idx + 1;
733        }
734    }
735    best
736}
737
738fn split_inline_decision_fields(content: &str) -> Vec<String> {
739    if !starts_with_decision_field(content) {
740        return vec![content.to_string()];
741    }
742    let mut out = Vec::new();
743    let mut rest = content.trim_end().to_string();
744    while let Some(idx) = find_next_decision_field_boundary(&rest) {
745        let left = rest[..idx].trim_end().to_string();
746        if left.is_empty() {
747            break;
748        }
749        out.push(left);
750        rest = rest[idx..].trim_start().to_string();
751    }
752    if !rest.is_empty() {
753        out.push(rest.trim_end().to_string());
754    }
755    if out.is_empty() {
756        vec![content.to_string()]
757    } else {
758        out
759    }
760}
761
762fn normalize_inline_decision_fields(lines: Vec<String>) -> Vec<String> {
763    let mut out = Vec::with_capacity(lines.len());
764    let mut in_decision = false;
765
766    for line in lines {
767        let trimmed = line.trim();
768        let indent = line.chars().take_while(|c| *c == ' ').count();
769
770        if indent == 0 && trimmed.starts_with("decision ") {
771            in_decision = true;
772            out.push(line);
773            continue;
774        }
775
776        if in_decision && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with("//") {
777            in_decision = false;
778        }
779
780        if in_decision && trimmed.is_empty() {
781            continue;
782        }
783
784        if in_decision && indent > 0 {
785            let content = &line[indent..];
786            let parts = split_inline_decision_fields(content);
787            if parts.len() > 1 {
788                for part in parts {
789                    out.push(format!("{}{}", " ".repeat(indent), part));
790                }
791                continue;
792            }
793        }
794
795        out.push(line);
796    }
797
798    out
799}
800
801pub fn try_format_source(source: &str) -> Result<String, String> {
802    let lines = normalize_source_lines(source);
803    let normalized = lines.join("\n");
804    let ast_info = parse_ast_info_checked(&normalized)?;
805
806    // 3) Split into top-level blocks and co-locate verify blocks under their functions.
807    let blocks = split_top_level_blocks(&lines, Some(&ast_info));
808    let reordered = reorder_verify_blocks(blocks);
809
810    // 4) Rejoin with one blank line between top-level blocks.
811    let mut non_empty_blocks = Vec::new();
812    for block in reordered {
813        let text = normalize_internal_blank_runs(&block.text);
814        let text = text.trim_matches('\n').to_string();
815        if !text.is_empty() {
816            non_empty_blocks.push(text);
817        }
818    }
819
820    if non_empty_blocks.is_empty() {
821        return Ok("\n".to_string());
822    }
823    let mut out = non_empty_blocks.join("\n\n");
824    out.push('\n');
825    Ok(out)
826}
827
828#[cfg(test)]
829pub fn format_source(source: &str) -> String {
830    match try_format_source(source) {
831        Ok(formatted) => formatted,
832        Err(err) => panic!("format_source received invalid Aver source: {err}"),
833    }
834}
835
836#[cfg(test)]
837mod tests {
838    use super::{format_source, try_format_source};
839
840    #[test]
841    fn normalizes_line_endings_and_trailing_ws() {
842        let src = "module A\r\n    fn x() -> Int   \r\n        1\t \r\n";
843        let got = format_source(src);
844        assert_eq!(got, "module A\n    fn x() -> Int\n        1\n");
845    }
846
847    #[test]
848    fn converts_leading_tabs_only() {
849        let src = "\tfn x() -> String\n\t\t\"a\\tb\"\n";
850        let got = format_source(src);
851        assert_eq!(got, "    fn x() -> String\n        \"a\\tb\"\n");
852    }
853
854    #[test]
855    fn collapses_long_blank_runs() {
856        let src = "module A\n\n\n\nfn x() -> Int\n    1\n";
857        let got = format_source(src);
858        assert_eq!(got, "module A\n\nfn x() -> Int\n    1\n");
859    }
860
861    #[test]
862    fn keeps_single_final_newline() {
863        let src = "module A\nfn x() -> Int\n    1\n\n\n";
864        let got = format_source(src);
865        assert_eq!(got, "module A\n\nfn x() -> Int\n    1\n");
866    }
867
868    #[test]
869    fn rejects_removed_eq_expr_syntax() {
870        let src = "fn x() -> Int\n    = 1\n";
871        let err = try_format_source(src).expect_err("old '= expr' syntax should fail");
872        assert!(
873            err.contains("no longer use '= expr'"),
874            "unexpected error: {}",
875            err
876        );
877    }
878
879    #[test]
880    fn moves_verify_directly_under_function() {
881        let src = r#"module Demo
882
883fn a(x: Int) -> Int
884    x + 1
885
886fn b(x: Int) -> Int
887    x + 2
888
889verify a
890    a(1) => 2
891
892verify b
893    b(1) => 3
894"#;
895        let got = format_source(src);
896        assert_eq!(
897            got,
898            r#"module Demo
899
900fn a(x: Int) -> Int
901    x + 1
902
903verify a
904    a(1) => 2
905
906fn b(x: Int) -> Int
907    x + 2
908
909verify b
910    b(1) => 3
911"#
912        );
913    }
914
915    #[test]
916    fn leaves_orphan_verify_at_end() {
917        let src = r#"module Demo
918
919verify missing
920    missing(1) => 2
921"#;
922        let got = format_source(src);
923        assert_eq!(
924            got,
925            r#"module Demo
926
927verify missing
928    missing(1) => 2
929"#
930        );
931    }
932
933    #[test]
934    fn keeps_inline_module_intent_inline() {
935        let src = r#"module Demo
936    intent = "Inline intent."
937    exposes [x]
938fn x() -> Int
939    1
940"#;
941        let got = format_source(src);
942        assert_eq!(
943            got,
944            r#"module Demo
945    intent = "Inline intent."
946    exposes [x]
947
948fn x() -> Int
949    1
950"#
951        );
952    }
953
954    #[test]
955    fn expands_multiline_module_intent_to_block() {
956        let src = r#"module Demo
957    intent = "First line."
958        "Second line."
959    exposes [x]
960fn x() -> Int
961    1
962"#;
963        let got = format_source(src);
964        assert_eq!(
965            got,
966            r#"module Demo
967    intent =
968        "First line."
969        "Second line."
970    exposes [x]
971
972fn x() -> Int
973    1
974"#
975        );
976    }
977
978    #[test]
979    fn splits_inline_decision_fields_to_separate_lines() {
980        let src = r#"module Demo
981    intent = "x"
982    exposes [main]
983
984decision D
985    date = "2026-03-02"
986    chosen = "A"    rejected = ["B"]
987    impacts = [main]
988"#;
989        let got = format_source(src);
990        assert_eq!(
991            got,
992            r#"module Demo
993    intent = "x"
994    exposes [main]
995
996decision D
997    date = "2026-03-02"
998    chosen = "A"
999    rejected = ["B"]
1000    impacts = [main]
1001"#
1002        );
1003    }
1004
1005    #[test]
1006    fn keeps_inline_function_description_inline() {
1007        let src = r#"fn add(a: Int, b: Int) -> Int
1008    ? "Adds two numbers."
1009    a + b
1010"#;
1011        let got = format_source(src);
1012        assert_eq!(
1013            got,
1014            r#"fn add(a: Int, b: Int) -> Int
1015    ? "Adds two numbers."
1016    a + b
1017"#
1018        );
1019    }
1020
1021    #[test]
1022    fn keeps_short_effect_lists_inline() {
1023        let src = r#"fn apply(f: Fn(Int) -> Int ! [Console.warn, Console.print], x: Int) -> Int
1024    ! [Http.post, Console.print, Http.get, Console.warn]
1025    f(x)
1026"#;
1027        let got = format_source(src);
1028        assert_eq!(
1029            got,
1030            r#"fn apply(f: Fn(Int) -> Int ! [Console.print, Console.warn], x: Int) -> Int
1031    ! [Console.print, Console.warn, Http.get, Http.post]
1032    f(x)
1033"#
1034        );
1035    }
1036
1037    #[test]
1038    fn expands_long_effect_lists_to_multiline_alphabetical_groups() {
1039        let src = r#"fn main() -> Unit
1040    ! [Args.get, Console.print, Console.warn, Time.now, Disk.makeDir, Disk.exists, Disk.readText, Disk.writeText, Disk.appendText]
1041    Unit
1042"#;
1043        let got = format_source(src);
1044        assert_eq!(
1045            got,
1046            r#"fn main() -> Unit
1047    ! [
1048        Args.get,
1049        Console.print, Console.warn,
1050        Disk.appendText, Disk.exists, Disk.makeDir, Disk.readText, Disk.writeText,
1051        Time.now,
1052    ]
1053    Unit
1054"#
1055        );
1056    }
1057
1058    #[test]
1059    fn sorts_function_type_effects_inline() {
1060        let src = r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Unit
1061    handler(value)
1062"#;
1063        let got = format_source(src);
1064        assert_eq!(
1065            got,
1066            r#"fn useHandler(handler: Fn(Int) -> Result<String, String> ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Unit
1067    handler(value)
1068"#
1069        );
1070    }
1071
1072    #[test]
1073    fn keeps_long_function_type_effects_inline() {
1074        let src = r#"fn apply(handler: Fn(Int) -> Int ! [Time.now, Args.get, Console.warn, Console.print, Disk.readText], value: Int) -> Int
1075    handler(value)
1076"#;
1077        let got = format_source(src);
1078        assert_eq!(
1079            got,
1080            r#"fn apply(handler: Fn(Int) -> Int ! [Args.get, Console.print, Console.warn, Disk.readText, Time.now], value: Int) -> Int
1081    handler(value)
1082"#
1083        );
1084    }
1085}