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 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 let spaces_before = s[..idx].chars().rev().take_while(|c| *c == ' ').count();
727 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 let blocks = split_top_level_blocks(&lines, Some(&ast_info));
810 let reordered = reorder_verify_blocks(blocks);
811
812 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}