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