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