Fork of daniellemaywood.uk/gleam — Wasm codegen work
120 kB
3338 lines
1#[cfg(test)]
2mod tests;
3
4use crate::{
5 Error, Result,
6 ast::{
7 CustomType, Import, ModuleConstant, TypeAlias, TypeAstConstructor, TypeAstFn, TypeAstHole,
8 TypeAstTuple, TypeAstVar, *,
9 },
10 build::Target,
11 docvec,
12 io::Utf8Writer,
13 parse::SpannedString,
14 parse::extra::{Comment, ModuleExtra},
15 pretty::{self, *},
16 warning::WarningEmitter,
17};
18use ecow::{EcoString, eco_format};
19use itertools::Itertools;
20use std::cmp::Ordering;
21use vec1::Vec1;
22
23use crate::type_::Deprecation;
24use camino::Utf8Path;
25
26const INDENT: isize = 2;
27
28pub fn pretty(writer: &mut impl Utf8Writer, src: &EcoString, path: &Utf8Path) -> Result<()> {
29 let parsed = crate::parse::parse_module(path.to_owned(), src, &WarningEmitter::null())
30 .map_err(|error| Error::Parse {
31 path: path.to_path_buf(),
32 src: src.clone(),
33 error: Box::new(error),
34 })?;
35 let intermediate = Intermediate::from_extra(&parsed.extra, src);
36 Formatter::with_comments(&intermediate)
37 .module(&parsed.module)
38 .pretty_print(80, writer)
39}
40
41pub(crate) struct Intermediate<'a> {
42 comments: Vec<Comment<'a>>,
43 doc_comments: Vec<Comment<'a>>,
44 module_comments: Vec<Comment<'a>>,
45 empty_lines: &'a [u32],
46 new_lines: &'a [u32],
47 trailing_commas: &'a [u32],
48}
49
50impl<'a> Intermediate<'a> {
51 pub fn from_extra(extra: &'a ModuleExtra, src: &'a EcoString) -> Intermediate<'a> {
52 Intermediate {
53 comments: extra
54 .comments
55 .iter()
56 .map(|span| Comment::from((span, src)))
57 .collect(),
58 doc_comments: extra
59 .doc_comments
60 .iter()
61 .map(|span| Comment::from((span, src)))
62 .collect(),
63 empty_lines: &extra.empty_lines,
64 module_comments: extra
65 .module_comments
66 .iter()
67 .map(|span| Comment::from((span, src)))
68 .collect(),
69 new_lines: &extra.new_lines,
70 trailing_commas: &extra.trailing_commas,
71 }
72 }
73}
74
75#[derive(Debug)]
76enum FnCapturePosition {
77 RightHandSideOfPipe,
78 EverywhereElse,
79}
80
81#[derive(Debug)]
82/// One of the pieces making a record update arg list: it could be the starting
83/// record being updated, or one of the subsequent arguments.
84///
85enum RecordUpdatePiece<'a> {
86 Record(&'a RecordBeingUpdated),
87 Argument(&'a UntypedRecordUpdateArg),
88}
89
90impl HasLocation for RecordUpdatePiece<'_> {
91 fn location(&self) -> SrcSpan {
92 match self {
93 RecordUpdatePiece::Record(record) => record.location,
94 RecordUpdatePiece::Argument(arg) => arg.location,
95 }
96 }
97}
98
99/// Hayleigh's bane
100#[derive(Debug, Clone, Default)]
101pub struct Formatter<'a> {
102 comments: &'a [Comment<'a>],
103 doc_comments: &'a [Comment<'a>],
104 module_comments: &'a [Comment<'a>],
105 empty_lines: &'a [u32],
106 new_lines: &'a [u32],
107 trailing_commas: &'a [u32],
108}
109
110impl<'comments> Formatter<'comments> {
111 pub fn new() -> Self {
112 Default::default()
113 }
114
115 pub(crate) fn with_comments(extra: &'comments Intermediate<'comments>) -> Self {
116 Self {
117 comments: &extra.comments,
118 doc_comments: &extra.doc_comments,
119 module_comments: &extra.module_comments,
120 empty_lines: extra.empty_lines,
121 new_lines: extra.new_lines,
122 trailing_commas: extra.trailing_commas,
123 }
124 }
125
126 fn any_comments(&self, limit: u32) -> bool {
127 self.comments
128 .first()
129 .is_some_and(|comment| comment.start < limit)
130 }
131
132 fn any_empty_lines(&self, limit: u32) -> bool {
133 self.empty_lines.first().is_some_and(|line| *line < limit)
134 }
135
136 /// Pop comments that occur before a byte-index in the source, consuming
137 /// and retaining any empty lines contained within.
138 /// Returns an iterator of comments with their start position.
139 fn pop_comments_with_position(
140 &mut self,
141 limit: u32,
142 ) -> impl Iterator<Item = (u32, Option<&'comments str>)> + use<'comments> {
143 let (popped, rest, empty_lines) =
144 comments_before(self.comments, self.empty_lines, limit, true);
145 self.comments = rest;
146 self.empty_lines = empty_lines;
147 popped
148 }
149
150 /// Pop comments that occur before a byte-index in the source, consuming
151 /// and retaining any empty lines contained within.
152 fn pop_comments(
153 &mut self,
154 limit: u32,
155 ) -> impl Iterator<Item = Option<&'comments str>> + use<'comments> {
156 self.pop_comments_with_position(limit)
157 .map(|(_position, comment)| comment)
158 }
159
160 /// Pop doc comments that occur before a byte-index in the source, consuming
161 /// and dropping any empty lines contained within.
162 fn pop_doc_comments(
163 &mut self,
164 limit: u32,
165 ) -> impl Iterator<Item = Option<&'comments str>> + use<'comments> {
166 let (popped, rest, empty_lines) =
167 comments_before(self.doc_comments, self.empty_lines, limit, false);
168 self.doc_comments = rest;
169 self.empty_lines = empty_lines;
170 popped.map(|(_position, comment)| comment)
171 }
172
173 /// Remove between 0 and `limit` empty lines following the current position,
174 /// returning true if any empty lines were removed.
175 fn pop_empty_lines(&mut self, limit: u32) -> bool {
176 let mut end = 0;
177 for (i, &position) in self.empty_lines.iter().enumerate() {
178 if position > limit {
179 break;
180 }
181 end = i + 1;
182 }
183
184 self.empty_lines = self
185 .empty_lines
186 .get(end..)
187 .expect("Pop empty lines slicing");
188 end != 0
189 }
190
191 fn targeted_definition<'a>(&mut self, definition: &'a TargetedDefinition) -> Document<'a> {
192 let target = definition.target;
193 let definition = &definition.definition;
194 let start = definition.location().start;
195
196 let comments = self.pop_comments_with_position(start);
197 let comments = self.printed_documented_comments(comments);
198 let document = self.documented_definition(definition);
199 let document = match target {
200 None => document,
201 Some(Target::Erlang) => docvec!["@target(erlang)", line(), document],
202 Some(Target::JavaScript) => docvec!["@target(javascript)", line(), document],
203 };
204
205 comments.to_doc().append(document.group())
206 }
207
208 pub(crate) fn module<'a>(&mut self, module: &'a UntypedModule) -> Document<'a> {
209 let mut documents = vec![];
210 let mut previous_was_a_definition = false;
211
212 // Here we take consecutive groups of imports so that they can be sorted
213 // alphabetically.
214 for (is_import_group, definitions) in &module
215 .definitions
216 .iter()
217 .chunk_by(|definition| definition.definition.is_import())
218 {
219 if is_import_group {
220 if previous_was_a_definition {
221 documents.push(lines(2));
222 }
223 documents.append(&mut self.imports(definitions.collect_vec()));
224 previous_was_a_definition = false;
225 } else {
226 for definition in definitions {
227 if !documents.is_empty() {
228 documents.push(lines(2));
229 }
230 documents.push(self.targeted_definition(definition));
231 }
232 previous_was_a_definition = true;
233 }
234 }
235
236 let definitions = concat(documents);
237
238 // Now that definitions has been collected, only freestanding comments (//)
239 // and doc comments (///) remain. Freestanding comments aren't associated
240 // with any statement, and are moved to the bottom of the module.
241 let doc_comments = join(
242 self.doc_comments
243 .iter()
244 .map(|comment| "///".to_doc().append(EcoString::from(comment.content))),
245 line(),
246 );
247
248 let comments = match printed_comments(self.pop_comments(u32::MAX), false) {
249 Some(comments) => comments,
250 None => nil(),
251 };
252
253 let module_comments = if !self.module_comments.is_empty() {
254 let comments = self
255 .module_comments
256 .iter()
257 .map(|s| "////".to_doc().append(EcoString::from(s.content)));
258 join(comments, line()).append(line())
259 } else {
260 nil()
261 };
262
263 let non_empty = vec![module_comments, definitions, doc_comments, comments]
264 .into_iter()
265 .filter(|doc| !doc.is_empty());
266
267 join(non_empty, line()).append(line())
268 }
269
270 /// Separates the imports in groups delimited by comments or empty lines and
271 /// sorts each group alphabetically.
272 ///
273 /// The formatter needs to play nicely with import groups defined by the
274 /// programmer. If one puts a comment before an import then that's a clue
275 /// for the formatter that it has run into a gorup of related imports.
276 ///
277 /// So we can't just sort `imports` and format each one, we have to be a
278 /// bit smarter and see if each import is preceded by a comment.
279 /// Once we find a comment we know we're done with the current import
280 /// group and a new one has started.
281 ///
282 /// ```gleam
283 /// // This is an import group.
284 /// import gleam/int
285 /// import gleam/string
286 ///
287 /// // This marks the beginning of a new import group that can't
288 /// // be mushed together with the previous one!
289 /// import wibble
290 /// import wobble
291 /// ```
292 fn imports<'a>(&mut self, imports: Vec<&'a TargetedDefinition>) -> Vec<Document<'a>> {
293 let mut import_groups_docs = vec![];
294 let mut current_group = vec![];
295 let mut current_group_delimiter = nil();
296
297 for import in imports {
298 let start = import.definition.location().start;
299
300 // We need to start a new group if the `import` is preceded by one or
301 // more empty lines or a `//` comment.
302 let start_new_group = self.any_comments(start) || self.any_empty_lines(start);
303 if start_new_group {
304 // First we print the previous group and clear it out to start a
305 // new empty group containing the import we've just ran into.
306 if !current_group.is_empty() {
307 import_groups_docs.push(docvec![
308 current_group_delimiter,
309 self.sorted_import_group(¤t_group)
310 ]);
311 current_group.clear();
312 }
313
314 // Now that we've taken care of the previous group we can start
315 // the new one. We know it's preceded either by an empty line or
316 // some comments se we have to be a bit more precise and save the
317 // actual delimiter that we're going to put at the top of this
318 // group.
319
320 let comments = self.pop_comments(start);
321 let _ = self.pop_empty_lines(start);
322 current_group_delimiter = printed_comments(comments, true).unwrap_or(nil());
323 }
324 // Lastly we add the import to the group.
325 current_group.push(import);
326 }
327
328 // Let's not forget about the last import group!
329 if !current_group.is_empty() {
330 import_groups_docs.push(docvec![
331 current_group_delimiter,
332 self.sorted_import_group(¤t_group)
333 ]);
334 }
335
336 // We want all consecutive import groups to be separated by an empty line.
337 // This should really be `.intersperse(line())` but I can't do that
338 // because of https://github.com/rust-lang/rust/issues/48919.
339 Itertools::intersperse(import_groups_docs.into_iter(), lines(2)).collect_vec()
340 }
341
342 /// Prints the imports as a single sorted group of import statements.
343 ///
344 fn sorted_import_group<'a>(&mut self, imports: &[&'a TargetedDefinition]) -> Document<'a> {
345 let imports = imports
346 .iter()
347 .sorted_by(|one, other| match (&one.definition, &other.definition) {
348 (Definition::Import(one), Definition::Import(other)) => {
349 one.module.cmp(&other.module)
350 }
351 // It shouldn't really be possible for a non import to be here so
352 // we just return a default value.
353 _ => Ordering::Equal,
354 })
355 .map(|import| self.targeted_definition(import));
356
357 // This should really be `.intersperse(line())` but I can't do that
358 // because of https://github.com/rust-lang/rust/issues/48919.
359 Itertools::intersperse(imports, line())
360 .collect_vec()
361 .to_doc()
362 }
363
364 fn definition<'a>(&mut self, statement: &'a UntypedDefinition) -> Document<'a> {
365 match statement {
366 Definition::Function(function) => self.statement_fn(function),
367
368 Definition::TypeAlias(TypeAlias {
369 alias,
370 parameters: args,
371 type_ast: resolved_type,
372 publicity,
373 deprecation,
374 location,
375 ..
376 }) => self.type_alias(
377 *publicity,
378 alias,
379 args,
380 resolved_type,
381 deprecation,
382 location,
383 ),
384
385 Definition::CustomType(ct) => self.custom_type(ct),
386
387 Definition::Import(Import {
388 module,
389 as_name,
390 unqualified_values,
391 unqualified_types,
392 ..
393 }) => {
394 let second = if unqualified_values.is_empty() && unqualified_types.is_empty() {
395 nil()
396 } else {
397 let unqualified_types = unqualified_types
398 .iter()
399 .sorted_by(|a, b| a.name.cmp(&b.name))
400 .map(|type_| docvec!["type ", type_]);
401 let unqualified_values = unqualified_values
402 .iter()
403 .sorted_by(|a, b| a.name.cmp(&b.name))
404 .map(|value| value.to_doc());
405 let unqualified = join(
406 unqualified_types.chain(unqualified_values),
407 flex_break(",", ", "),
408 );
409 let unqualified = break_("", "")
410 .append(unqualified)
411 .nest(INDENT)
412 .append(break_(",", ""))
413 .group();
414 ".{".to_doc().append(unqualified).append("}")
415 };
416
417 let doc = docvec!["import ", module.as_str(), second];
418 let default_module_access_name = module.split('/').next_back().map(EcoString::from);
419 match (default_module_access_name, as_name) {
420 // If the `as name` is the same as the module name that would be
421 // used anyways we won't render it. For example:
422 // ```gleam
423 // import gleam/int as int
424 // ^^^^^^ this is redundant and removed
425 // ```
426 (Some(module_name), Some((AssignName::Variable(name), _)))
427 if &module_name == name =>
428 {
429 doc
430 }
431 (_, None) => doc,
432 (_, Some((AssignName::Variable(name) | AssignName::Discard(name), _))) => {
433 doc.append(" as ").append(name)
434 }
435 }
436 }
437
438 Definition::ModuleConstant(ModuleConstant {
439 publicity,
440 name,
441 annotation,
442 value,
443 ..
444 }) => {
445 let attributes = AttributesPrinter::new().set_internal(*publicity).to_doc();
446 let head = attributes
447 .append(pub_(*publicity))
448 .append("const ")
449 .append(name.as_str());
450 let head = match annotation {
451 None => head,
452 Some(t) => head.append(": ").append(self.type_ast(t)),
453 };
454 head.append(" = ").append(self.const_expr(value))
455 }
456 }
457 }
458
459 fn const_expr<'a, A, B>(&mut self, value: &'a Constant<A, B>) -> Document<'a> {
460 let comments = self.pop_comments(value.location().start);
461 let document = match value {
462 Constant::Int { value, .. } => self.int(value),
463
464 Constant::Float { value, .. } => self.float(value),
465
466 Constant::String { value, .. } => self.string(value),
467
468 Constant::List {
469 elements, location, ..
470 } => self.const_list(elements, location),
471
472 Constant::Tuple {
473 elements, location, ..
474 } => self.const_tuple(elements, location),
475
476 Constant::BitArray {
477 segments, location, ..
478 } => {
479 let segment_docs = segments
480 .iter()
481 .map(|segment| bit_array_segment(segment, |e| self.const_expr(e)))
482 .collect_vec();
483
484 self.bit_array(
485 segment_docs,
486 segments
487 .iter()
488 .all(|s| s.value.can_have_multiple_per_line()),
489 location,
490 )
491 }
492
493 Constant::Record {
494 name,
495 args,
496 module: None,
497 ..
498 } if args.is_empty() => name.to_doc(),
499
500 Constant::Record {
501 name,
502 args,
503 module: Some((m, _)),
504 ..
505 } if args.is_empty() => m.to_doc().append(".").append(name.as_str()),
506
507 Constant::Record {
508 name,
509 args,
510 module: None,
511 location,
512 ..
513 } => {
514 let args = args.iter().map(|a| self.constant_call_arg(a)).collect_vec();
515 name.to_doc()
516 .append(self.wrap_args(args, location.end))
517 .group()
518 }
519
520 Constant::Record {
521 name,
522 args,
523 module: Some((m, _)),
524 location,
525 ..
526 } => {
527 let args = args.iter().map(|a| self.constant_call_arg(a)).collect_vec();
528 m.to_doc()
529 .append(".")
530 .append(name.as_str())
531 .append(self.wrap_args(args, location.end))
532 .group()
533 }
534
535 Constant::Var {
536 name, module: None, ..
537 } => name.to_doc(),
538
539 Constant::Var {
540 name,
541 module: Some((module, _)),
542 ..
543 } => docvec![module, ".", name],
544
545 Constant::StringConcatenation { left, right, .. } => self
546 .const_expr(left)
547 .append(break_("", " ").append("<>".to_doc()))
548 .nest(INDENT)
549 .append(" ")
550 .append(self.const_expr(right)),
551
552 Constant::Invalid { .. } => {
553 panic!("invalid constants can not be in an untyped ast")
554 }
555 };
556 commented(document, comments)
557 }
558
559 fn const_list<'a, A, B>(
560 &mut self,
561 elements: &'a [Constant<A, B>],
562 location: &SrcSpan,
563 ) -> Document<'a> {
564 if elements.is_empty() {
565 // We take all comments that come _before_ the end of the list,
566 // that is all comments that are inside "[" and "]", if there's
567 // any comment we want to put it inside the empty list!
568 return match printed_comments(self.pop_comments(location.end), false) {
569 None => "[]".to_doc(),
570 Some(comments) => "["
571 .to_doc()
572 .append(break_("", "").nest(INDENT))
573 .append(comments)
574 .append(break_("", ""))
575 .append("]")
576 // vvv We want to make sure the comments are on a separate
577 // line from the opening and closing brackets so we
578 // force the breaks to be split on newlines.
579 .force_break(),
580 };
581 }
582
583 let list_packing = self.list_items_packing(
584 elements,
585 None,
586 |element| element.can_have_multiple_per_line(),
587 *location,
588 );
589 let comma = match list_packing {
590 ListItemsPacking::FitMultiplePerLine => flex_break(",", ", "),
591 ListItemsPacking::FitOnePerLine | ListItemsPacking::BreakOnePerLine => {
592 break_(",", ", ")
593 }
594 };
595
596 let mut elements_doc = nil();
597 for element in elements.iter() {
598 let empty_lines = self.pop_empty_lines(element.location().start);
599 let element_doc = self.const_expr(element);
600
601 elements_doc = if elements_doc.is_empty() {
602 element_doc
603 } else if empty_lines {
604 // If there's empty lines before the list item we want to add an
605 // empty line here. Notice how we're making sure no nesting is
606 // added after the comma, otherwise we would be adding needless
607 // whitespace in the empty line!
608 docvec![
609 elements_doc,
610 comma.clone().set_nesting(0),
611 line(),
612 element_doc
613 ]
614 } else {
615 docvec![elements_doc, comma.clone(), element_doc]
616 };
617 }
618 elements_doc = elements_doc.next_break_fits(NextBreakFitsMode::Disabled);
619
620 let doc = break_("[", "[").append(elements_doc).nest(INDENT);
621
622 // We get all remaining comments that come before the list's closing
623 // square bracket.
624 // If there's any we add those before the closing square bracket instead
625 // of moving those out of the list.
626 // Otherwise those would be moved out of the list.
627 let comments = self.pop_comments(location.end);
628 let doc = match printed_comments(comments, false) {
629 None => doc.append(break_(",", "")).append("]"),
630 Some(comment) => doc
631 .append(break_(",", "").nest(INDENT))
632 // ^ See how here we're adding the missing indentation to the
633 // final break so that the final comment is as indented as the
634 // list's items.
635 .append(comment)
636 .append(line())
637 .append("]")
638 .force_break(),
639 };
640
641 match list_packing {
642 ListItemsPacking::FitOnePerLine | ListItemsPacking::FitMultiplePerLine => doc.group(),
643 ListItemsPacking::BreakOnePerLine => doc.force_break(),
644 }
645 }
646
647 pub fn const_tuple<'a, A, B>(
648 &mut self,
649 elements: &'a [Constant<A, B>],
650 location: &SrcSpan,
651 ) -> Document<'a> {
652 if elements.is_empty() {
653 // We take all comments that come _before_ the end of the tuple,
654 // that is all comments that are inside "#(" and ")", if there's
655 // any comment we want to put it inside the empty list!
656 return match printed_comments(self.pop_comments(location.end), false) {
657 None => "#()".to_doc(),
658 Some(comments) => "#("
659 .to_doc()
660 .append(break_("", "").nest(INDENT))
661 .append(comments)
662 .append(break_("", ""))
663 .append(")")
664 // vvv We want to make sure the comments are on a separate
665 // line from the opening and closing parentheses so we
666 // force the breaks to be split on newlines.
667 .force_break(),
668 };
669 }
670
671 let args_docs = elements.iter().map(|element| self.const_expr(element));
672 let tuple_doc = break_("#(", "#(")
673 .append(join(args_docs, break_(",", ", ")).next_break_fits(NextBreakFitsMode::Disabled))
674 .nest(INDENT);
675
676 let comments = self.pop_comments(location.end);
677 match printed_comments(comments, false) {
678 None => tuple_doc.append(break_(",", "")).append(")").group(),
679 Some(comments) => tuple_doc
680 .append(break_(",", "").nest(INDENT))
681 .append(comments)
682 .append(line())
683 .append(")")
684 .force_break(),
685 }
686 }
687
688 fn documented_definition<'a>(&mut self, s: &'a UntypedDefinition) -> Document<'a> {
689 let comments = self.doc_comments(s.location().start);
690 comments.append(self.definition(s).group()).group()
691 }
692
693 fn doc_comments<'a>(&mut self, limit: u32) -> Document<'a> {
694 let mut comments = self.pop_doc_comments(limit).peekable();
695 match comments.peek() {
696 None => nil(),
697 Some(_) => join(
698 comments.map(|c| match c {
699 Some(c) => "///".to_doc().append(EcoString::from(c)),
700 None => unreachable!("empty lines dropped by pop_doc_comments"),
701 }),
702 line(),
703 )
704 .append(line())
705 .force_break(),
706 }
707 }
708
709 fn type_ast_constructor<'a>(
710 &mut self,
711 module: &'a Option<(EcoString, SrcSpan)>,
712 name: &'a str,
713 args: &'a [TypeAst],
714 location: &SrcSpan,
715 _name_location: &SrcSpan,
716 ) -> Document<'a> {
717 let head = module
718 .as_ref()
719 .map(|(qualifier, _)| qualifier.to_doc().append(".").append(name))
720 .unwrap_or_else(|| name.to_doc());
721
722 if args.is_empty() {
723 head
724 } else {
725 head.append(self.type_arguments(args, location))
726 }
727 }
728
729 fn type_ast<'a>(&mut self, t: &'a TypeAst) -> Document<'a> {
730 match t {
731 TypeAst::Hole(TypeAstHole { name, .. }) => name.to_doc(),
732
733 TypeAst::Constructor(TypeAstConstructor {
734 name,
735 arguments: args,
736 module,
737 location,
738 name_location,
739 }) => self.type_ast_constructor(module, name, args, location, name_location),
740
741 TypeAst::Fn(TypeAstFn {
742 arguments: args,
743 return_,
744 location,
745 }) => "fn"
746 .to_doc()
747 .append(self.type_arguments(args, location))
748 .group()
749 .append(" ->")
750 .append(break_("", " ").append(self.type_ast(return_)).nest(INDENT)),
751
752 TypeAst::Var(TypeAstVar { name, .. }) => name.to_doc(),
753
754 TypeAst::Tuple(TypeAstTuple { elements, location }) => {
755 "#".to_doc().append(self.type_arguments(elements, location))
756 }
757 }
758 .group()
759 }
760
761 fn type_arguments<'a>(&mut self, args: &'a [TypeAst], location: &SrcSpan) -> Document<'a> {
762 let args = args.iter().map(|type_| self.type_ast(type_)).collect_vec();
763 self.wrap_args(args, location.end)
764 }
765
766 pub fn type_alias<'a>(
767 &mut self,
768 publicity: Publicity,
769 name: &'a str,
770 args: &'a [SpannedString],
771 type_: &'a TypeAst,
772 deprecation: &'a Deprecation,
773 location: &SrcSpan,
774 ) -> Document<'a> {
775 let attributes = AttributesPrinter::new()
776 .set_deprecation(deprecation)
777 .set_internal(publicity)
778 .to_doc();
779
780 let head = docvec![attributes, pub_(publicity), "type ", name];
781 let head = if args.is_empty() {
782 head
783 } else {
784 let args = args.iter().map(|(_, e)| e.to_doc()).collect_vec();
785 head.append(self.wrap_args(args, location.end).group())
786 };
787
788 head.append(" =")
789 .append(line().append(self.type_ast(type_)).group().nest(INDENT))
790 }
791
792 fn fn_arg<'a, A>(&mut self, arg: &'a Arg<A>) -> Document<'a> {
793 let comments = self.pop_comments(arg.location.start);
794 let doc = match &arg.annotation {
795 None => arg.names.to_doc(),
796 Some(a) => arg.names.to_doc().append(": ").append(self.type_ast(a)),
797 }
798 .group();
799 commented(doc, comments)
800 }
801
802 fn statement_fn<'a>(&mut self, function: &'a UntypedFunction) -> Document<'a> {
803 let attributes = AttributesPrinter::new()
804 .set_deprecation(&function.deprecation)
805 .set_internal(function.publicity)
806 .set_external_erlang(&function.external_erlang)
807 .set_external_javascript(&function.external_javascript)
808 .to_doc();
809
810 // Fn name and args
811 let args = function
812 .arguments
813 .iter()
814 .map(|argument| self.fn_arg(argument))
815 .collect_vec();
816 let signature = pub_(function.publicity)
817 .append("fn ")
818 .append(
819 &function
820 .name
821 .as_ref()
822 .expect("Function in a statement must be named")
823 .1,
824 )
825 .append(self.wrap_args(args, function.location.end));
826
827 // Add return annotation
828 let signature = match &function.return_annotation {
829 Some(anno) => signature.append(" -> ").append(self.type_ast(anno)),
830 None => signature,
831 }
832 .group();
833
834 let body = &function.body;
835 if body.len() == 1 && body.first().is_placeholder() {
836 return attributes.append(signature);
837 }
838
839 let head = attributes.append(signature);
840
841 // Format body
842 let body = self.statements(body);
843
844 // Add any trailing comments
845 let body = match printed_comments(self.pop_comments(function.end_position), false) {
846 Some(comments) => body.append(line()).append(comments),
847 None => body,
848 };
849
850 // Stick it all together
851 head.append(" {")
852 .append(line().append(body).nest(INDENT).group())
853 .append(line())
854 .append("}")
855 }
856
857 fn expr_fn<'a>(
858 &mut self,
859 args: &'a [UntypedArg],
860 return_annotation: Option<&'a TypeAst>,
861 body: &'a Vec1<UntypedStatement>,
862 location: &SrcSpan,
863 end_of_head_byte_index: &u32,
864 ) -> Document<'a> {
865 let args_docs = args.iter().map(|arg| self.fn_arg(arg)).collect_vec();
866 let args = self
867 .wrap_args(args_docs, *end_of_head_byte_index)
868 .group()
869 .next_break_fits(NextBreakFitsMode::Disabled);
870 // ^^^ We add this so that when an expression function is passed as
871 // the last argument of a function and it goes over the line
872 // limit with just its arguments we don't get some strange
873 // splitting.
874 // See https://github.com/gleam-lang/gleam/issues/2571
875 //
876 // There's many ways we could be smarter than this. For example:
877 // - still split the arguments like it did in the example shown in the
878 // issue if the expr_fn has more than one argument
879 // - make sure that an anonymous function whose body is made up of a
880 // single expression doesn't get split (I think that could boil down
881 // to wrapping the body with a `next_break_fits(Disabled)`)
882 //
883 // These are some of the ways we could tweak the look of expression
884 // functions in the future if people are not satisfied with it.
885
886 let header = "fn".to_doc().append(args);
887
888 let header = match return_annotation {
889 None => header,
890 Some(t) => header.append(" -> ").append(self.type_ast(t)),
891 };
892
893 let statements = self.statements(body);
894 let body = match printed_comments(self.pop_comments(location.end), false) {
895 None => statements,
896 Some(comments) => statements.append(line()).append(comments).force_break(),
897 };
898
899 header.append(" ").append(wrap_block(body)).group()
900 }
901
902 fn statements<'a>(&mut self, statements: &'a Vec1<UntypedStatement>) -> Document<'a> {
903 let mut previous_position = 0;
904 let count = statements.len();
905 let mut documents = Vec::with_capacity(count * 2);
906 for (i, statement) in statements.iter().enumerate() {
907 let preceding_newline = self.pop_empty_lines(previous_position + 1);
908 if i != 0 && preceding_newline {
909 documents.push(lines(2));
910 } else if i != 0 {
911 documents.push(line());
912 }
913 previous_position = statement.location().end;
914 documents.push(self.statement(statement).group());
915
916 // If the last statement is a use we make sure it's followed by a
917 // todo to make it explicit it has an unimplemented callback.
918 if statement.is_use() && i == count - 1 {
919 documents.push(line());
920 documents.push("todo".to_doc());
921 }
922 }
923 if count == 1 && statements.first().is_expression() {
924 documents.to_doc()
925 } else {
926 documents.to_doc().force_break()
927 }
928 }
929
930 fn assignment<'a>(&mut self, assignment: &'a UntypedAssignment) -> Document<'a> {
931 let comments = self.pop_comments(assignment.location.start);
932 let Assignment {
933 pattern,
934 value,
935 kind,
936 annotation,
937 ..
938 } = assignment;
939
940 let _ = self.pop_empty_lines(pattern.location().end);
941
942 let (keyword, message) = match kind {
943 AssignmentKind::Let | AssignmentKind::Generated => ("let ", None),
944 AssignmentKind::Assert { message, .. } => ("let assert ", message.as_ref()),
945 };
946
947 let pattern = self.pattern(pattern);
948
949 let annotation = annotation
950 .as_ref()
951 .map(|a| ": ".to_doc().append(self.type_ast(a)));
952
953 let doc = keyword
954 .to_doc()
955 .append(pattern.append(annotation).group())
956 .append(" =")
957 .append(self.assigned_value(value));
958
959 commented(self.append_as_message(doc, message), comments)
960 }
961
962 fn expr<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> {
963 let comments = self.pop_comments(expr.start_byte_index());
964
965 let document = match expr {
966 UntypedExpr::Placeholder { .. } => panic!("Placeholders should not be formatted"),
967
968 UntypedExpr::Panic { message: None, .. } => "panic".to_doc(),
969 UntypedExpr::Panic {
970 message: Some(message),
971 ..
972 } => docvec!["panic as ", self.expr(message)],
973
974 UntypedExpr::Todo { message: None, .. } => "todo".to_doc(),
975 UntypedExpr::Todo {
976 message: Some(message),
977 ..
978 } => docvec!["todo as ", self.expr(message)],
979
980 UntypedExpr::Echo {
981 expression,
982 location: _,
983 keyword_end: _,
984 message,
985 } => self.echo(expression, message),
986
987 UntypedExpr::PipeLine { expressions, .. } => self.pipeline(expressions, false),
988
989 UntypedExpr::Int { value, .. } => self.int(value),
990
991 UntypedExpr::Float { value, .. } => self.float(value),
992
993 UntypedExpr::String { value, .. } => self.string(value),
994
995 UntypedExpr::Block {
996 statements,
997 location,
998 ..
999 } => self.block(location, statements, false),
1000
1001 UntypedExpr::Var { name, .. } if name == CAPTURE_VARIABLE => "_".to_doc(),
1002
1003 UntypedExpr::Var { name, .. } => name.to_doc(),
1004
1005 UntypedExpr::TupleIndex { tuple, index, .. } => self.tuple_index(tuple, *index),
1006
1007 UntypedExpr::NegateInt { value, .. } => self.negate_int(value),
1008
1009 UntypedExpr::NegateBool { value, .. } => self.negate_bool(value),
1010
1011 UntypedExpr::Fn { kind, body, .. } if kind.is_capture() => {
1012 self.fn_capture(body, FnCapturePosition::EverywhereElse)
1013 }
1014
1015 UntypedExpr::Fn {
1016 return_annotation,
1017 arguments: args,
1018 body,
1019 location,
1020 end_of_head_byte_index,
1021 ..
1022 } => self.expr_fn(
1023 args,
1024 return_annotation.as_ref(),
1025 body,
1026 location,
1027 end_of_head_byte_index,
1028 ),
1029
1030 UntypedExpr::List {
1031 elements,
1032 tail,
1033 location,
1034 } => self.list(elements, tail.as_deref(), location),
1035
1036 UntypedExpr::Call {
1037 fun,
1038 arguments: args,
1039 location,
1040 ..
1041 } => self.call(fun, args, location),
1042
1043 UntypedExpr::BinOp {
1044 name, left, right, ..
1045 } => self.bin_op(name, left, right, false),
1046
1047 UntypedExpr::Case {
1048 subjects,
1049 clauses,
1050 location,
1051 } => self.case(subjects, clauses.as_deref().unwrap_or_default(), location),
1052
1053 UntypedExpr::FieldAccess {
1054 label, container, ..
1055 } => if let UntypedExpr::TupleIndex { .. } = container.as_ref() {
1056 self.expr(container).surround("{ ", " }")
1057 } else {
1058 self.expr(container)
1059 }
1060 .append(".")
1061 .append(label.as_str()),
1062
1063 UntypedExpr::Tuple { elements, location } => self.tuple(elements, location),
1064
1065 UntypedExpr::BitArray {
1066 segments, location, ..
1067 } => {
1068 let segment_docs = segments
1069 .iter()
1070 .map(|segment| bit_array_segment(segment, |e| self.bit_array_segment_expr(e)))
1071 .collect_vec();
1072
1073 self.bit_array(
1074 segment_docs,
1075 segments
1076 .iter()
1077 .all(|s| s.value.can_have_multiple_per_line()),
1078 location,
1079 )
1080 }
1081 UntypedExpr::RecordUpdate {
1082 constructor,
1083 record,
1084 arguments: args,
1085 location,
1086 ..
1087 } => self.record_update(constructor, record, args, location),
1088 };
1089 commented(document, comments)
1090 }
1091
1092 fn string<'a>(&self, string: &'a EcoString) -> Document<'a> {
1093 let doc = string.to_doc().surround("\"", "\"");
1094 if string.contains('\n') {
1095 doc.force_break()
1096 } else {
1097 doc
1098 }
1099 }
1100
1101 fn bin_op_string<'a>(&self, string: &'a EcoString) -> Document<'a> {
1102 let lines = string.split('\n').collect_vec();
1103 match lines.as_slice() {
1104 [] | [_] => string.to_doc().surround("\"", "\""),
1105 [first_line, lines @ ..] => {
1106 let mut doc = docvec!["\"", first_line];
1107 for line in lines {
1108 doc = doc
1109 .append(pretty::line().set_nesting(0))
1110 .append(line.to_doc())
1111 }
1112 doc.append("\"".to_doc()).group()
1113 }
1114 }
1115 }
1116
1117 fn float<'a>(&self, value: &'a str) -> Document<'a> {
1118 // Create parts
1119 let mut parts = value.split('.');
1120 let integer_part = parts.next().unwrap_or_default();
1121 // floating point part
1122 let fp_part = parts.next().unwrap_or_default();
1123 let integer_doc = self.underscore_integer_string(integer_part);
1124 let dot_doc = ".".to_doc();
1125
1126 // Split fp_part into a regular fractional and maybe a scientific part
1127 let (fp_part_fractional, fp_part_scientific) = fp_part.split_at(
1128 fp_part
1129 .chars()
1130 .position(|ch| ch == 'e')
1131 .unwrap_or(fp_part.len()),
1132 );
1133
1134 // Trim right any consequtive '0's
1135 let mut fp_part_fractional = fp_part_fractional.trim_end_matches('0').to_string();
1136 // If there is no fractional part left, add a '0', thus that 1. becomes 1.0 etc.
1137 if fp_part_fractional.is_empty() {
1138 fp_part_fractional.push('0');
1139 }
1140 let fp_doc = fp_part_fractional.chars().collect::<EcoString>();
1141
1142 integer_doc
1143 .append(dot_doc)
1144 .append(fp_doc)
1145 .append(fp_part_scientific)
1146 }
1147
1148 fn int<'a>(&self, value: &'a str) -> Document<'a> {
1149 if value.starts_with("0x") || value.starts_with("0b") || value.starts_with("0o") {
1150 return value.to_doc();
1151 }
1152
1153 self.underscore_integer_string(value)
1154 }
1155
1156 fn underscore_integer_string<'a>(&self, value: &'a str) -> Document<'a> {
1157 let underscore_ch = '_';
1158 let minus_ch = '-';
1159
1160 let len = value.len();
1161 let underscore_ch_cnt = value.matches(underscore_ch).count();
1162 let reformat_watershed = if value.starts_with(minus_ch) { 6 } else { 5 };
1163 let insert_underscores = (len - underscore_ch_cnt) >= reformat_watershed;
1164
1165 let mut new_value = String::new();
1166 let mut j = 0;
1167 for (i, ch) in value.chars().rev().enumerate() {
1168 if ch == '_' {
1169 continue;
1170 }
1171
1172 if insert_underscores && i != 0 && ch != minus_ch && i < len && j % 3 == 0 {
1173 new_value.push(underscore_ch);
1174 }
1175 new_value.push(ch);
1176
1177 j += 1;
1178 }
1179
1180 new_value.chars().rev().collect::<EcoString>().to_doc()
1181 }
1182
1183 fn pattern_constructor<'a>(
1184 &mut self,
1185 name: &'a str,
1186 args: &'a [CallArg<UntypedPattern>],
1187 module: &'a Option<(EcoString, SrcSpan)>,
1188 spread: Option<SrcSpan>,
1189 location: &SrcSpan,
1190 ) -> Document<'a> {
1191 fn is_breakable(expr: &UntypedPattern) -> bool {
1192 match expr {
1193 Pattern::Tuple { .. } | Pattern::List { .. } | Pattern::BitArray { .. } => true,
1194 Pattern::Constructor {
1195 arguments: args, ..
1196 } => !args.is_empty(),
1197 _ => false,
1198 }
1199 }
1200
1201 let name = match module {
1202 Some((m, _)) => m.to_doc().append(".").append(name),
1203 None => name.to_doc(),
1204 };
1205
1206 if args.is_empty() && spread.is_some() {
1207 name.append("(..)")
1208 } else if args.is_empty() {
1209 name
1210 } else if spread.is_some() {
1211 let args = args.iter().map(|a| self.pattern_call_arg(a)).collect_vec();
1212 name.append(self.wrap_args_with_spread(args, location.end))
1213 } else {
1214 match args {
1215 [arg] if is_breakable(&arg.value) => name
1216 .append("(")
1217 .append(self.pattern_call_arg(arg))
1218 .append(")")
1219 .group(),
1220
1221 _ => {
1222 let args = args.iter().map(|a| self.pattern_call_arg(a)).collect_vec();
1223 name.append(self.wrap_args(args, location.end)).group()
1224 }
1225 }
1226 }
1227 }
1228
1229 fn call<'a>(
1230 &mut self,
1231 fun: &'a UntypedExpr,
1232 args: &'a [CallArg<UntypedExpr>],
1233 location: &SrcSpan,
1234 ) -> Document<'a> {
1235 let expr = match fun {
1236 UntypedExpr::Placeholder { .. } => panic!("Placeholders should not be formatted"),
1237
1238 UntypedExpr::PipeLine { .. } => break_block(self.expr(fun)),
1239
1240 UntypedExpr::BinOp { .. }
1241 | UntypedExpr::Int { .. }
1242 | UntypedExpr::Float { .. }
1243 | UntypedExpr::String { .. }
1244 | UntypedExpr::Block { .. }
1245 | UntypedExpr::Var { .. }
1246 | UntypedExpr::Fn { .. }
1247 | UntypedExpr::List { .. }
1248 | UntypedExpr::Call { .. }
1249 | UntypedExpr::Case { .. }
1250 | UntypedExpr::FieldAccess { .. }
1251 | UntypedExpr::Tuple { .. }
1252 | UntypedExpr::TupleIndex { .. }
1253 | UntypedExpr::Todo { .. }
1254 | UntypedExpr::Echo { .. }
1255 | UntypedExpr::Panic { .. }
1256 | UntypedExpr::BitArray { .. }
1257 | UntypedExpr::RecordUpdate { .. }
1258 | UntypedExpr::NegateBool { .. }
1259 | UntypedExpr::NegateInt { .. } => self.expr(fun),
1260 };
1261
1262 let arity = args.len();
1263 self.append_inlinable_wrapped_args(
1264 expr,
1265 args,
1266 location,
1267 |arg| &arg.value,
1268 |self_, arg| self_.call_arg(arg, arity),
1269 )
1270 }
1271
1272 fn tuple<'a>(&mut self, elements: &'a [UntypedExpr], location: &SrcSpan) -> Document<'a> {
1273 if elements.is_empty() {
1274 // We take all comments that come _before_ the end of the tuple,
1275 // that is all comments that are inside "#(" and ")", if there's
1276 // any comment we want to put it inside the empty tuple!
1277 return match printed_comments(self.pop_comments(location.end), false) {
1278 None => "#()".to_doc(),
1279 Some(comments) => "#("
1280 .to_doc()
1281 .append(break_("", "").nest(INDENT))
1282 .append(comments)
1283 .append(break_("", ""))
1284 .append(")")
1285 // vvv We want to make sure the comments are on a separate
1286 // line from the opening and closing parentheses so we
1287 // force the breaks to be split on newlines.
1288 .force_break(),
1289 };
1290 }
1291
1292 self.append_inlinable_wrapped_args(
1293 "#".to_doc(),
1294 elements,
1295 location,
1296 |e| e,
1297 |self_, e| self_.comma_separated_item(e, elements.len()),
1298 )
1299 }
1300
1301 // Appends to the given docs a comma-separated list of documents wrapped by
1302 // parentheses. If the last item of the argument list is splittable the
1303 // resulting document will try to first split that before splitting all the
1304 // other arguments.
1305 // This is used for function calls and tuples.
1306 fn append_inlinable_wrapped_args<'a, 'b, T, ToExpr, ToDoc>(
1307 &mut self,
1308 doc: Document<'a>,
1309 values: &'b [T],
1310 location: &SrcSpan,
1311 to_expr: ToExpr,
1312 to_doc: ToDoc,
1313 ) -> Document<'a>
1314 where
1315 T: HasLocation,
1316 ToExpr: Fn(&T) -> &UntypedExpr,
1317 ToDoc: Fn(&mut Self, &'b T) -> Document<'a>,
1318 {
1319 match init_and_last(values) {
1320 Some((initial_values, last_value))
1321 if is_breakable_argument(to_expr(last_value), values.len())
1322 && !self.any_comments(last_value.location().start) =>
1323 {
1324 let mut docs = initial_values
1325 .iter()
1326 .map(|value| to_doc(self, value))
1327 .collect_vec();
1328
1329 let last_value_doc = to_doc(self, last_value)
1330 .group()
1331 .next_break_fits(NextBreakFitsMode::Enabled);
1332
1333 docs.append(&mut vec![last_value_doc]);
1334
1335 doc.append(self.wrap_function_call_args(docs, location))
1336 .next_break_fits(NextBreakFitsMode::Disabled)
1337 .group()
1338 }
1339
1340 Some(_) | None => {
1341 let docs = values.iter().map(|value| to_doc(self, value)).collect_vec();
1342 doc.append(self.wrap_function_call_args(docs, location))
1343 .group()
1344 }
1345 }
1346 }
1347
1348 pub fn case<'a>(
1349 &mut self,
1350 subjects: &'a [UntypedExpr],
1351 clauses: &'a [UntypedClause],
1352 location: &'a SrcSpan,
1353 ) -> Document<'a> {
1354 let subjects_doc = break_("case", "case ")
1355 .append(join(
1356 subjects.iter().map(|s| self.expr(s).group()),
1357 break_(",", ", "),
1358 ))
1359 .nest(INDENT)
1360 .append(break_("", " "))
1361 .append("{")
1362 .next_break_fits(NextBreakFitsMode::Disabled)
1363 .group();
1364
1365 let clauses_doc = concat(
1366 clauses
1367 .iter()
1368 .enumerate()
1369 .map(|(i, c)| self.clause(c, i as u32).group()),
1370 );
1371
1372 // We get all remaining comments that come before the case's closing
1373 // bracket. If there's any we add those before the closing bracket
1374 // instead of moving those out of the case expression.
1375 // Otherwise those would be moved out of the case expression.
1376 let comments = self.pop_comments(location.end);
1377 let closing_bracket = match printed_comments(comments, false) {
1378 None => docvec![line(), "}"],
1379 Some(comment) => docvec![line(), comment]
1380 .nest(INDENT)
1381 .append(line())
1382 .append("}"),
1383 };
1384
1385 subjects_doc
1386 .append(line().append(clauses_doc).nest(INDENT))
1387 .append(closing_bracket)
1388 .force_break()
1389 }
1390
1391 pub fn record_update<'a>(
1392 &mut self,
1393 constructor: &'a UntypedExpr,
1394 record: &'a RecordBeingUpdated,
1395 args: &'a [UntypedRecordUpdateArg],
1396 location: &SrcSpan,
1397 ) -> Document<'a> {
1398 let constructor_doc: Document<'a> = self.expr(constructor);
1399 let pieces = std::iter::once(RecordUpdatePiece::Record(record))
1400 .chain(args.iter().map(RecordUpdatePiece::Argument))
1401 .collect_vec();
1402
1403 self.append_inlinable_wrapped_args(
1404 constructor_doc,
1405 &pieces,
1406 location,
1407 |arg| match arg {
1408 RecordUpdatePiece::Argument(arg) => &arg.value,
1409 RecordUpdatePiece::Record(record) => record.base.as_ref(),
1410 },
1411 |this, arg| match arg {
1412 RecordUpdatePiece::Argument(arg) => this.record_update_arg(arg),
1413 RecordUpdatePiece::Record(record) => {
1414 let comments = this.pop_comments(record.base.location().start);
1415 commented("..".to_doc().append(this.expr(&record.base)), comments)
1416 }
1417 },
1418 )
1419 }
1420
1421 pub fn bin_op<'a>(
1422 &mut self,
1423 name: &'a BinOp,
1424 left: &'a UntypedExpr,
1425 right: &'a UntypedExpr,
1426 nest_steps: bool,
1427 ) -> Document<'a> {
1428 let left_side = self.bin_op_side(name, left, nest_steps);
1429
1430 let comments = self.pop_comments(right.start_byte_index());
1431 let name_doc = break_("", " ").append(commented(name.to_doc(), comments));
1432
1433 let right_side = self.bin_op_side(name, right, nest_steps);
1434
1435 left_side
1436 .append(if nest_steps {
1437 name_doc.nest(INDENT)
1438 } else {
1439 name_doc
1440 })
1441 .append(" ")
1442 .append(right_side)
1443 }
1444
1445 fn bin_op_side<'a>(
1446 &mut self,
1447 operator: &'a BinOp,
1448 side: &'a UntypedExpr,
1449 nest_steps: bool,
1450 ) -> Document<'a> {
1451 let side_doc = match side {
1452 UntypedExpr::String { value, .. } => self.bin_op_string(value),
1453 UntypedExpr::BinOp {
1454 name, left, right, ..
1455 } => self.bin_op(name, left, right, nest_steps),
1456 _ => self.expr(side),
1457 };
1458 match side.bin_op_name() {
1459 // In case the other side is a binary operation as well and it can
1460 // be grouped together with the current binary operation, the two
1461 // docs are simply concatenated, so that they will end up in the
1462 // same group and the formatter will try to keep those on a single
1463 // line.
1464 Some(side_name) if side_name.can_be_grouped_with(operator) => side_doc,
1465 // In case the binary operations cannot be grouped together the
1466 // other side is treated as a group on its own so that it can be
1467 // broken independently of other pieces of the binary operations
1468 // chain.
1469 _ => self.operator_side(
1470 side_doc.group(),
1471 operator.precedence(),
1472 side.bin_op_precedence(),
1473 ),
1474 }
1475 }
1476
1477 pub fn operator_side<'a>(&self, doc: Document<'a>, op: u8, side: u8) -> Document<'a> {
1478 if op > side {
1479 wrap_block(doc).group()
1480 } else {
1481 doc
1482 }
1483 }
1484
1485 fn spans_multiple_lines(&self, start: u32, end: u32) -> bool {
1486 self.new_lines
1487 .binary_search_by(|newline| {
1488 if *newline <= start {
1489 Ordering::Less
1490 } else if *newline >= end {
1491 Ordering::Greater
1492 } else {
1493 // If the newline is in between the pipe start and end
1494 // then we've found it!
1495 Ordering::Equal
1496 }
1497 })
1498 // If we couldn't find any newline between the start and end of
1499 // the pipeline then we will try and keep it on a single line.
1500 .is_ok()
1501 }
1502
1503 /// Returns true if there's a trailing comma between `start` and `end`.
1504 ///
1505 fn has_trailing_comma(&self, start: u32, end: u32) -> bool {
1506 self.trailing_commas
1507 .binary_search_by(|comma| {
1508 if *comma < start {
1509 Ordering::Less
1510 } else if *comma > end {
1511 Ordering::Greater
1512 } else {
1513 Ordering::Equal
1514 }
1515 })
1516 .is_ok()
1517 }
1518
1519 fn pipeline<'a>(
1520 &mut self,
1521 expressions: &'a Vec1<UntypedExpr>,
1522 nest_pipe: bool,
1523 ) -> Document<'a> {
1524 let mut docs = Vec::with_capacity(expressions.len() * 3);
1525 let first = expressions.first();
1526 let first_precedence = first.bin_op_precedence();
1527 let first = self.expr(first).group();
1528 docs.push(self.operator_side(first, 5, first_precedence));
1529
1530 let pipeline_start = expressions.first().location().start;
1531 let pipeline_end = expressions.last().location().end;
1532 let try_to_keep_on_one_line = !self.spans_multiple_lines(pipeline_start, pipeline_end);
1533
1534 for expr in expressions.iter().skip(1) {
1535 let comments = self.pop_comments(expr.location().start);
1536 let doc = match expr {
1537 UntypedExpr::Fn { kind, body, .. } if kind.is_capture() => {
1538 self.fn_capture(body, FnCapturePosition::RightHandSideOfPipe)
1539 }
1540 _ => self.expr(expr),
1541 };
1542 let doc = if nest_pipe { doc.nest(INDENT) } else { doc };
1543 let space = if try_to_keep_on_one_line {
1544 break_("", " ")
1545 } else {
1546 line()
1547 };
1548 let pipe = space.append(commented("|> ".to_doc(), comments));
1549 let pipe = if nest_pipe { pipe.nest(INDENT) } else { pipe };
1550 docs.push(pipe);
1551 docs.push(self.operator_side(doc, 4, expr.bin_op_precedence()));
1552 }
1553
1554 if try_to_keep_on_one_line {
1555 docs.to_doc()
1556 } else {
1557 docs.to_doc().force_break()
1558 }
1559 }
1560
1561 fn fn_capture<'a>(
1562 &mut self,
1563 call: &'a [UntypedStatement],
1564 position: FnCapturePosition,
1565 ) -> Document<'a> {
1566 // The body of a capture being multiple statements shouldn't be possible...
1567 if call.len() != 1 {
1568 panic!("Function capture found not to have a single statement call");
1569 }
1570
1571 let Some(Statement::Expression(UntypedExpr::Call {
1572 fun,
1573 arguments,
1574 location,
1575 })) = call.first()
1576 else {
1577 // The body of a capture being not a fn shouldn't be possible...
1578 panic!("Function capture body found not to be a call in the formatter")
1579 };
1580
1581 match (position, arguments.as_slice()) {
1582 // The capture has a single unlabelled hole:
1583 //
1584 // wibble |> wobble(_)
1585 // list.map([], wobble(_))
1586 //
1587 // We want these to become:
1588 //
1589 // wibble |> wobble
1590 // list.map([], wobble)
1591 //
1592 (FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse, [arg])
1593 if arg.is_capture_hole() && arg.label.is_none() =>
1594 {
1595 self.expr(fun)
1596 }
1597
1598 // The capture is on the right hand side of a pipe and its first
1599 // argument it an unlabelled capture hole:
1600 //
1601 // wibble |> wobble(_, woo)
1602 //
1603 // We want it to become:
1604 //
1605 // wibble |> wobble(woo)
1606 //
1607 (FnCapturePosition::RightHandSideOfPipe, [arg, rest @ ..])
1608 if arg.is_capture_hole() && arg.label.is_none() =>
1609 {
1610 let expr = self.expr(fun);
1611 let arity = rest.len();
1612 self.append_inlinable_wrapped_args(
1613 expr,
1614 rest,
1615 location,
1616 |arg| &arg.value,
1617 |self_, arg| self_.call_arg(arg, arity),
1618 )
1619 }
1620
1621 // In all other cases we print it like a regular function call
1622 // without changing it.
1623 //
1624 (
1625 FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse,
1626 arguments,
1627 ) => {
1628 let expr = self.expr(fun);
1629 let arity = arguments.len();
1630 self.append_inlinable_wrapped_args(
1631 expr,
1632 arguments,
1633 location,
1634 |arg| &arg.value,
1635 |self_, arg| self_.call_arg(arg, arity),
1636 )
1637 }
1638 }
1639 }
1640
1641 pub fn record_constructor<'a, A>(
1642 &mut self,
1643 constructor: &'a RecordConstructor<A>,
1644 ) -> Document<'a> {
1645 let comments = self.pop_comments(constructor.location.start);
1646 let doc_comments = self.doc_comments(constructor.location.start);
1647 let attributes = AttributesPrinter::new()
1648 .set_deprecation(&constructor.deprecation)
1649 .to_doc();
1650
1651 let doc = if constructor.arguments.is_empty() {
1652 if self.any_comments(constructor.location.end) {
1653 attributes
1654 .append(constructor.name.as_str().to_doc())
1655 .append(self.wrap_args(vec![], constructor.location.end))
1656 .group()
1657 } else {
1658 attributes.append(constructor.name.as_str().to_doc())
1659 }
1660 } else {
1661 let args = constructor
1662 .arguments
1663 .iter()
1664 .map(
1665 |RecordConstructorArg {
1666 label,
1667 ast,
1668 location,
1669 ..
1670 }| {
1671 let arg_comments = self.pop_comments(location.start);
1672 let arg = match label {
1673 Some((_, l)) => l.to_doc().append(": ").append(self.type_ast(ast)),
1674 None => self.type_ast(ast),
1675 };
1676
1677 commented(
1678 self.doc_comments(location.start).append(arg).group(),
1679 arg_comments,
1680 )
1681 },
1682 )
1683 .collect_vec();
1684
1685 attributes
1686 .append(constructor.name.as_str().to_doc())
1687 .append(self.wrap_args(args, constructor.location.end).group())
1688 };
1689
1690 commented(doc_comments.append(doc).group(), comments)
1691 }
1692
1693 pub fn custom_type<'a, A>(&mut self, ct: &'a CustomType<A>) -> Document<'a> {
1694 let _ = self.pop_empty_lines(ct.location.end);
1695
1696 let attributes = AttributesPrinter::new()
1697 .set_deprecation(&ct.deprecation)
1698 .set_internal(ct.publicity)
1699 .to_doc();
1700
1701 let doc = attributes
1702 .append(pub_(ct.publicity))
1703 .append(if ct.opaque { "opaque type " } else { "type " })
1704 .append(if ct.parameters.is_empty() {
1705 ct.name.clone().to_doc()
1706 } else {
1707 let args = ct.parameters.iter().map(|(_, e)| e.to_doc()).collect_vec();
1708 ct.name
1709 .clone()
1710 .to_doc()
1711 .append(self.wrap_args(args, ct.location.end))
1712 .group()
1713 });
1714
1715 if ct.constructors.is_empty() {
1716 return doc;
1717 }
1718 let doc = doc.append(" {");
1719
1720 let inner = concat(ct.constructors.iter().map(|c| {
1721 if self.pop_empty_lines(c.location.start) {
1722 lines(2)
1723 } else {
1724 line()
1725 }
1726 .append(self.record_constructor(c))
1727 }));
1728
1729 // Add any trailing comments
1730 let inner = match printed_comments(self.pop_comments(ct.end_position), false) {
1731 Some(comments) => inner.append(line()).append(comments),
1732 None => inner,
1733 }
1734 .nest(INDENT)
1735 .group();
1736
1737 doc.append(inner).append(line()).append("}")
1738 }
1739
1740 fn call_arg<'a>(&mut self, arg: &'a CallArg<UntypedExpr>, arity: usize) -> Document<'a> {
1741 self.format_call_arg(arg, expr_call_arg_formatting, |this, value| {
1742 this.comma_separated_item(value, arity)
1743 })
1744 }
1745
1746 fn format_call_arg<'a, A, F, G>(
1747 &mut self,
1748 arg: &'a CallArg<A>,
1749 figure_formatting: F,
1750 format_value: G,
1751 ) -> Document<'a>
1752 where
1753 F: Fn(&'a CallArg<A>) -> CallArgFormatting<'a, A>,
1754 G: Fn(&mut Self, &'a A) -> Document<'a>,
1755 {
1756 match figure_formatting(arg) {
1757 CallArgFormatting::Unlabelled(value) => format_value(self, value),
1758 CallArgFormatting::ShorthandLabelled(label) => {
1759 let comments = self.pop_comments(arg.location.start);
1760 let label = label.as_ref().to_doc().append(":");
1761 commented(label, comments)
1762 }
1763 CallArgFormatting::Labelled(label, value) => {
1764 let comments = self.pop_comments(arg.location.start);
1765 let label = label.as_ref().to_doc().append(": ");
1766 let value = format_value(self, value);
1767 commented(label, comments).append(value)
1768 }
1769 }
1770 }
1771
1772 fn record_update_arg<'a>(&mut self, arg: &'a UntypedRecordUpdateArg) -> Document<'a> {
1773 let comments = self.pop_comments(arg.location.start);
1774 match arg {
1775 // Argument supplied with a label shorthand.
1776 _ if arg.uses_label_shorthand() => {
1777 commented(arg.label.as_str().to_doc().append(":"), comments)
1778 }
1779 // Labelled argument.
1780 _ => {
1781 let doc = arg
1782 .label
1783 .as_str()
1784 .to_doc()
1785 .append(": ")
1786 .append(self.expr(&arg.value))
1787 .group();
1788
1789 if arg.value.is_binop() || arg.value.is_pipeline() {
1790 commented(doc, comments).nest(INDENT)
1791 } else {
1792 commented(doc, comments)
1793 }
1794 }
1795 }
1796 }
1797
1798 fn tuple_index<'a>(&mut self, tuple: &'a UntypedExpr, index: u64) -> Document<'a> {
1799 match tuple {
1800 // In case we have a block with a single variable tuple access we
1801 // remove that redundat wrapper:
1802 //
1803 // {tuple.1}.0 becomes
1804 // tuple.1.0
1805 //
1806 UntypedExpr::Block { statements, .. } => match statements.as_slice() {
1807 [Statement::Expression(tuple @ UntypedExpr::TupleIndex { tuple: inner, .. })]
1808 // We can't apply this change if the inner thing is a
1809 // literal tuple because the compiler cannot currently parse
1810 // it: `#(1, #(2, 3)).1.0` is a syntax error at the moment.
1811 if !inner.is_tuple() =>
1812 {
1813 self.expr(tuple)
1814 }
1815 _ => self.expr(tuple),
1816 },
1817 _ => self.expr(tuple),
1818 }
1819 .append(".")
1820 .append(index)
1821 }
1822
1823 fn case_clause_value<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> {
1824 match expr {
1825 UntypedExpr::Fn { .. }
1826 | UntypedExpr::List { .. }
1827 | UntypedExpr::Tuple { .. }
1828 | UntypedExpr::BitArray { .. } => {
1829 let expression_comments = self.pop_comments(expr.location().start);
1830 let expression_doc = self.expr(expr);
1831 match printed_comments(expression_comments, true) {
1832 Some(comments) => line().append(comments).append(expression_doc).nest(INDENT),
1833 None => " ".to_doc().append(expression_doc),
1834 }
1835 }
1836
1837 UntypedExpr::Case { .. } => line().append(self.expr(expr)).nest(INDENT),
1838
1839 UntypedExpr::Block {
1840 statements,
1841 location,
1842 ..
1843 } => " ".to_doc().append(self.block(location, statements, true)),
1844
1845 _ => break_("", " ").append(self.expr(expr).group()).nest(INDENT),
1846 }
1847 .next_break_fits(NextBreakFitsMode::Disabled)
1848 .group()
1849 }
1850
1851 fn assigned_value<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> {
1852 match expr {
1853 UntypedExpr::Case { .. } => " ".to_doc().append(self.expr(expr)).group(),
1854 _ => self.case_clause_value(expr),
1855 }
1856 }
1857
1858 fn clause<'a>(&mut self, clause: &'a UntypedClause, index: u32) -> Document<'a> {
1859 let space_before = self.pop_empty_lines(clause.location.start);
1860 let comments = self.pop_comments(clause.location.start);
1861
1862 let clause_doc = match &clause.guard {
1863 None => self.alternative_patterns(clause),
1864 Some(guard) => self
1865 .alternative_patterns(clause)
1866 .append(break_("", " ").nest(INDENT))
1867 .append("if ")
1868 .append(self.clause_guard(guard).group().nest(INDENT)),
1869 };
1870
1871 // In case there's a guard or multiple subjects, if we decide to break
1872 // the patterns on multiple lines we also want the arrow to end up on
1873 // its own line to improve legibility.
1874 //
1875 // This looks like this:
1876 // ```gleam
1877 // case wibble, wobble {
1878 // Wibble(_), // pretend this goes over the line limit
1879 // Wobble(_)
1880 // -> todo
1881 // // Notice how the arrow is broken on its own line, the same goes
1882 // // for patterns with `if` guards.
1883 // }
1884 // ```
1885
1886 let has_guard = clause.guard.is_some();
1887 let has_multiple_subjects = clause.pattern.len() > 1;
1888 let arrow_break = if has_guard || has_multiple_subjects {
1889 break_("", " ")
1890 } else {
1891 " ".to_doc()
1892 };
1893
1894 let clause_doc = clause_doc
1895 .append(arrow_break)
1896 .group()
1897 .append("->")
1898 .append(self.case_clause_value(&clause.then).group())
1899 .group();
1900
1901 let clause_doc = match printed_comments(comments, false) {
1902 Some(comments) => comments.append(line()).append(clause_doc),
1903 None => clause_doc,
1904 };
1905
1906 if index == 0 {
1907 clause_doc
1908 } else if space_before {
1909 lines(2).append(clause_doc)
1910 } else {
1911 line().append(clause_doc)
1912 }
1913 }
1914
1915 fn alternative_patterns<'a>(&mut self, clause: &'a UntypedClause) -> Document<'a> {
1916 let has_guard = clause.guard.is_some();
1917 let has_multiple_subjects = clause.pattern.len() > 1;
1918
1919 // In case there's an `if` guard but no multiple subjects we want to add
1920 // additional indentation before the vartical bar separating alternative
1921 // patterns `|`.
1922 // We're not adding the indentation if there's multiple subjects as that
1923 // would make things harder to read, aligning the vertical bar with the
1924 // different subjects:
1925 // ```
1926 // case wibble, wobble {
1927 // Wibble,
1928 // Wobble
1929 // | Wibble, // <- we don't want this indentation!
1930 // Wobble -> todo
1931 // }
1932 // ```
1933 let alternatives_separator = if has_guard && !has_multiple_subjects {
1934 break_("", " ").nest(INDENT).append("| ")
1935 } else {
1936 break_("", " ").append("| ")
1937 };
1938
1939 let alternative_patterns = std::iter::once(&clause.pattern)
1940 .chain(&clause.alternative_patterns)
1941 .enumerate()
1942 .map(|(alternative_index, p)| {
1943 // Here `p` is a single pattern that can be comprised of
1944 // multiple subjects.
1945 // ```gleam
1946 // case wibble, wobble {
1947 // True, False
1948 // //^^^^^^^^^^^ This is a single pattern with multiple subjects
1949 // | _, _ -> todo
1950 // }
1951 // ```
1952 let is_first_alternative = alternative_index == 0;
1953 let subject_docs = p.iter().enumerate().map(|(subject_index, subject)| {
1954 // There's a small catch in turning each subject into a document.
1955 // Sadly we can't simply call `self.pattern` on each subject and
1956 // then nest each one in case it gets broken.
1957 // The first ever pattern that appears in a case clause (that is
1958 // the first subject of the first alternative) must not be nested
1959 // further; otherwise, when broken, it would have 2 extra spaces
1960 // of indentation: https://github.com/gleam-lang/gleam/issues/2940.
1961 let is_first_subject = subject_index == 0;
1962 let is_first_pattern_of_clause = is_first_subject && is_first_alternative;
1963 let subject_doc = self.pattern(subject);
1964 if is_first_pattern_of_clause {
1965 subject_doc
1966 } else {
1967 subject_doc.nest(INDENT)
1968 }
1969 });
1970 // We join all subjects with a breakable comma (that's also
1971 // going to be nested) and make the subjects into a group to
1972 // make sure the formatter tries to keep them on a single line.
1973 join(subject_docs, break_(",", ", ").nest(INDENT)).group()
1974 });
1975 // Last, we make sure that the formatter tries to keep each
1976 // alternative on a single line by making it a group!
1977 join(alternative_patterns, alternatives_separator).group()
1978 }
1979
1980 fn list<'a>(
1981 &mut self,
1982 elements: &'a [UntypedExpr],
1983 tail: Option<&'a UntypedExpr>,
1984 location: &SrcSpan,
1985 ) -> Document<'a> {
1986 if elements.is_empty() {
1987 return match tail {
1988 Some(tail) => self.expr(tail),
1989 // We take all comments that come _before_ the end of the list,
1990 // that is all comments that are inside "[" and "]", if there's
1991 // any comment we want to put it inside the empty list!
1992 None => match printed_comments(self.pop_comments(location.end), false) {
1993 None => "[]".to_doc(),
1994 Some(comments) => "["
1995 .to_doc()
1996 .append(break_("", "").nest(INDENT))
1997 .append(comments)
1998 .append(break_("", ""))
1999 .append("]")
2000 // vvv We want to make sure the comments are on a separate
2001 // line from the opening and closing brackets so we
2002 // force the breaks to be split on newlines.
2003 .force_break(),
2004 },
2005 };
2006 }
2007
2008 let list_packing = self.list_items_packing(
2009 elements,
2010 tail,
2011 UntypedExpr::can_have_multiple_per_line,
2012 *location,
2013 );
2014
2015 let comma = match list_packing {
2016 ListItemsPacking::FitMultiplePerLine => flex_break(",", ", "),
2017 ListItemsPacking::FitOnePerLine | ListItemsPacking::BreakOnePerLine => {
2018 break_(",", ", ")
2019 }
2020 };
2021
2022 let list_size = elements.len()
2023 + match tail {
2024 Some(_) => 1,
2025 None => 0,
2026 };
2027
2028 let mut elements_doc = nil();
2029 for element in elements.iter() {
2030 let empty_lines = self.pop_empty_lines(element.location().start);
2031 let element_doc = self.comma_separated_item(element, list_size);
2032
2033 elements_doc = if elements_doc.is_empty() {
2034 element_doc
2035 } else if empty_lines {
2036 // If there's empty lines before the list item we want to add an
2037 // empty line here. Notice how we're making sure no nesting is
2038 // added after the comma, otherwise we would be adding needless
2039 // whitespace in the empty line!
2040 docvec![
2041 elements_doc,
2042 comma.clone().set_nesting(0),
2043 line(),
2044 element_doc
2045 ]
2046 } else {
2047 docvec![elements_doc, comma.clone(), element_doc]
2048 };
2049 }
2050 elements_doc = elements_doc.next_break_fits(NextBreakFitsMode::Disabled);
2051
2052 let doc = break_("[", "[").append(elements_doc);
2053 // We need to keep the last break aside and do not add it immediately
2054 // because in case there's a final comment before the closing square
2055 // bracket we want to add indentation (to just that break). Otherwise,
2056 // the final comment would be less indented than list's elements.
2057 let (doc, last_break) = match tail {
2058 None => (doc.nest(INDENT), break_(",", "")),
2059
2060 Some(tail) => {
2061 let comments = self.pop_comments(tail.location().start);
2062 let tail = commented(docvec!["..", self.expr(tail)], comments);
2063 (
2064 doc.append(break_(",", ", ")).append(tail).nest(INDENT),
2065 break_("", ""),
2066 )
2067 }
2068 };
2069
2070 // We get all remaining comments that come before the list's closing
2071 // square bracket.
2072 // If there's any we add those before the closing square bracket instead
2073 // of moving those out of the list.
2074 // Otherwise those would be moved out of the list.
2075 let comments = self.pop_comments(location.end);
2076 let doc = match printed_comments(comments, false) {
2077 None => doc.append(last_break).append("]"),
2078 Some(comment) => doc
2079 .append(last_break.nest(INDENT))
2080 // ^ See how here we're adding the missing indentation to the
2081 // final break so that the final comment is as indented as the
2082 // list's items.
2083 .append(comment)
2084 .append(line())
2085 .append("]")
2086 .force_break(),
2087 };
2088
2089 match list_packing {
2090 ListItemsPacking::FitOnePerLine | ListItemsPacking::FitMultiplePerLine => doc.group(),
2091 ListItemsPacking::BreakOnePerLine => doc.force_break(),
2092 }
2093 }
2094
2095 fn list_items_packing<'a, T: HasLocation>(
2096 &self,
2097 items: &'a [T],
2098 tail: Option<&'a T>,
2099 can_have_multiple_per_line: impl Fn(&'a T) -> bool,
2100 list_location: SrcSpan,
2101 ) -> ListItemsPacking {
2102 let ends_with_trailing_comma = tail
2103 .map(|tail| tail.location().end)
2104 .or_else(|| items.last().map(|last| last.location().end))
2105 .is_some_and(|last_element_end| {
2106 self.has_trailing_comma(last_element_end, list_location.end)
2107 });
2108
2109 let has_multiple_elements_per_line =
2110 self.has_items_on_the_same_line(items.iter().chain(tail));
2111
2112 let has_empty_lines_between_elements = match (items.first(), items.last().or(tail)) {
2113 (Some(first), Some(last)) => self.empty_lines.first().is_some_and(|empty_line| {
2114 *empty_line >= first.location().end && *empty_line < last.location().start
2115 }),
2116 _ => false,
2117 };
2118
2119 if has_empty_lines_between_elements {
2120 // If there's any empty line between elements we want to force each
2121 // item onto its own line to preserve the empty lines that were
2122 // intentionally added.
2123 ListItemsPacking::BreakOnePerLine
2124 } else if !ends_with_trailing_comma {
2125 // If the list doesn't end with a trailing comma we try and pack it in
2126 // a single line; if we can't we'll put one item per line, no matter
2127 // the content of the list.
2128 ListItemsPacking::FitOnePerLine
2129 } else if tail.is_none()
2130 && items.iter().all(can_have_multiple_per_line)
2131 && has_multiple_elements_per_line
2132 && self.spans_multiple_lines(list_location.start, list_location.end)
2133 {
2134 // If there's a trailing comma, we can have multiple items per line,
2135 // and there's already multiple items per line, we try and pack as
2136 // many items as possible on each line.
2137 //
2138 // Note how we only ever try and pack lists where all items are
2139 // unbreakable primitives. To pack a list we need to use put
2140 // `flex_break`s between each item.
2141 // If the items themselves had breaks we could end up in a situation
2142 // where an item gets broken making it span multiple lines and the
2143 // spaces are not, for example:
2144 //
2145 // ```gleam
2146 // [Constructor("wibble", "lorem ipsum dolor sit amet something something"), Other(1)]
2147 // ```
2148 //
2149 // If we used flex breaks here the list would be formatted as:
2150 //
2151 // ```gleam
2152 // [
2153 // Constructor(
2154 // "wibble",
2155 // "lorem ipsum dolor sit amet something something",
2156 // ), Other(1)
2157 // ]
2158 // ```
2159 //
2160 // The first item is broken, meaning that once we get to the flex
2161 // space separating it from the following one the formatter is not
2162 // going to break it since there's enough space in the current line!
2163 ListItemsPacking::FitMultiplePerLine
2164 } else {
2165 // If it ends with a trailing comma we will force the list on
2166 // multiple lines, with one item per line.
2167 ListItemsPacking::BreakOnePerLine
2168 }
2169 }
2170
2171 fn has_items_on_the_same_line<'a, L: HasLocation + 'a, T: Iterator<Item = &'a L>>(
2172 &self,
2173 items: T,
2174 ) -> bool {
2175 let mut previous: Option<SrcSpan> = None;
2176 for item in items {
2177 let item_location = item.location();
2178 // A list has multiple items on the same line if two consecutive
2179 // ones do not span multiple lines.
2180 if let Some(previous) = previous {
2181 if !self.spans_multiple_lines(previous.end, item_location.start) {
2182 return true;
2183 }
2184 }
2185 previous = Some(item_location);
2186 }
2187 false
2188 }
2189
2190 /// Pretty prints an expression to be used in a comma separated list; for
2191 /// example as a list item, a tuple item or as an argument of a function call.
2192 fn comma_separated_item<'a>(
2193 &mut self,
2194 expression: &'a UntypedExpr,
2195 siblings: usize,
2196 ) -> Document<'a> {
2197 // If there's more than one item in the comma separated list and there's a
2198 // pipeline or long binary chain, we want to indent those to make it
2199 // easier to tell where one item ends and the other starts.
2200 // Othewise we just print the expression as a normal expr.
2201 match expression {
2202 UntypedExpr::BinOp {
2203 name, left, right, ..
2204 } if siblings > 1 => {
2205 let comments = self.pop_comments(expression.start_byte_index());
2206 let doc = self.bin_op(name, left, right, true).group();
2207 commented(doc, comments)
2208 }
2209 UntypedExpr::PipeLine { expressions } if siblings > 1 => {
2210 let comments = self.pop_comments(expression.start_byte_index());
2211 let doc = self.pipeline(expressions, true).group();
2212 commented(doc, comments)
2213 }
2214 _ => self.expr(expression).group(),
2215 }
2216 }
2217
2218 fn pattern<'a>(&mut self, pattern: &'a UntypedPattern) -> Document<'a> {
2219 let comments = self.pop_comments(pattern.location().start);
2220 let doc = match pattern {
2221 Pattern::Int { value, .. } => self.int(value),
2222
2223 Pattern::Float { value, .. } => self.float(value),
2224
2225 Pattern::String { value, .. } => self.string(value),
2226
2227 Pattern::Variable { name, .. } => name.to_doc(),
2228
2229 Pattern::BitArraySize(size) => self.bit_array_size(size),
2230
2231 Pattern::Assign { name, pattern, .. } => {
2232 self.pattern(pattern).append(" as ").append(name.as_str())
2233 }
2234
2235 Pattern::Discard { name, .. } => name.to_doc(),
2236
2237 Pattern::List { elements, tail, .. } => self.list_pattern(elements, tail),
2238
2239 Pattern::Constructor {
2240 name,
2241 arguments: args,
2242 module,
2243 spread,
2244 location,
2245 ..
2246 } => self.pattern_constructor(name, args, module, *spread, location),
2247
2248 Pattern::Tuple {
2249 elements, location, ..
2250 } => {
2251 let args = elements
2252 .iter()
2253 .map(|element| self.pattern(element))
2254 .collect_vec();
2255 "#".to_doc()
2256 .append(self.wrap_args(args, location.end))
2257 .group()
2258 }
2259
2260 Pattern::BitArray {
2261 segments, location, ..
2262 } => {
2263 let segment_docs = segments
2264 .iter()
2265 .map(|segment| bit_array_segment(segment, |pattern| self.pattern(pattern)))
2266 .collect_vec();
2267
2268 self.bit_array(segment_docs, false, location)
2269 }
2270
2271 Pattern::StringPrefix {
2272 left_side_string: left,
2273 right_side_assignment: right,
2274 left_side_assignment: left_assign,
2275 ..
2276 } => {
2277 let left = self.string(left);
2278 let right = match right {
2279 AssignName::Variable(name) => name.to_doc(),
2280 AssignName::Discard(name) => name.to_doc(),
2281 };
2282 match left_assign {
2283 Some((name, _)) => docvec![left, " as ", name, " <> ", right],
2284 None => docvec![left, " <> ", right],
2285 }
2286 }
2287
2288 Pattern::Invalid { .. } => panic!("invalid patterns can not be in an untyped ast"),
2289 };
2290 commented(doc, comments)
2291 }
2292
2293 fn bit_array_size<'a>(&mut self, size: &'a BitArraySize<()>) -> Document<'a> {
2294 match size {
2295 BitArraySize::Int { value, .. } => self.int(value),
2296 BitArraySize::Variable { name, .. } => name.to_doc(),
2297 BitArraySize::BinaryOperator {
2298 left,
2299 right,
2300 operator,
2301 ..
2302 } => {
2303 let operator = match operator {
2304 IntegerOperator::Add => " + ",
2305 IntegerOperator::Subtract => " - ",
2306 IntegerOperator::Multiply => " * ",
2307 IntegerOperator::Divide => " / ",
2308 IntegerOperator::Remainder => " % ",
2309 };
2310
2311 docvec![
2312 self.bit_array_size(left),
2313 operator,
2314 self.bit_array_size(right)
2315 ]
2316 }
2317 }
2318 }
2319
2320 fn list_pattern<'a>(
2321 &mut self,
2322 elements: &'a [UntypedPattern],
2323 tail: &'a Option<Box<UntypedPattern>>,
2324 ) -> Document<'a> {
2325 if elements.is_empty() {
2326 return match tail {
2327 Some(tail) => self.pattern(tail),
2328 None => "[]".to_doc(),
2329 };
2330 }
2331 let elements = join(
2332 elements.iter().map(|element| self.pattern(element)),
2333 break_(",", ", "),
2334 );
2335 let doc = break_("[", "[").append(elements);
2336 match tail {
2337 None => doc.nest(INDENT).append(break_(",", "")),
2338
2339 Some(tail) => {
2340 let comments = self.pop_comments(tail.location().start);
2341 let tail = if tail.is_discard() {
2342 "..".to_doc()
2343 } else {
2344 docvec!["..", self.pattern(tail)]
2345 };
2346 let tail = commented(tail, comments);
2347 doc.append(break_(",", ", "))
2348 .append(tail)
2349 .nest(INDENT)
2350 .append(break_("", ""))
2351 }
2352 }
2353 .append("]")
2354 .group()
2355 }
2356
2357 fn pattern_call_arg<'a>(&mut self, arg: &'a CallArg<UntypedPattern>) -> Document<'a> {
2358 self.format_call_arg(arg, pattern_call_arg_formatting, |this, value| {
2359 this.pattern(value)
2360 })
2361 }
2362
2363 pub fn clause_guard_bin_op<'a>(
2364 &mut self,
2365 name: &'a BinOp,
2366 left: &'a UntypedClauseGuard,
2367 right: &'a UntypedClauseGuard,
2368 ) -> Document<'a> {
2369 self.clause_guard_bin_op_side(name, left, left.precedence())
2370 .append(break_("", " "))
2371 .append(name.to_doc())
2372 .append(" ")
2373 .append(self.clause_guard_bin_op_side(name, right, right.precedence() - 1))
2374 }
2375
2376 fn clause_guard_bin_op_side<'a>(
2377 &mut self,
2378 name: &BinOp,
2379 side: &'a UntypedClauseGuard,
2380 // As opposed to `bin_op_side`, here we take the side precedence as an
2381 // argument instead of computing it ourselves. That's because
2382 // `clause_guard_bin_op` will reduce the precedence of any right side to
2383 // make sure the formatter doesn't remove any needed curly bracket.
2384 side_precedence: u8,
2385 ) -> Document<'a> {
2386 let side_doc = self.clause_guard(side);
2387 match side.bin_op_name() {
2388 // In case the other side is a binary operation as well and it can
2389 // be grouped together with the current binary operation, the two
2390 // docs are simply concatenated, so that they will end up in the
2391 // same group and the formatter will try to keep those on a single
2392 // line.
2393 Some(side_name) if side_name.can_be_grouped_with(name) => {
2394 self.operator_side(side_doc, name.precedence(), side_precedence)
2395 }
2396 // In case the binary operations cannot be grouped together the
2397 // other side is treated as a group on its own so that it can be
2398 // broken independently of other pieces of the binary operations
2399 // chain.
2400 _ => self.operator_side(side_doc.group(), name.precedence(), side_precedence),
2401 }
2402 }
2403
2404 fn clause_guard<'a>(&mut self, clause_guard: &'a UntypedClauseGuard) -> Document<'a> {
2405 match clause_guard {
2406 ClauseGuard::And { left, right, .. } => {
2407 self.clause_guard_bin_op(&BinOp::And, left, right)
2408 }
2409 ClauseGuard::Or { left, right, .. } => {
2410 self.clause_guard_bin_op(&BinOp::Or, left, right)
2411 }
2412 ClauseGuard::Equals { left, right, .. } => {
2413 self.clause_guard_bin_op(&BinOp::Eq, left, right)
2414 }
2415 ClauseGuard::NotEquals { left, right, .. } => {
2416 self.clause_guard_bin_op(&BinOp::NotEq, left, right)
2417 }
2418 ClauseGuard::GtInt { left, right, .. } => {
2419 self.clause_guard_bin_op(&BinOp::GtInt, left, right)
2420 }
2421 ClauseGuard::GtEqInt { left, right, .. } => {
2422 self.clause_guard_bin_op(&BinOp::GtEqInt, left, right)
2423 }
2424 ClauseGuard::LtInt { left, right, .. } => {
2425 self.clause_guard_bin_op(&BinOp::LtInt, left, right)
2426 }
2427 ClauseGuard::LtEqInt { left, right, .. } => {
2428 self.clause_guard_bin_op(&BinOp::LtEqInt, left, right)
2429 }
2430 ClauseGuard::GtFloat { left, right, .. } => {
2431 self.clause_guard_bin_op(&BinOp::GtFloat, left, right)
2432 }
2433 ClauseGuard::GtEqFloat { left, right, .. } => {
2434 self.clause_guard_bin_op(&BinOp::GtEqFloat, left, right)
2435 }
2436 ClauseGuard::LtFloat { left, right, .. } => {
2437 self.clause_guard_bin_op(&BinOp::LtFloat, left, right)
2438 }
2439 ClauseGuard::LtEqFloat { left, right, .. } => {
2440 self.clause_guard_bin_op(&BinOp::LtEqFloat, left, right)
2441 }
2442 ClauseGuard::AddInt { left, right, .. } => {
2443 self.clause_guard_bin_op(&BinOp::AddInt, left, right)
2444 }
2445 ClauseGuard::AddFloat { left, right, .. } => {
2446 self.clause_guard_bin_op(&BinOp::AddFloat, left, right)
2447 }
2448 ClauseGuard::SubInt { left, right, .. } => {
2449 self.clause_guard_bin_op(&BinOp::SubInt, left, right)
2450 }
2451 ClauseGuard::SubFloat { left, right, .. } => {
2452 self.clause_guard_bin_op(&BinOp::SubFloat, left, right)
2453 }
2454 ClauseGuard::MultInt { left, right, .. } => {
2455 self.clause_guard_bin_op(&BinOp::MultInt, left, right)
2456 }
2457 ClauseGuard::MultFloat { left, right, .. } => {
2458 self.clause_guard_bin_op(&BinOp::MultFloat, left, right)
2459 }
2460 ClauseGuard::DivInt { left, right, .. } => {
2461 self.clause_guard_bin_op(&BinOp::DivInt, left, right)
2462 }
2463 ClauseGuard::DivFloat { left, right, .. } => {
2464 self.clause_guard_bin_op(&BinOp::DivFloat, left, right)
2465 }
2466 ClauseGuard::RemainderInt { left, right, .. } => {
2467 self.clause_guard_bin_op(&BinOp::RemainderInt, left, right)
2468 }
2469
2470 ClauseGuard::Var { name, .. } => name.to_doc(),
2471
2472 ClauseGuard::TupleIndex { tuple, index, .. } => {
2473 self.clause_guard(tuple).append(".").append(*index).to_doc()
2474 }
2475
2476 ClauseGuard::FieldAccess {
2477 container, label, ..
2478 } => self
2479 .clause_guard(container)
2480 .append(".")
2481 .append(label)
2482 .to_doc(),
2483
2484 ClauseGuard::ModuleSelect {
2485 module_name, label, ..
2486 } => module_name.to_doc().append(".").append(label).to_doc(),
2487
2488 ClauseGuard::Constant(constant) => self.const_expr(constant),
2489
2490 ClauseGuard::Not { expression, .. } => docvec!["!", self.clause_guard(expression)],
2491 }
2492 }
2493
2494 fn constant_call_arg<'a, A, B>(&mut self, arg: &'a CallArg<Constant<A, B>>) -> Document<'a> {
2495 self.format_call_arg(arg, constant_call_arg_formatting, |this, value| {
2496 this.const_expr(value)
2497 })
2498 }
2499
2500 fn negate_bool<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> {
2501 match expr {
2502 UntypedExpr::BinOp { .. } => "!".to_doc().append(wrap_block(self.expr(expr))),
2503 _ => docvec!["!", self.expr(expr)],
2504 }
2505 }
2506
2507 fn negate_int<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> {
2508 match expr {
2509 UntypedExpr::BinOp { .. } | UntypedExpr::NegateInt { .. } => {
2510 "- ".to_doc().append(self.expr(expr))
2511 }
2512
2513 _ => docvec!["-", self.expr(expr)],
2514 }
2515 }
2516
2517 fn use_<'a>(&mut self, use_: &'a UntypedUse) -> Document<'a> {
2518 let comments = self.pop_comments(use_.location.start);
2519
2520 let call = if use_.call.is_call() {
2521 docvec![" ", self.expr(&use_.call)]
2522 } else {
2523 docvec![break_("", " "), self.expr(&use_.call)].nest(INDENT)
2524 }
2525 .group();
2526
2527 let doc = if use_.assignments.is_empty() {
2528 docvec!["use <-", call]
2529 } else {
2530 let assignments = use_.assignments.iter().map(|use_assignment| {
2531 let pattern = self.pattern(&use_assignment.pattern);
2532 let annotation = use_assignment
2533 .annotation
2534 .as_ref()
2535 .map(|a| ": ".to_doc().append(self.type_ast(a)));
2536
2537 pattern.append(annotation).group()
2538 });
2539 let assignments = Itertools::intersperse(assignments, break_(",", ", "));
2540 let left = ["use".to_doc(), break_("", " ")]
2541 .into_iter()
2542 .chain(assignments);
2543 let left = concat(left).nest(INDENT).append(break_("", " ")).group();
2544 docvec![left, "<-", call].group()
2545 };
2546
2547 commented(doc, comments)
2548 }
2549
2550 fn assert<'a>(&mut self, assert: &'a UntypedAssert) -> Document<'a> {
2551 let expression = if assert.value.is_binop() || assert.value.is_pipeline() {
2552 self.expr(&assert.value).nest(INDENT)
2553 } else {
2554 self.expr(&assert.value)
2555 };
2556
2557 let doc = self.append_as_message(expression, assert.message.as_ref());
2558 docvec!["assert ", doc]
2559 }
2560
2561 fn bit_array<'a>(
2562 &mut self,
2563 segments: Vec<Document<'a>>,
2564 can_have_multiple_per_line: bool,
2565 location: &SrcSpan,
2566 ) -> Document<'a> {
2567 let comments = self.pop_comments(location.end);
2568 let comments_doc = printed_comments(comments, false);
2569
2570 // Avoid adding illegal comma in empty bit array by explicitly handling it
2571 if segments.is_empty() {
2572 // We take all comments that come _before_ the end of the bit array,
2573 // that is all comments that are inside "<<" and ">>", if there's
2574 // any comment we want to put it inside the empty bit array!
2575 // Refer to the `list` function for a similar procedure.
2576 return match comments_doc {
2577 None => "<<>>".to_doc(),
2578 Some(comments) => "<<"
2579 .to_doc()
2580 .append(break_("", "").nest(INDENT))
2581 .append(comments)
2582 .append(break_("", ""))
2583 .append(">>")
2584 // vvv We want to make sure the comments are on a separate
2585 // line from the opening and closing angle brackets so
2586 // we force the breaks to be split on newlines.
2587 .force_break(),
2588 };
2589 }
2590 let comma = if can_have_multiple_per_line {
2591 flex_break(",", ", ")
2592 } else {
2593 break_(",", ", ")
2594 };
2595
2596 let last_break = break_(",", "");
2597 let doc = break_("<<", "<<")
2598 .append(join(segments, comma))
2599 .nest(INDENT);
2600
2601 match comments_doc {
2602 None => doc.append(last_break).append(">>").group(),
2603 Some(comments) => doc
2604 .append(last_break.nest(INDENT))
2605 // ^ Notice how in this case we nest the final break before
2606 // adding it: this way the comments are going to be as
2607 // indented as the bit array items.
2608 .append(comments.nest(INDENT))
2609 .append(line())
2610 .append(">>")
2611 .force_break()
2612 .group(),
2613 }
2614 }
2615
2616 fn bit_array_segment_expr<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> {
2617 match expr {
2618 UntypedExpr::Placeholder { .. } => panic!("Placeholders should not be formatted"),
2619
2620 UntypedExpr::BinOp { .. } => wrap_block(self.expr(expr)),
2621
2622 UntypedExpr::Int { .. }
2623 | UntypedExpr::Float { .. }
2624 | UntypedExpr::String { .. }
2625 | UntypedExpr::Var { .. }
2626 | UntypedExpr::Fn { .. }
2627 | UntypedExpr::List { .. }
2628 | UntypedExpr::Call { .. }
2629 | UntypedExpr::PipeLine { .. }
2630 | UntypedExpr::Case { .. }
2631 | UntypedExpr::FieldAccess { .. }
2632 | UntypedExpr::Tuple { .. }
2633 | UntypedExpr::TupleIndex { .. }
2634 | UntypedExpr::Todo { .. }
2635 | UntypedExpr::Panic { .. }
2636 | UntypedExpr::Echo { .. }
2637 | UntypedExpr::BitArray { .. }
2638 | UntypedExpr::RecordUpdate { .. }
2639 | UntypedExpr::NegateBool { .. }
2640 | UntypedExpr::NegateInt { .. }
2641 | UntypedExpr::Block { .. } => self.expr(expr),
2642 }
2643 }
2644
2645 fn statement<'a>(&mut self, statement: &'a UntypedStatement) -> Document<'a> {
2646 match statement {
2647 Statement::Expression(expression) => self.expr(expression),
2648 Statement::Assignment(assignment) => self.assignment(assignment),
2649 Statement::Use(use_) => self.use_(use_),
2650 Statement::Assert(assert) => self.assert(assert),
2651 }
2652 }
2653
2654 fn block<'a>(
2655 &mut self,
2656 location: &SrcSpan,
2657 statements: &'a Vec1<UntypedStatement>,
2658 force_breaks: bool,
2659 ) -> Document<'a> {
2660 let statements_doc = docvec![break_("", " "), self.statements(statements)].nest(INDENT);
2661 let trailing_comments = self.pop_comments(location.end);
2662 let trailing_comments = printed_comments(trailing_comments, false);
2663 let block_doc = match trailing_comments {
2664 Some(trailing_comments_doc) => docvec![
2665 "{",
2666 statements_doc,
2667 line().nest(INDENT),
2668 trailing_comments_doc.nest(INDENT),
2669 line(),
2670 "}"
2671 ]
2672 .force_break(),
2673 None => docvec!["{", statements_doc, break_("", " "), "}"],
2674 };
2675
2676 if force_breaks {
2677 block_doc.force_break().group()
2678 } else {
2679 block_doc.group()
2680 }
2681 }
2682
2683 pub fn wrap_function_call_args<'a, I>(&mut self, args: I, location: &SrcSpan) -> Document<'a>
2684 where
2685 I: IntoIterator<Item = Document<'a>>,
2686 {
2687 let mut args = args.into_iter().peekable();
2688 if args.peek().is_none() {
2689 return "()".to_doc();
2690 }
2691
2692 let args_doc = break_("", "")
2693 .append(join(args, break_(",", ", ")))
2694 .nest_if_broken(INDENT);
2695
2696 // We get all remaining comments that come before the call's closing
2697 // parenthesis.
2698 // If there's any we add those before the closing parenthesis instead
2699 // of moving those out of the call.
2700 // Otherwise those would be moved out of the call.
2701 let comments = self.pop_comments(location.end);
2702 let closing_parens = match printed_comments(comments, false) {
2703 None => docvec![break_(",", ""), ")"],
2704 Some(comment) => {
2705 docvec![break_(",", "").nest(INDENT), comment, line(), ")"].force_break()
2706 }
2707 };
2708
2709 "(".to_doc().append(args_doc).append(closing_parens).group()
2710 }
2711
2712 pub fn wrap_args<'a, I>(&mut self, args: I, comments_limit: u32) -> Document<'a>
2713 where
2714 I: IntoIterator<Item = Document<'a>>,
2715 {
2716 let mut args = args.into_iter().peekable();
2717 if args.peek().is_none() {
2718 let comments = self.pop_comments(comments_limit);
2719 return match printed_comments(comments, false) {
2720 Some(comments) => "("
2721 .to_doc()
2722 .append(break_("", ""))
2723 .append(comments)
2724 .nest_if_broken(INDENT)
2725 .force_break()
2726 .append(break_("", ""))
2727 .append(")"),
2728 None => "()".to_doc(),
2729 };
2730 }
2731 let doc = break_("(", "(").append(join(args, break_(",", ", ")));
2732
2733 // Include trailing comments if there are any
2734 let comments = self.pop_comments(comments_limit);
2735 match printed_comments(comments, false) {
2736 Some(comments) => doc
2737 .append(break_(",", ""))
2738 .append(comments)
2739 .nest_if_broken(INDENT)
2740 .force_break()
2741 .append(break_("", ""))
2742 .append(")"),
2743 None => doc
2744 .nest_if_broken(INDENT)
2745 .append(break_(",", ""))
2746 .append(")"),
2747 }
2748 }
2749
2750 pub fn wrap_args_with_spread<'a, I>(&mut self, args: I, comments_limit: u32) -> Document<'a>
2751 where
2752 I: IntoIterator<Item = Document<'a>>,
2753 {
2754 let mut args = args.into_iter().peekable();
2755 if args.peek().is_none() {
2756 return self.wrap_args(args, comments_limit);
2757 }
2758 let doc = break_("(", "(")
2759 .append(join(args, break_(",", ", ")))
2760 .append(break_(",", ", "))
2761 .append("..");
2762
2763 // Include trailing comments if there are any
2764 let comments = self.pop_comments(comments_limit);
2765 match printed_comments(comments, false) {
2766 Some(comments) => doc
2767 .append(break_(",", ""))
2768 .append(comments)
2769 .nest_if_broken(INDENT)
2770 .force_break()
2771 .append(break_("", ""))
2772 .append(")"),
2773 None => doc
2774 .nest_if_broken(INDENT)
2775 .append(break_(",", ""))
2776 .append(")"),
2777 }
2778 }
2779
2780 /// Given some regular comments it pretty prints those with any respective
2781 /// doc comment that might be preceding those.
2782 /// For example:
2783 ///
2784 /// ```gleam
2785 /// /// Doc
2786 /// // comment
2787 ///
2788 /// /// Doc
2789 /// pub fn wibble() {}
2790 /// ```
2791 ///
2792 /// We don't want the first doc comment to be merged together with
2793 /// `wibble`'s doc comment, so when we run into comments like `// comment`
2794 /// we need to first print all documentation comments that come before it.
2795 ///
2796 fn printed_documented_comments<'a, 'b>(
2797 &mut self,
2798 comments: impl IntoIterator<Item = (u32, Option<&'b str>)>,
2799 ) -> Option<Document<'a>> {
2800 let mut comments = comments.into_iter().peekable();
2801 let _ = comments.peek()?;
2802
2803 let mut doc = Vec::new();
2804 while let Some(c) = comments.next() {
2805 let (is_doc_commented, c) = match c {
2806 (comment_start, Some(c)) => {
2807 let doc_comment = self.doc_comments(comment_start);
2808 let is_doc_commented = !doc_comment.is_empty();
2809 doc.push(doc_comment);
2810 (is_doc_commented, c)
2811 }
2812 (_, None) => continue,
2813 };
2814 doc.push("//".to_doc().append(EcoString::from(c)));
2815 match comments.peek() {
2816 // Next line is a comment
2817 Some((_, Some(_))) => doc.push(line()),
2818 // Next line is empty
2819 Some((_, None)) => {
2820 let _ = comments.next();
2821 doc.push(lines(2));
2822 }
2823 // We've reached the end, there are no more lines
2824 None => {
2825 if is_doc_commented {
2826 doc.push(lines(2));
2827 } else {
2828 doc.push(line());
2829 }
2830 }
2831 }
2832 }
2833 let doc = concat(doc);
2834 Some(doc.force_break())
2835 }
2836
2837 fn append_as_message<'a>(
2838 &mut self,
2839 doc: Document<'a>,
2840 message: Option<&'a UntypedExpr>,
2841 ) -> Document<'a> {
2842 let Some(message) = message else { return doc };
2843
2844 docvec![
2845 doc.group(),
2846 break_("", " ").nest(INDENT),
2847 "as ",
2848 self.expr(message).group().nest(INDENT)
2849 ]
2850 .group()
2851 }
2852
2853 fn echo<'a>(
2854 &mut self,
2855 expression: &'a Option<Box<UntypedExpr>>,
2856 message: &'a Option<Box<UntypedExpr>>,
2857 ) -> Document<'a> {
2858 let Some(expression) = expression else {
2859 return self.append_as_message("echo".to_doc(), message.as_deref());
2860 };
2861
2862 // When a binary expression gets broken on multiple lines we don't want
2863 // it to be on the same line as echo, or it would look confusing;
2864 // instead it's nested onto a new line:
2865 //
2866 // ```gleam
2867 // echo first
2868 // |> wobble
2869 // |> wibble
2870 // ```
2871 //
2872 // So it's easier to see echo is printing the whole thing. Otherwise,
2873 // it would look like echo is printing just the first item:
2874 //
2875 // ```gleam
2876 // echo first
2877 // |> wobble
2878 // |> wibble
2879 // ```
2880 //
2881 let doc = self.expr(expression);
2882 if expression.is_binop() || expression.is_pipeline() {
2883 let doc = self.append_as_message(doc.nest(INDENT), message.as_deref());
2884 docvec!["echo ", doc]
2885 } else {
2886 docvec!["echo ", self.append_as_message(doc, message.as_deref())]
2887 }
2888 }
2889}
2890
2891fn init_and_last<T>(vec: &[T]) -> Option<(&[T], &T)> {
2892 match vec {
2893 [] => None,
2894 _ => match vec.split_at(vec.len() - 1) {
2895 (init, [last]) => Some((init, last)),
2896 _ => panic!("unreachable"),
2897 },
2898 }
2899}
2900
2901impl<'a> Documentable<'a> for &'a ArgNames {
2902 fn to_doc(self) -> Document<'a> {
2903 match self {
2904 ArgNames::Named { name, .. } | ArgNames::Discard { name, .. } => name.to_doc(),
2905 ArgNames::LabelledDiscard { label, name, .. }
2906 | ArgNames::NamedLabelled { label, name, .. } => {
2907 docvec![label, " ", name]
2908 }
2909 }
2910 }
2911}
2912
2913fn pub_(publicity: Publicity) -> Document<'static> {
2914 match publicity {
2915 Publicity::Public | Publicity::Internal { .. } => "pub ".to_doc(),
2916 Publicity::Private => nil(),
2917 }
2918}
2919
2920impl<'a> Documentable<'a> for &'a UnqualifiedImport {
2921 fn to_doc(self) -> Document<'a> {
2922 self.name.as_str().to_doc().append(match &self.as_name {
2923 None => nil(),
2924 Some(s) => " as ".to_doc().append(s.as_str()),
2925 })
2926 }
2927}
2928
2929impl<'a> Documentable<'a> for &'a BinOp {
2930 fn to_doc(self) -> Document<'a> {
2931 match self {
2932 BinOp::And => "&&",
2933 BinOp::Or => "||",
2934 BinOp::LtInt => "<",
2935 BinOp::LtEqInt => "<=",
2936 BinOp::LtFloat => "<.",
2937 BinOp::LtEqFloat => "<=.",
2938 BinOp::Eq => "==",
2939 BinOp::NotEq => "!=",
2940 BinOp::GtEqInt => ">=",
2941 BinOp::GtInt => ">",
2942 BinOp::GtEqFloat => ">=.",
2943 BinOp::GtFloat => ">.",
2944 BinOp::AddInt => "+",
2945 BinOp::AddFloat => "+.",
2946 BinOp::SubInt => "-",
2947 BinOp::SubFloat => "-.",
2948 BinOp::MultInt => "*",
2949 BinOp::MultFloat => "*.",
2950 BinOp::DivInt => "/",
2951 BinOp::DivFloat => "/.",
2952 BinOp::RemainderInt => "%",
2953 BinOp::Concatenate => "<>",
2954 }
2955 .to_doc()
2956 }
2957}
2958
2959#[allow(clippy::enum_variant_names)]
2960enum ListItemsPacking {
2961 /// Try and fit everything on a single line; if the items don't fit, break
2962 /// the list putting each item into its own line.
2963 ///
2964 /// ```gleam
2965 /// // unbroken
2966 /// [1, 2, 3]
2967 ///
2968 /// // broken
2969 /// [
2970 /// 1,
2971 /// 2,
2972 /// 3,
2973 /// ]
2974 /// ```
2975 ///
2976 FitOnePerLine,
2977
2978 /// Try and fit everything on a single line; if the items don't fit, break
2979 /// the list putting as many items as possible in a single line.
2980 ///
2981 /// ```gleam
2982 /// // unbroken
2983 /// [1, 2, 3]
2984 ///
2985 /// // broken
2986 /// [
2987 /// 1, 2, 3, ...
2988 /// 4, 100,
2989 /// ]
2990 /// ```
2991 ///
2992 FitMultiplePerLine,
2993
2994 /// Always break the list, putting each item into its own line:
2995 ///
2996 /// ```gleam
2997 /// [
2998 /// 1,
2999 /// 2,
3000 /// 3,
3001 /// ]
3002 /// ```
3003 ///
3004 BreakOnePerLine,
3005}
3006
3007pub fn break_block(doc: Document<'_>) -> Document<'_> {
3008 "{".to_doc()
3009 .append(line().append(doc).nest(INDENT))
3010 .append(line())
3011 .append("}")
3012 .force_break()
3013}
3014
3015pub fn wrap_block(doc: Document<'_>) -> Document<'_> {
3016 break_("{", "{ ")
3017 .append(doc)
3018 .nest(INDENT)
3019 .append(break_("", " "))
3020 .append("}")
3021}
3022
3023fn printed_comments<'a, 'comments>(
3024 comments: impl IntoIterator<Item = Option<&'comments str>>,
3025 trailing_newline: bool,
3026) -> Option<Document<'a>> {
3027 let mut comments = comments.into_iter().peekable();
3028 let _ = comments.peek()?;
3029
3030 let mut doc = Vec::new();
3031 while let Some(c) = comments.next() {
3032 let c = match c {
3033 Some(c) => c,
3034 None => continue,
3035 };
3036 doc.push("//".to_doc().append(EcoString::from(c)));
3037 match comments.peek() {
3038 // Next line is a comment
3039 Some(Some(_)) => doc.push(line()),
3040 // Next line is empty
3041 Some(None) => {
3042 let _ = comments.next();
3043 match comments.peek() {
3044 Some(_) => doc.push(lines(2)),
3045 None => {
3046 if trailing_newline {
3047 doc.push(lines(2));
3048 }
3049 }
3050 }
3051 }
3052 // We've reached the end, there are no more lines
3053 None => {
3054 if trailing_newline {
3055 doc.push(line());
3056 }
3057 }
3058 }
3059 }
3060 let doc = concat(doc);
3061 if trailing_newline {
3062 Some(doc.force_break())
3063 } else {
3064 Some(doc)
3065 }
3066}
3067
3068fn commented<'a, 'comments>(
3069 doc: Document<'a>,
3070 comments: impl IntoIterator<Item = Option<&'comments str>>,
3071) -> Document<'a> {
3072 match printed_comments(comments, true) {
3073 Some(comments) => comments.append(doc.group()),
3074 None => doc,
3075 }
3076}
3077
3078fn bit_array_segment<Value, Type, ToDoc>(
3079 segment: &BitArraySegment<Value, Type>,
3080 mut to_doc: ToDoc,
3081) -> Document<'_>
3082where
3083 ToDoc: FnMut(&Value) -> Document<'_>,
3084{
3085 match segment {
3086 BitArraySegment { value, options, .. } if options.is_empty() => to_doc(value),
3087
3088 BitArraySegment { value, options, .. } => to_doc(value).append(":").append(join(
3089 options
3090 .iter()
3091 .map(|option| segment_option(option, |value| to_doc(value))),
3092 "-".to_doc(),
3093 )),
3094 }
3095}
3096
3097fn segment_option<ToDoc, Value>(option: &BitArrayOption<Value>, mut to_doc: ToDoc) -> Document<'_>
3098where
3099 ToDoc: FnMut(&Value) -> Document<'_>,
3100{
3101 match option {
3102 BitArrayOption::Bytes { .. } => "bytes".to_doc(),
3103 BitArrayOption::Bits { .. } => "bits".to_doc(),
3104 BitArrayOption::Int { .. } => "int".to_doc(),
3105 BitArrayOption::Float { .. } => "float".to_doc(),
3106 BitArrayOption::Utf8 { .. } => "utf8".to_doc(),
3107 BitArrayOption::Utf16 { .. } => "utf16".to_doc(),
3108 BitArrayOption::Utf32 { .. } => "utf32".to_doc(),
3109 BitArrayOption::Utf8Codepoint { .. } => "utf8_codepoint".to_doc(),
3110 BitArrayOption::Utf16Codepoint { .. } => "utf16_codepoint".to_doc(),
3111 BitArrayOption::Utf32Codepoint { .. } => "utf32_codepoint".to_doc(),
3112 BitArrayOption::Signed { .. } => "signed".to_doc(),
3113 BitArrayOption::Unsigned { .. } => "unsigned".to_doc(),
3114 BitArrayOption::Big { .. } => "big".to_doc(),
3115 BitArrayOption::Little { .. } => "little".to_doc(),
3116 BitArrayOption::Native { .. } => "native".to_doc(),
3117
3118 BitArrayOption::Size {
3119 value,
3120 short_form: false,
3121 ..
3122 } => "size"
3123 .to_doc()
3124 .append("(")
3125 .append(to_doc(value))
3126 .append(")"),
3127
3128 BitArrayOption::Size {
3129 value,
3130 short_form: true,
3131 ..
3132 } => to_doc(value),
3133
3134 BitArrayOption::Unit { value, .. } => "unit"
3135 .to_doc()
3136 .append("(")
3137 .append(eco_format!("{value}"))
3138 .append(")"),
3139 }
3140}
3141
3142pub fn comments_before<'a>(
3143 comments: &'a [Comment<'a>],
3144 empty_lines: &'a [u32],
3145 limit: u32,
3146 retain_empty_lines: bool,
3147) -> (
3148 impl Iterator<Item = (u32, Option<&'a str>)>,
3149 &'a [Comment<'a>],
3150 &'a [u32],
3151) {
3152 let end_comments = comments
3153 .iter()
3154 .position(|c| c.start > limit)
3155 .unwrap_or(comments.len());
3156 let end_empty_lines = empty_lines
3157 .iter()
3158 .position(|l| *l > limit)
3159 .unwrap_or(empty_lines.len());
3160 let popped_comments = comments
3161 .get(0..end_comments)
3162 .expect("0..end_comments is guaranteed to be in bounds")
3163 .iter()
3164 .map(|c| (c.start, Some(c.content)));
3165 let popped_empty_lines = if retain_empty_lines { empty_lines } else { &[] }
3166 .get(0..end_empty_lines)
3167 .unwrap_or(&[])
3168 .iter()
3169 .map(|i| (i, i))
3170 // compact consecutive empty lines into a single line
3171 .coalesce(|(a_start, a_end), (b_start, b_end)| {
3172 if *a_end + 1 == *b_start {
3173 Ok((a_start, b_end))
3174 } else {
3175 Err(((a_start, a_end), (b_start, b_end)))
3176 }
3177 })
3178 .map(|l| (*l.0, None));
3179 let popped = popped_comments
3180 .merge_by(popped_empty_lines, |(a, _), (b, _)| a < b)
3181 .skip_while(|(_, comment_or_line)| comment_or_line.is_none());
3182 (
3183 popped,
3184 comments.get(end_comments..).expect("in bounds"),
3185 empty_lines.get(end_empty_lines..).expect("in bounds"),
3186 )
3187}
3188
3189fn is_breakable_argument(expr: &UntypedExpr, arity: usize) -> bool {
3190 match expr {
3191 // A call is only breakable if it is the only argument
3192 UntypedExpr::Call { .. } => arity == 1,
3193
3194 UntypedExpr::Fn { .. }
3195 | UntypedExpr::Block { .. }
3196 | UntypedExpr::Case { .. }
3197 | UntypedExpr::List { .. }
3198 | UntypedExpr::Tuple { .. }
3199 | UntypedExpr::BitArray { .. } => true,
3200 _ => false,
3201 }
3202}
3203
3204enum CallArgFormatting<'a, A> {
3205 ShorthandLabelled(&'a EcoString),
3206 Unlabelled(&'a A),
3207 Labelled(&'a EcoString, &'a A),
3208}
3209
3210fn expr_call_arg_formatting(arg: &CallArg<UntypedExpr>) -> CallArgFormatting<'_, UntypedExpr> {
3211 match arg {
3212 // An argument supplied using label shorthand syntax.
3213 _ if arg.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
3214 arg.label.as_ref().expect("label shorthand with no label"),
3215 ),
3216 // A labelled argument.
3217 CallArg {
3218 label: Some(label),
3219 value,
3220 ..
3221 } => CallArgFormatting::Labelled(label, value),
3222 // An unlabelled argument.
3223 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
3224 }
3225}
3226
3227fn pattern_call_arg_formatting(
3228 arg: &CallArg<UntypedPattern>,
3229) -> CallArgFormatting<'_, UntypedPattern> {
3230 match arg {
3231 // An argument supplied using label shorthand syntax.
3232 _ if arg.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
3233 arg.label.as_ref().expect("label shorthand with no label"),
3234 ),
3235 // A labelled argument.
3236 CallArg {
3237 label: Some(label),
3238 value,
3239 ..
3240 } => CallArgFormatting::Labelled(label, value),
3241 // An unlabelled argument.
3242 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
3243 }
3244}
3245
3246fn constant_call_arg_formatting<A, B>(
3247 arg: &CallArg<Constant<A, B>>,
3248) -> CallArgFormatting<'_, Constant<A, B>> {
3249 match arg {
3250 // An argument supplied using label shorthand syntax.
3251 _ if arg.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
3252 arg.label.as_ref().expect("label shorthand with no label"),
3253 ),
3254 // A labelled argument.
3255 CallArg {
3256 label: Some(label),
3257 value,
3258 ..
3259 } => CallArgFormatting::Labelled(label, value),
3260 // An unlabelled argument.
3261 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
3262 }
3263}
3264
3265struct AttributesPrinter<'a> {
3266 external_erlang: &'a Option<(EcoString, EcoString, SrcSpan)>,
3267 external_javascript: &'a Option<(EcoString, EcoString, SrcSpan)>,
3268 deprecation: &'a Deprecation,
3269 internal: bool,
3270}
3271
3272impl<'a> AttributesPrinter<'a> {
3273 pub fn new() -> Self {
3274 Self {
3275 external_erlang: &None,
3276 external_javascript: &None,
3277 deprecation: &Deprecation::NotDeprecated,
3278 internal: false,
3279 }
3280 }
3281
3282 pub fn set_external_erlang(
3283 mut self,
3284 external: &'a Option<(EcoString, EcoString, SrcSpan)>,
3285 ) -> Self {
3286 self.external_erlang = external;
3287 self
3288 }
3289
3290 pub fn set_external_javascript(
3291 mut self,
3292 external: &'a Option<(EcoString, EcoString, SrcSpan)>,
3293 ) -> Self {
3294 self.external_javascript = external;
3295 self
3296 }
3297
3298 pub fn set_internal(mut self, publicity: Publicity) -> Self {
3299 self.internal = publicity.is_internal();
3300 self
3301 }
3302
3303 pub fn set_deprecation(mut self, deprecation: &'a Deprecation) -> Self {
3304 self.deprecation = deprecation;
3305 self
3306 }
3307}
3308
3309impl<'a> Documentable<'a> for AttributesPrinter<'a> {
3310 fn to_doc(self) -> Document<'a> {
3311 let mut attributes = vec![];
3312
3313 // @deprecated attribute
3314 if let Deprecation::Deprecated { message } = self.deprecation {
3315 attributes.push(docvec!["@deprecated(\"", message, "\")"])
3316 };
3317
3318 // @external attributes
3319 if let Some((m, f, _)) = self.external_erlang {
3320 attributes.push(docvec!["@external(erlang, \"", m, "\", \"", f, "\")"])
3321 };
3322
3323 if let Some((m, f, _)) = self.external_javascript {
3324 attributes.push(docvec!["@external(javascript, \"", m, "\", \"", f, "\")"])
3325 };
3326
3327 // @internal attribute
3328 if self.internal {
3329 attributes.push("@internal".to_doc());
3330 };
3331
3332 if attributes.is_empty() {
3333 nil()
3334 } else {
3335 join(attributes, line()).append(line())
3336 }
3337 }
3338}