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