Fork of daniellemaywood.uk/gleam — Wasm codegen work
104 kB
2834 lines
1use num_bigint::BigInt;
2use vec1::Vec1;
3
4use super::{decision::ASSIGNMENT_VAR, *};
5use crate::{
6 ast::*,
7 exhaustiveness::StringEncoding,
8 line_numbers::LineNumbers,
9 pretty::*,
10 type_::{
11 ModuleValueConstructor, Type, TypedCallArg, ValueConstructor, ValueConstructorVariant,
12 },
13};
14use std::sync::Arc;
15
16#[derive(Debug, Clone)]
17pub enum Position {
18 /// We are compiling the last expression in a function, meaning that it should
19 /// use `return` to return the value it produces from the function.
20 Tail,
21 /// We are inside a function, but the value of this expression isn't being
22 /// used, so we don't need to do anything with the returned value.
23 Statement,
24 /// The value of this expression needs to be used inside another expression,
25 /// so we need to use the value that is returned by this expression.
26 Expression(Ordering),
27 /// We are compiling an expression inside a block, meaning we must assign
28 /// to the `_block` variable at the end of the scope, because blocks are not
29 /// expressions in JS.
30 /// Since JS doesn't have variable shadowing, we must store the name of the
31 /// variable being used, which will include the incrementing counter.
32 /// For example, `block$2`
33 Assign(EcoString),
34}
35
36impl Position {
37 /// Returns `true` if the position is [`Tail`].
38 ///
39 /// [`Tail`]: Position::Tail
40 #[must_use]
41 pub fn is_tail(&self) -> bool {
42 matches!(self, Self::Tail)
43 }
44
45 #[must_use]
46 pub fn ordering(&self) -> Ordering {
47 match self {
48 Self::Expression(ordering) => *ordering,
49 Self::Tail | Self::Assign(_) | Self::Statement => Ordering::Loose,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy)]
55/// Determines whether we can lift blocks into statement level instead of using
56/// immediately invoked function expressions. Consider the following piece of code:
57///
58/// ```gleam
59/// some_function(function_with_side_effect(), {
60/// let a = 10
61/// other_function_with_side_effects(a)
62/// })
63/// ```
64/// Here, if we lift the block that is the second argument of the function, we
65/// would end up running `other_function_with_side_effects` before
66/// `function_with_side_effects`. This would be invalid, as code in Gleam should be
67/// evaluated left-to-right, top-to-bottom. In this case, the ordering would be
68/// `Strict`, indicating that we cannot lift the block.
69///
70/// However, in this example:
71///
72/// ```gleam
73/// let value = !{
74/// let value = False
75/// some_function_with_side_effect()
76/// value
77/// }
78/// ```
79/// The only expression is the block, meaning it can be safely lifted without
80/// changing the evaluation order of the program. So the ordering is `Loose`.
81///
82pub enum Ordering {
83 Strict,
84 Loose,
85}
86
87/// Tracking where the current function is a module function or an anonymous function.
88#[derive(Debug)]
89enum CurrentFunction {
90 /// The current function is a module function
91 ///
92 /// ```gleam
93 /// pub fn main() -> Nil {
94 /// // we are here
95 /// }
96 /// ```
97 Module,
98
99 /// The current function is a module function, but one of its arguments shadows
100 /// the reference to itself so it cannot recurse.
101 ///
102 /// ```gleam
103 /// pub fn main(main: fn() -> Nil) -> Nil {
104 /// // we are here
105 /// }
106 /// ```
107 ModuleWithShadowingArgument,
108
109 /// The current function is an anonymous function
110 ///
111 /// ```gleam
112 /// pub fn main() -> Nil {
113 /// fn() {
114 /// // we are here
115 /// }
116 /// }
117 /// ```
118 Anonymous,
119}
120
121impl CurrentFunction {
122 #[inline]
123 fn can_recurse(&self) -> bool {
124 match self {
125 CurrentFunction::Module => true,
126 CurrentFunction::ModuleWithShadowingArgument => false,
127 CurrentFunction::Anonymous => false,
128 }
129 }
130}
131
132#[derive(Debug)]
133pub(crate) struct Generator<'module, 'ast> {
134 module_name: EcoString,
135 src_path: EcoString,
136 line_numbers: &'module LineNumbers,
137 function_name: EcoString,
138 function_arguments: Vec<Option<&'module EcoString>>,
139 current_function: CurrentFunction,
140 pub current_scope_vars: im::HashMap<EcoString, usize>,
141 pub function_position: Position,
142 pub scope_position: Position,
143 // We register whether these features are used within an expression so that
144 // the module generator can output a suitable function if it is needed.
145 pub tracker: &'module mut UsageTracker,
146 // We track whether tail call recursion is used so that we can render a loop
147 // at the top level of the function to use in place of pushing new stack
148 // frames.
149 pub tail_recursion_used: bool,
150 /// Statements to be compiled when lifting blocks into statement scope.
151 /// For example, when compiling the following code:
152 /// ```gleam
153 /// let a = {
154 /// let b = 1
155 /// b + 1
156 /// }
157 /// ```
158 /// There will be 2 items in `statement_level`: The first will be `let _block;`
159 /// The second will be the generated code for the block being assigned to `a`.
160 /// This lets use return `_block` as the value that the block evaluated to,
161 /// while still including the necessary code in the output at the right place.
162 ///
163 /// Once the `let` statement has compiled its value, it will add anything accumulated
164 /// in `statement_level` to the generated code, so it will result in:
165 ///
166 /// ```javascript
167 /// let _block;
168 /// {...}
169 /// let a = _block;
170 /// ```
171 ///
172 statement_level: Vec<Document<'ast>>,
173
174 /// This will be true if we've generated a `let assert` statement that we know
175 /// is guaranteed to throw.
176 /// This means we can stop code generation for all the following statements
177 /// in the same block!
178 pub let_assert_always_panics: bool,
179}
180
181impl<'module, 'a> Generator<'module, 'a> {
182 #[allow(clippy::too_many_arguments)] // TODO: FIXME
183 pub fn new(
184 module_name: EcoString,
185 src_path: EcoString,
186 line_numbers: &'module LineNumbers,
187 function_name: EcoString,
188 function_arguments: Vec<Option<&'module EcoString>>,
189 tracker: &'module mut UsageTracker,
190 mut current_scope_vars: im::HashMap<EcoString, usize>,
191 ) -> Self {
192 let mut current_function = CurrentFunction::Module;
193 for &name in function_arguments.iter().flatten() {
194 // Initialise the function arguments
195 let _ = current_scope_vars.insert(name.clone(), 0);
196
197 // If any of the function arguments shadow the current function then
198 // recursion is no longer possible.
199 if function_name.as_ref() == name {
200 current_function = CurrentFunction::ModuleWithShadowingArgument;
201 }
202 }
203 Self {
204 tracker,
205 module_name,
206 src_path,
207 line_numbers,
208 function_name,
209 function_arguments,
210 tail_recursion_used: false,
211 current_scope_vars,
212 current_function,
213 function_position: Position::Tail,
214 scope_position: Position::Tail,
215 statement_level: Vec::new(),
216 let_assert_always_panics: false,
217 }
218 }
219
220 pub fn local_var(&mut self, name: &EcoString) -> EcoString {
221 match self.current_scope_vars.get(name) {
222 None => {
223 let _ = self.current_scope_vars.insert(name.clone(), 0);
224 maybe_escape_identifier(name)
225 }
226 Some(0) => maybe_escape_identifier(name),
227 Some(n) if name == "$" => eco_format!("${n}"),
228 Some(n) => eco_format!("{name}${n}"),
229 }
230 }
231
232 pub fn next_local_var(&mut self, name: &EcoString) -> EcoString {
233 let next = self.current_scope_vars.get(name).map_or(0, |i| i + 1);
234 let _ = self.current_scope_vars.insert(name.clone(), next);
235 self.local_var(name)
236 }
237
238 pub fn function_body(
239 &mut self,
240 body: &'a [TypedStatement],
241 arguments: &'a [TypedArg],
242 ) -> Document<'a> {
243 let body = self.statements(body);
244 if self.tail_recursion_used {
245 self.tail_call_loop(body, arguments)
246 } else {
247 body
248 }
249 }
250
251 fn tail_call_loop(&mut self, body: Document<'a>, arguments: &'a [TypedArg]) -> Document<'a> {
252 let loop_assignments = concat(arguments.iter().flat_map(Arg::get_variable_name).map(
253 |name| {
254 let var = maybe_escape_identifier(name);
255 docvec!["let ", var, " = loop$", name, ";", line()]
256 },
257 ));
258 docvec![
259 "while (true) {",
260 docvec![line(), loop_assignments, body].nest(INDENT),
261 line(),
262 "}"
263 ]
264 }
265
266 fn statement(&mut self, statement: &'a TypedStatement) -> Document<'a> {
267 let expression_doc = match statement {
268 Statement::Expression(expression) => self.expression(expression),
269 Statement::Assignment(assignment) => self.assignment(assignment),
270 Statement::Use(use_) => self.expression(&use_.call),
271 Statement::Assert(assert) => self.assert(assert),
272 };
273 self.add_statement_level(expression_doc)
274 }
275
276 fn add_statement_level(&mut self, expression: Document<'a>) -> Document<'a> {
277 if self.statement_level.is_empty() {
278 expression
279 } else {
280 let mut statements = std::mem::take(&mut self.statement_level);
281 statements.push(expression);
282 join(statements, line())
283 }
284 }
285
286 pub fn expression(&mut self, expression: &'a TypedExpr) -> Document<'a> {
287 let document = match expression {
288 TypedExpr::String { value, .. } => string(value),
289
290 TypedExpr::Int { value, .. } => int(value),
291 TypedExpr::Float { float_value, .. } => float_from_value(float_value.value()),
292
293 TypedExpr::List { elements, tail, .. } => {
294 self.not_in_tail_position(Some(Ordering::Strict), |this| match tail {
295 Some(tail) => {
296 this.tracker.prepend_used = true;
297 let tail = this.wrap_expression(tail);
298 prepend(
299 elements.iter().map(|element| this.wrap_expression(element)),
300 tail,
301 )
302 }
303 None => {
304 this.tracker.list_used = true;
305 list(elements.iter().map(|element| this.wrap_expression(element)))
306 }
307 })
308 }
309
310 TypedExpr::Tuple { elements, .. } => self.tuple(elements),
311 TypedExpr::TupleIndex { tuple, index, .. } => self.tuple_index(tuple, *index),
312
313 TypedExpr::Case {
314 subjects,
315 clauses,
316 compiled_case,
317 ..
318 } => decision::case(compiled_case, clauses, subjects, self),
319
320 TypedExpr::Call { fun, arguments, .. } => self.call(fun, arguments),
321 TypedExpr::Fn {
322 arguments, body, ..
323 } => self.fn_(arguments, body),
324
325 TypedExpr::RecordAccess { record, label, .. } => self.record_access(record, label),
326
327 TypedExpr::PositionalAccess { record, index, .. } => {
328 self.positional_access(record, *index)
329 }
330
331 TypedExpr::RecordUpdate {
332 record_assignment,
333 constructor,
334 arguments,
335 ..
336 } => self.record_update(record_assignment, constructor, arguments),
337
338 TypedExpr::Var {
339 name, constructor, ..
340 } => self.variable(name, constructor),
341
342 TypedExpr::Pipeline {
343 first_value,
344 assignments,
345 finally,
346 ..
347 } => self.pipeline(first_value, assignments.as_slice(), finally),
348
349 TypedExpr::Block { statements, .. } => self.block(statements),
350
351 TypedExpr::BinOp {
352 name, left, right, ..
353 } => self.bin_op(name, left, right),
354
355 TypedExpr::Todo {
356 message, location, ..
357 } => self.todo(message.as_ref().map(|m| &**m), location),
358
359 TypedExpr::Panic {
360 location, message, ..
361 } => self.panic(location, message.as_ref().map(|m| &**m)),
362
363 TypedExpr::BitArray { segments, .. } => self.bit_array(segments),
364
365 TypedExpr::ModuleSelect {
366 module_alias,
367 label,
368 constructor,
369 ..
370 } => self.module_select(module_alias, label, constructor),
371
372 TypedExpr::NegateBool { value, .. } => self.negate_with("!", value),
373
374 TypedExpr::NegateInt { value, .. } => self.negate_with("- ", value),
375
376 TypedExpr::Echo {
377 expression,
378 message,
379 location,
380 ..
381 } => {
382 let expression = expression
383 .as_ref()
384 .expect("echo with no expression outside of pipe");
385 let expresion_doc =
386 self.not_in_tail_position(None, |this| this.wrap_expression(expression));
387 self.echo(expresion_doc, message.as_deref(), location)
388 }
389
390 TypedExpr::Invalid { .. } => {
391 panic!("invalid expressions should not reach code generation")
392 }
393 };
394 if expression.handles_own_return() {
395 document
396 } else {
397 self.wrap_return(document)
398 }
399 }
400
401 fn negate_with(&mut self, with: &'static str, value: &'a TypedExpr) -> Document<'a> {
402 self.not_in_tail_position(None, |this| docvec![with, this.wrap_expression(value)])
403 }
404
405 fn bit_array(&mut self, segments: &'a [TypedExprBitArraySegment]) -> Document<'a> {
406 self.tracker.bit_array_literal_used = true;
407
408 // Collect all the values used in segments.
409 let segments_array = array(segments.iter().map(|segment| {
410 let value = self.not_in_tail_position(Some(Ordering::Strict), |this| {
411 this.wrap_expression(&segment.value)
412 });
413
414 let details = self.bit_array_segment_details(segment);
415
416 match details.type_ {
417 BitArraySegmentType::BitArray => {
418 if segment.size().is_some() {
419 self.tracker.bit_array_slice_used = true;
420 docvec!["bitArraySlice(", value, ", 0, ", details.size, ")"]
421 } else {
422 value
423 }
424 }
425 BitArraySegmentType::Int => match (details.size_value, segment.value.as_ref()) {
426 (Some(size_value), TypedExpr::Int { int_value, .. })
427 if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into()
428 && (&size_value % BigInt::from(8) == BigInt::ZERO) =>
429 {
430 let bytes = bit_array_segment_int_value_to_bytes(
431 int_value.clone(),
432 size_value,
433 segment.endianness(),
434 );
435
436 u8_slice(&bytes)
437 }
438
439 (Some(size_value), _) if size_value == 8.into() => value,
440
441 (Some(size_value), _) if size_value <= 0.into() => nil(),
442
443 _ => {
444 self.tracker.sized_integer_segment_used = true;
445 let size = details.size;
446 let is_big = bool(segment.endianness().is_big());
447 docvec!["sizedInt(", value, ", ", size, ", ", is_big, ")"]
448 }
449 },
450 BitArraySegmentType::Float => {
451 self.tracker.float_bit_array_segment_used = true;
452 let size = details.size;
453 let is_big = bool(details.endianness.is_big());
454 docvec!["sizedFloat(", value, ", ", size, ", ", is_big, ")"]
455 }
456 BitArraySegmentType::String(StringEncoding::Utf8) => {
457 self.tracker.string_bit_array_segment_used = true;
458 docvec!["stringBits(", value, ")"]
459 }
460 BitArraySegmentType::String(StringEncoding::Utf16) => {
461 self.tracker.string_utf16_bit_array_segment_used = true;
462 let is_big = bool(details.endianness.is_big());
463 docvec!["stringToUtf16(", value, ", ", is_big, ")"]
464 }
465 BitArraySegmentType::String(StringEncoding::Utf32) => {
466 self.tracker.string_utf32_bit_array_segment_used = true;
467 let is_big = bool(details.endianness.is_big());
468 docvec!["stringToUtf32(", value, ", ", is_big, ")"]
469 }
470 BitArraySegmentType::UtfCodepoint(StringEncoding::Utf8) => {
471 self.tracker.codepoint_bit_array_segment_used = true;
472 docvec!["codepointBits(", value, ")"]
473 }
474 BitArraySegmentType::UtfCodepoint(StringEncoding::Utf16) => {
475 self.tracker.codepoint_utf16_bit_array_segment_used = true;
476 let is_big = bool(details.endianness.is_big());
477 docvec!["codepointToUtf16(", value, ", ", is_big, ")"]
478 }
479 BitArraySegmentType::UtfCodepoint(StringEncoding::Utf32) => {
480 self.tracker.codepoint_utf32_bit_array_segment_used = true;
481 let is_big = bool(details.endianness.is_big());
482 docvec!["codepointToUtf32(", value, ", ", is_big, ")"]
483 }
484 }
485 }));
486
487 docvec!["toBitArray(", segments_array, ")"]
488 }
489
490 fn bit_array_segment_details(
491 &mut self,
492 segment: &'a TypedExprBitArraySegment,
493 ) -> BitArraySegmentDetails<'a> {
494 let size = segment.size();
495 let unit = segment.unit();
496 let (size_value, size) = match size {
497 Some(TypedExpr::Int { int_value, .. }) => {
498 let size_value = int_value * unit;
499 let size = eco_format!("{}", size_value).to_doc();
500 (Some(size_value), size)
501 }
502 Some(size) => {
503 let mut size = self.not_in_tail_position(Some(Ordering::Strict), |this| {
504 this.wrap_expression(size)
505 });
506
507 if unit != 1 {
508 size = size.group().append(" * ".to_doc().append(unit.to_doc()));
509 }
510
511 (None, size)
512 }
513
514 None => {
515 let size_value: usize = if segment.type_.is_int() { 8 } else { 64 };
516 (Some(BigInt::from(size_value)), docvec![size_value])
517 }
518 };
519
520 let type_ = BitArraySegmentType::from_segment(segment);
521
522 BitArraySegmentDetails {
523 type_,
524 size,
525 size_value,
526 endianness: segment.endianness(),
527 }
528 }
529
530 pub fn wrap_return(&mut self, document: Document<'a>) -> Document<'a> {
531 match &self.scope_position {
532 Position::Tail => docvec!["return ", document, ";"],
533 Position::Expression(_) | Position::Statement => document,
534 Position::Assign(name) => docvec![name.clone(), " = ", document, ";"],
535 }
536 }
537
538 pub fn not_in_tail_position<CompileFn, Output>(
539 &mut self,
540 // If ordering is None, it is inherited from the parent scope.
541 // It will be None in cases like `!x`, where `x` can be lifted
542 // only if the ordering is already loose.
543 ordering: Option<Ordering>,
544 compile: CompileFn,
545 ) -> Output
546 where
547 CompileFn: Fn(&mut Self) -> Output,
548 {
549 let new_ordering = ordering.unwrap_or(self.scope_position.ordering());
550
551 let function_position = std::mem::replace(
552 &mut self.function_position,
553 Position::Expression(new_ordering),
554 );
555 let scope_position =
556 std::mem::replace(&mut self.scope_position, Position::Expression(new_ordering));
557
558 let result = compile(self);
559
560 self.function_position = function_position;
561 self.scope_position = scope_position;
562 result
563 }
564
565 /// Use the `_block` variable if the expression is JS statement.
566 pub fn wrap_expression(&mut self, expression: &'a TypedExpr) -> Document<'a> {
567 match (expression, &self.scope_position) {
568 (_, Position::Tail | Position::Assign(_)) => self.expression(expression),
569 (
570 TypedExpr::Panic { .. }
571 | TypedExpr::Todo { .. }
572 | TypedExpr::Case { .. }
573 | TypedExpr::Pipeline { .. }
574 | TypedExpr::RecordUpdate {
575 // Record updates that assign a variable generate multiple statements
576 record_assignment: Some(_),
577 ..
578 },
579 Position::Expression(Ordering::Loose),
580 ) => self.wrap_block(|this| this.expression(expression)),
581 (
582 TypedExpr::Panic { .. }
583 | TypedExpr::Todo { .. }
584 | TypedExpr::Case { .. }
585 | TypedExpr::Pipeline { .. }
586 | TypedExpr::RecordUpdate {
587 // Record updates that assign a variable generate multiple statements
588 record_assignment: Some(_),
589 ..
590 },
591 Position::Expression(Ordering::Strict),
592 ) => self.immediately_invoked_function_expression(expression, |this, expr| {
593 this.expression(expr)
594 }),
595 _ => self.expression(expression),
596 }
597 }
598
599 /// Wrap an expression using the `_block` variable if required due to being
600 /// a JS statement, or in parens if required due to being an operator or
601 /// a function literal.
602 pub fn child_expression(&mut self, expression: &'a TypedExpr) -> Document<'a> {
603 match expression {
604 TypedExpr::BinOp { name, .. } if name.is_operator_to_wrap() => {}
605 TypedExpr::Fn { .. } => {}
606
607 TypedExpr::Int { .. }
608 | TypedExpr::Float { .. }
609 | TypedExpr::String { .. }
610 | TypedExpr::Block { .. }
611 | TypedExpr::Pipeline { .. }
612 | TypedExpr::Var { .. }
613 | TypedExpr::List { .. }
614 | TypedExpr::Call { .. }
615 | TypedExpr::BinOp { .. }
616 | TypedExpr::Case { .. }
617 | TypedExpr::RecordAccess { .. }
618 | TypedExpr::PositionalAccess { .. }
619 | TypedExpr::ModuleSelect { .. }
620 | TypedExpr::Tuple { .. }
621 | TypedExpr::TupleIndex { .. }
622 | TypedExpr::Todo { .. }
623 | TypedExpr::Panic { .. }
624 | TypedExpr::Echo { .. }
625 | TypedExpr::BitArray { .. }
626 | TypedExpr::RecordUpdate { .. }
627 | TypedExpr::NegateBool { .. }
628 | TypedExpr::NegateInt { .. }
629 | TypedExpr::Invalid { .. } => return self.wrap_expression(expression),
630 }
631
632 let document = self.expression(expression);
633 match &self.scope_position {
634 // Here the document is a return statement: `return <expr>;`
635 // or an assignment: `_block = <expr>;`
636 Position::Tail | Position::Assign(_) | Position::Statement => document,
637 Position::Expression(_) => docvec!["(", document, ")"],
638 }
639 }
640
641 /// Wrap an expression in an immediately invoked function expression
642 fn immediately_invoked_function_expression<T, ToDoc>(
643 &mut self,
644 statements: &'a T,
645 to_doc: ToDoc,
646 ) -> Document<'a>
647 where
648 ToDoc: FnOnce(&mut Self, &'a T) -> Document<'a>,
649 {
650 // Save initial state
651 let scope_position = std::mem::replace(&mut self.scope_position, Position::Tail);
652 let statement_level = std::mem::take(&mut self.statement_level);
653
654 // Set state for in this iife
655 let current_scope_vars = self.current_scope_vars.clone();
656
657 // Generate the expression
658 let result = to_doc(self, statements);
659 let doc = self.add_statement_level(result);
660 let doc = immediately_invoked_function_expression_document(doc);
661
662 // Reset
663 self.current_scope_vars = current_scope_vars;
664 self.scope_position = scope_position;
665 self.statement_level = statement_level;
666
667 self.wrap_return(doc)
668 }
669
670 fn wrap_block<CompileFn>(&mut self, compile: CompileFn) -> Document<'a>
671 where
672 CompileFn: Fn(&mut Self) -> Document<'a>,
673 {
674 let block_variable = self.next_local_var(&BLOCK_VARIABLE.into());
675
676 // Save initial state
677 let scope_position = std::mem::replace(
678 &mut self.scope_position,
679 Position::Assign(block_variable.clone()),
680 );
681 let function_position = std::mem::replace(
682 &mut self.function_position,
683 Position::Expression(Ordering::Strict),
684 );
685
686 // Generate the expression
687 let statement_doc = compile(self);
688
689 // Reset
690 self.scope_position = scope_position;
691 self.function_position = function_position;
692
693 self.statement_level
694 .push(docvec!["let ", block_variable.clone(), ";"]);
695 self.statement_level.push(statement_doc);
696
697 self.wrap_return(block_variable.to_doc())
698 }
699
700 fn variable(&mut self, name: &'a EcoString, constructor: &'a ValueConstructor) -> Document<'a> {
701 match &constructor.variant {
702 ValueConstructorVariant::Record { arity, .. } => {
703 let type_ = constructor.type_.clone();
704 let tracker = &mut self.tracker;
705 record_constructor(type_, None, name, *arity, tracker)
706 }
707 ValueConstructorVariant::ModuleFn { .. }
708 | ValueConstructorVariant::ModuleConstant { .. }
709 | ValueConstructorVariant::LocalVariable { .. } => self.local_var(name).to_doc(),
710 }
711 }
712
713 fn pipeline(
714 &mut self,
715 first_value: &'a TypedPipelineAssignment,
716 assignments: &'a [(TypedPipelineAssignment, PipelineAssignmentKind)],
717 finally: &'a TypedExpr,
718 ) -> Document<'a> {
719 let count = assignments.len();
720 let mut documents = Vec::with_capacity((count + 2) * 2);
721
722 let all_assignments = std::iter::once(first_value)
723 .chain(assignments.iter().map(|(assignment, _kind)| assignment));
724
725 let mut latest_local_var: Option<EcoString> = None;
726 for assignment in all_assignments {
727 // An echo in a pipeline won't result in an assignment, instead it
728 // just prints the previous variable assigned in the pipeline.
729 if let TypedExpr::Echo {
730 expression: None,
731 message,
732 location,
733 ..
734 } = assignment.value.as_ref()
735 {
736 documents.push(self.not_in_tail_position(Some(Ordering::Strict), |this| {
737 let var = latest_local_var
738 .as_ref()
739 .expect("echo with no previous step in a pipe");
740 this.echo(var.to_doc(), message.as_deref(), location)
741 }))
742 } else {
743 // Otherwise we assign the intermediate pipe value to a variable.
744 let assignment_document = self
745 .not_in_tail_position(Some(Ordering::Strict), |this| {
746 this.simple_variable_assignment(&assignment.name, &assignment.value)
747 });
748 documents.push(self.add_statement_level(assignment_document));
749 latest_local_var = Some(self.local_var(&assignment.name));
750 }
751
752 documents.push(line());
753 }
754
755 if let TypedExpr::Echo {
756 expression: None,
757 message,
758 location,
759 ..
760 } = finally
761 {
762 let var = latest_local_var.expect("echo with no previous step in a pipe");
763 documents.push(self.echo(var.to_doc(), message.as_deref(), location));
764 } else {
765 let finally = self.expression(finally);
766 documents.push(self.add_statement_level(finally))
767 }
768
769 documents.to_doc().force_break()
770 }
771
772 pub(crate) fn expression_flattening_blocks(
773 &mut self,
774 expression: &'a TypedExpr,
775 ) -> Document<'a> {
776 if let TypedExpr::Block { statements, .. } = expression {
777 self.statements(statements)
778 } else {
779 self.expression(expression)
780 }
781 }
782
783 fn block(&mut self, statements: &'a Vec1<TypedStatement>) -> Document<'a> {
784 if statements.len() == 1 {
785 match statements.first() {
786 Statement::Expression(expression) => return self.child_expression(expression),
787
788 Statement::Assignment(assignment) => match &assignment.kind {
789 AssignmentKind::Let | AssignmentKind::Generated => {
790 return self.child_expression(&assignment.value);
791 }
792 // We can't just return the right-hand side of a `let assert`
793 // assignment; we still need to check that the pattern matches.
794 AssignmentKind::Assert { .. } => {}
795 },
796
797 Statement::Use(use_) => return self.child_expression(&use_.call),
798
799 // Similar to `let assert`, we can't immediately return the value
800 // that is asserted; we have to actually perform the assertion.
801 Statement::Assert(_) => {}
802 }
803 }
804 match &self.scope_position {
805 Position::Tail | Position::Assign(_) | Position::Statement => {
806 self.block_document(statements)
807 }
808 Position::Expression(Ordering::Strict) => self
809 .immediately_invoked_function_expression(statements, |this, statements| {
810 this.statements(statements)
811 }),
812 Position::Expression(Ordering::Loose) => self.wrap_block(|this| {
813 // Save previous scope
814 let current_scope_vars = this.current_scope_vars.clone();
815
816 let document = this.block_document(statements);
817
818 // Restore previous state
819 this.current_scope_vars = current_scope_vars;
820
821 document
822 }),
823 }
824 }
825
826 fn block_document(&mut self, statements: &'a Vec1<TypedStatement>) -> Document<'a> {
827 let statements = self.statements(statements);
828 docvec!["{", docvec![line(), statements].nest(INDENT), line(), "}"]
829 }
830
831 fn statements(&mut self, statements: &'a [TypedStatement]) -> Document<'a> {
832 // If there are any statements that need to be printed at statement level, that's
833 // for an outer scope so we don't want to print them inside this one.
834 let statement_level = std::mem::take(&mut self.statement_level);
835 let count = statements.len();
836 let mut documents = Vec::with_capacity(count * 3);
837 for (i, statement) in statements.iter().enumerate() {
838 if i + 1 < count {
839 let function_position =
840 std::mem::replace(&mut self.function_position, Position::Statement);
841 let scope_position =
842 std::mem::replace(&mut self.scope_position, Position::Statement);
843
844 documents.push(self.statement(statement));
845
846 self.function_position = function_position;
847 self.scope_position = scope_position;
848
849 if requires_semicolon(statement) {
850 documents.push(";".to_doc());
851 }
852 documents.push(line());
853 } else {
854 documents.push(self.statement(statement));
855 }
856
857 // If we've generated code for a statement that always throws, we
858 // can skip code generation for all the following ones.
859 if self.let_assert_always_panics {
860 self.let_assert_always_panics = false;
861 break;
862 }
863 }
864 self.statement_level = statement_level;
865 if count == 1 {
866 documents.to_doc()
867 } else {
868 documents.to_doc().force_break()
869 }
870 }
871
872 fn simple_variable_assignment(
873 &mut self,
874 name: &'a EcoString,
875 value: &'a TypedExpr,
876 ) -> Document<'a> {
877 // Subject must be rendered before the variable for variable numbering
878 let subject =
879 self.not_in_tail_position(Some(Ordering::Loose), |this| this.wrap_expression(value));
880 let js_name = self.next_local_var(name);
881 let assignment = docvec!["let ", js_name.clone(), " = ", subject, ";"];
882 let assignment = match &self.scope_position {
883 Position::Expression(_) | Position::Statement => assignment,
884 Position::Tail => docvec![assignment, line(), "return ", js_name, ";"],
885 Position::Assign(block_variable) => docvec![
886 assignment,
887 line(),
888 block_variable.clone(),
889 " = ",
890 js_name,
891 ";"
892 ],
893 };
894
895 assignment.force_break()
896 }
897
898 fn assignment(&mut self, assignment: &'a TypedAssignment) -> Document<'a> {
899 let TypedAssignment {
900 pattern,
901 kind,
902 value,
903 compiled_case,
904 annotation: _,
905 location: _,
906 } = assignment;
907
908 // In case the pattern is just a variable, we special case it to
909 // generate just a simple assignment instead of using the decision tree
910 // for the code generation step.
911 if let TypedPattern::Variable { name, .. } = pattern {
912 return self.simple_variable_assignment(name, value);
913 }
914
915 decision::let_(compiled_case, value, kind, self, pattern)
916 }
917
918 fn assert(&mut self, assert: &'a TypedAssert) -> Document<'a> {
919 let TypedAssert {
920 location,
921 value,
922 message,
923 } = assert;
924
925 let message = match message {
926 Some(message) => {
927 self.not_in_tail_position(Some(Ordering::Strict), |this| this.expression(message))
928 }
929 None => string("Assertion failed."),
930 };
931
932 let check = self.not_in_tail_position(Some(Ordering::Loose), |this| {
933 this.assert_check(value, &message, *location)
934 });
935
936 match &self.scope_position {
937 Position::Expression(_) | Position::Statement => check,
938 Position::Tail | Position::Assign(_) => {
939 docvec![check, line(), self.wrap_return("undefined".to_doc())]
940 }
941 }
942 }
943
944 fn assert_check(
945 &mut self,
946 subject: &'a TypedExpr,
947 message: &Document<'a>,
948 location: SrcSpan,
949 ) -> Document<'a> {
950 let (subject_document, mut fields) = match subject {
951 TypedExpr::Call { fun, arguments, .. } => {
952 let argument_variables = arguments
953 .iter()
954 .map(|element| {
955 self.not_in_tail_position(Some(Ordering::Strict), |this| {
956 this.assign_to_variable(&element.value)
957 })
958 })
959 .collect_vec();
960 (
961 self.call_with_doc_arguments(fun, argument_variables.clone()),
962 vec![
963 ("kind", string("function_call")),
964 (
965 "arguments",
966 array(argument_variables.into_iter().zip(arguments).map(
967 |(variable, argument)| {
968 self.asserted_expression(
969 AssertExpression::from_expression(&argument.value),
970 Some(variable),
971 argument.location(),
972 )
973 },
974 )),
975 ),
976 ],
977 )
978 }
979
980 TypedExpr::BinOp {
981 name, left, right, ..
982 } => {
983 match name {
984 BinOp::And => return self.assert_and(left, right, message, location),
985 BinOp::Or => return self.assert_or(left, right, message, location),
986 BinOp::Eq
987 | BinOp::NotEq
988 | BinOp::LtInt
989 | BinOp::LtEqInt
990 | BinOp::LtFloat
991 | BinOp::LtEqFloat
992 | BinOp::GtEqInt
993 | BinOp::GtInt
994 | BinOp::GtEqFloat
995 | BinOp::GtFloat
996 | BinOp::AddInt
997 | BinOp::AddFloat
998 | BinOp::SubInt
999 | BinOp::SubFloat
1000 | BinOp::MultInt
1001 | BinOp::MultFloat
1002 | BinOp::DivInt
1003 | BinOp::DivFloat
1004 | BinOp::RemainderInt
1005 | BinOp::Concatenate => {}
1006 }
1007
1008 let left_document = self.not_in_tail_position(Some(Ordering::Loose), |this| {
1009 this.assign_to_variable(left)
1010 });
1011 let right_document = self.not_in_tail_position(Some(Ordering::Loose), |this| {
1012 this.assign_to_variable(right)
1013 });
1014
1015 (
1016 self.bin_op_with_doc_operands(
1017 *name,
1018 left_document.clone(),
1019 right_document.clone(),
1020 &left.type_(),
1021 )
1022 .surround("(", ")"),
1023 vec![
1024 ("kind", string("binary_operator")),
1025 ("operator", string(name.name())),
1026 (
1027 "left",
1028 self.asserted_expression(
1029 AssertExpression::from_expression(left),
1030 Some(left_document),
1031 left.location(),
1032 ),
1033 ),
1034 (
1035 "right",
1036 self.asserted_expression(
1037 AssertExpression::from_expression(right),
1038 Some(right_document),
1039 right.location(),
1040 ),
1041 ),
1042 ],
1043 )
1044 }
1045
1046 TypedExpr::Int { .. }
1047 | TypedExpr::Float { .. }
1048 | TypedExpr::String { .. }
1049 | TypedExpr::Block { .. }
1050 | TypedExpr::Pipeline { .. }
1051 | TypedExpr::Var { .. }
1052 | TypedExpr::Fn { .. }
1053 | TypedExpr::List { .. }
1054 | TypedExpr::Case { .. }
1055 | TypedExpr::RecordAccess { .. }
1056 | TypedExpr::PositionalAccess { .. }
1057 | TypedExpr::ModuleSelect { .. }
1058 | TypedExpr::Tuple { .. }
1059 | TypedExpr::TupleIndex { .. }
1060 | TypedExpr::Todo { .. }
1061 | TypedExpr::Panic { .. }
1062 | TypedExpr::Echo { .. }
1063 | TypedExpr::BitArray { .. }
1064 | TypedExpr::RecordUpdate { .. }
1065 | TypedExpr::NegateBool { .. }
1066 | TypedExpr::NegateInt { .. }
1067 | TypedExpr::Invalid { .. } => (
1068 self.wrap_expression(subject),
1069 vec![
1070 ("kind", string("expression")),
1071 (
1072 "expression",
1073 self.asserted_expression(
1074 AssertExpression::from_expression(subject),
1075 Some("false".to_doc()),
1076 subject.location(),
1077 ),
1078 ),
1079 ],
1080 ),
1081 };
1082
1083 fields.push(("start", location.start.to_doc()));
1084 fields.push(("end", subject.location().end.to_doc()));
1085 fields.push(("expression_start", subject.location().start.to_doc()));
1086
1087 docvec![
1088 "if (",
1089 docvec!["!", subject_document].nest(INDENT),
1090 break_("", ""),
1091 ") {",
1092 docvec![
1093 line(),
1094 self.throw_error("assert", message, location, fields),
1095 ]
1096 .nest(INDENT),
1097 line(),
1098 "}",
1099 ]
1100 .group()
1101 }
1102
1103 fn negate_bool_expression(&mut self, value: &'a TypedExpr) -> Document<'a> {
1104 match value {
1105 TypedExpr::BinOp {
1106 name, left, right, ..
1107 } => match name {
1108 BinOp::And => self.print_bin_op(left, right, "||"),
1109 BinOp::Or => self.print_bin_op(left, right, "&&"),
1110 BinOp::Eq => self.equal(left, right, false),
1111 BinOp::NotEq => self.equal(left, right, true),
1112 BinOp::LtInt | BinOp::LtFloat => self.print_bin_op(left, right, ">="),
1113 BinOp::LtEqInt | BinOp::LtEqFloat => self.print_bin_op(left, right, ">"),
1114 BinOp::GtInt | BinOp::GtFloat => self.print_bin_op(left, right, "<="),
1115 BinOp::GtEqInt | BinOp::GtEqFloat => self.print_bin_op(left, right, "<"),
1116 BinOp::AddInt
1117 | BinOp::AddFloat
1118 | BinOp::SubInt
1119 | BinOp::SubFloat
1120 | BinOp::MultInt
1121 | BinOp::MultFloat
1122 | BinOp::DivInt
1123 | BinOp::DivFloat
1124 | BinOp::RemainderInt
1125 | BinOp::Concatenate => unreachable!("type checking should make this impossible"),
1126 },
1127 TypedExpr::NegateBool { value, .. } => self.wrap_expression(value),
1128 TypedExpr::Int { .. }
1129 | TypedExpr::Float { .. }
1130 | TypedExpr::String { .. }
1131 | TypedExpr::Block { .. }
1132 | TypedExpr::Pipeline { .. }
1133 | TypedExpr::Var { .. }
1134 | TypedExpr::Fn { .. }
1135 | TypedExpr::List { .. }
1136 | TypedExpr::Call { .. }
1137 | TypedExpr::Case { .. }
1138 | TypedExpr::RecordAccess { .. }
1139 | TypedExpr::PositionalAccess { .. }
1140 | TypedExpr::ModuleSelect { .. }
1141 | TypedExpr::Tuple { .. }
1142 | TypedExpr::TupleIndex { .. }
1143 | TypedExpr::Todo { .. }
1144 | TypedExpr::Panic { .. }
1145 | TypedExpr::Echo { .. }
1146 | TypedExpr::BitArray { .. }
1147 | TypedExpr::RecordUpdate { .. }
1148 | TypedExpr::NegateInt { .. }
1149 | TypedExpr::Invalid { .. } => docvec!["!", self.wrap_expression(value)],
1150 }
1151 }
1152
1153 /// In Gleam, the `&&` operator is short-circuiting, meaning that we can't
1154 /// pre-evaluate both sides of it, and use them in the exception that is
1155 /// thrown.
1156 /// Instead, we need to implement this short-circuiting logic ourself.
1157 ///
1158 /// If we short-circuit, we must leave the second expression unevaluated,
1159 /// and signal that using the `unevaluated` variant, as detailed in the
1160 /// exception format. For the first expression, we know it must be `false`,
1161 /// otherwise we would have continued by evaluating the second expression.
1162 ///
1163 /// Similarly, if we do evaluate the second expression and fail, we know
1164 /// that the first expression must have evaluated to `true`, and the second
1165 /// to `false`. This way, we avoid needing to evaluate either expression
1166 /// twice.
1167 ///
1168 /// The generated code then looks something like this:
1169 /// ```javascript
1170 /// if (expr1) {
1171 /// if (!expr2) {
1172 /// <throw exception>
1173 /// }
1174 /// } else {
1175 /// <throw exception>
1176 /// }
1177 /// ```
1178 ///
1179 fn assert_and(
1180 &mut self,
1181 left: &'a TypedExpr,
1182 right: &'a TypedExpr,
1183 message: &Document<'a>,
1184 location: SrcSpan,
1185 ) -> Document<'a> {
1186 let left_kind = AssertExpression::from_expression(left);
1187 let right_kind = AssertExpression::from_expression(right);
1188
1189 let fields_if_short_circuiting = vec![
1190 ("kind", string("binary_operator")),
1191 ("operator", string("&&")),
1192 (
1193 "left",
1194 self.asserted_expression(left_kind, Some("false".to_doc()), left.location()),
1195 ),
1196 (
1197 "right",
1198 self.asserted_expression(AssertExpression::Unevaluated, None, right.location()),
1199 ),
1200 ("start", location.start.to_doc()),
1201 ("end", right.location().end.to_doc()),
1202 ("expression_start", left.location().start.to_doc()),
1203 ];
1204
1205 let fields = vec![
1206 ("kind", string("binary_operator")),
1207 ("operator", string("&&")),
1208 (
1209 "left",
1210 self.asserted_expression(left_kind, Some("true".to_doc()), left.location()),
1211 ),
1212 (
1213 "right",
1214 self.asserted_expression(right_kind, Some("false".to_doc()), right.location()),
1215 ),
1216 ("start", location.start.to_doc()),
1217 ("end", right.location().end.to_doc()),
1218 ("expression_start", left.location().start.to_doc()),
1219 ];
1220
1221 let left_value =
1222 self.not_in_tail_position(Some(Ordering::Loose), |this| this.wrap_expression(left));
1223
1224 let right_value = self.not_in_tail_position(Some(Ordering::Strict), |this| {
1225 this.negate_bool_expression(right)
1226 });
1227
1228 let right_check = docvec![
1229 line(),
1230 "if (",
1231 right_value.nest(INDENT),
1232 ") {",
1233 docvec![
1234 line(),
1235 self.throw_error("assert", message, location, fields)
1236 ]
1237 .nest(INDENT),
1238 line(),
1239 "}",
1240 ];
1241
1242 docvec![
1243 "if (",
1244 left_value.nest(INDENT),
1245 ") {",
1246 right_check.nest(INDENT),
1247 line(),
1248 "} else {",
1249 docvec![
1250 line(),
1251 self.throw_error("assert", message, location, fields_if_short_circuiting)
1252 ]
1253 .nest(INDENT),
1254 line(),
1255 "}"
1256 ]
1257 }
1258
1259 /// Similar to `&&`, `||` is also short-circuiting in Gleam. However, if `||`
1260 /// short-circuits, that's because the first expression evaluated to `true`,
1261 /// meaning the whole assertion succeeds. This allows us to directly use the
1262 /// `||` operator in JavaScript.
1263 ///
1264 /// The only difference is that due to the nature of `||`, if the assertion fails,
1265 /// we know that both sides must have evaluated to `false`, so we don't
1266 /// need to store the values of them in variables beforehand.
1267 fn assert_or(
1268 &mut self,
1269 left: &'a TypedExpr,
1270 right: &'a TypedExpr,
1271 message: &Document<'a>,
1272 location: SrcSpan,
1273 ) -> Document<'a> {
1274 let fields = vec![
1275 ("kind", string("binary_operator")),
1276 ("operator", string("||")),
1277 (
1278 "left",
1279 self.asserted_expression(
1280 AssertExpression::from_expression(left),
1281 Some("false".to_doc()),
1282 left.location(),
1283 ),
1284 ),
1285 (
1286 "right",
1287 self.asserted_expression(
1288 AssertExpression::from_expression(right),
1289 Some("false".to_doc()),
1290 right.location(),
1291 ),
1292 ),
1293 ("start", location.start.to_doc()),
1294 ("end", right.location().end.to_doc()),
1295 ("expression_start", left.location().start.to_doc()),
1296 ];
1297
1298 let left_value =
1299 self.not_in_tail_position(Some(Ordering::Loose), |this| this.child_expression(left));
1300
1301 let right_value =
1302 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(right));
1303
1304 docvec![
1305 line(),
1306 "if (",
1307 docvec!["!(", left_value, " || ", right_value, ")"].nest(INDENT),
1308 ") {",
1309 docvec![
1310 line(),
1311 self.throw_error("assert", message, location, fields)
1312 ]
1313 .nest(INDENT),
1314 line(),
1315 "}",
1316 ]
1317 }
1318
1319 fn assign_to_variable(&mut self, value: &'a TypedExpr) -> Document<'a> {
1320 if let TypedExpr::Var { .. } = value {
1321 self.expression(value)
1322 } else {
1323 let value = self.wrap_expression(value);
1324 let variable = self.next_local_var(&ASSIGNMENT_VAR.into());
1325 let assignment = docvec!["let ", variable.clone(), " = ", value, ";"];
1326 self.statement_level.push(assignment);
1327 variable.to_doc()
1328 }
1329 }
1330
1331 fn asserted_expression(
1332 &mut self,
1333 kind: AssertExpression,
1334 value: Option<Document<'a>>,
1335 location: SrcSpan,
1336 ) -> Document<'a> {
1337 let kind = match kind {
1338 AssertExpression::Literal => string("literal"),
1339 AssertExpression::Expression => string("expression"),
1340 AssertExpression::Unevaluated => string("unevaluated"),
1341 };
1342
1343 let start = location.start.to_doc();
1344 let end = location.end.to_doc();
1345 let items = if let Some(value) = value {
1346 vec![
1347 ("kind", kind),
1348 ("value", value),
1349 ("start", start),
1350 ("end", end),
1351 ]
1352 } else {
1353 vec![("kind", kind), ("start", start), ("end", end)]
1354 };
1355
1356 wrap_object(
1357 items
1358 .into_iter()
1359 .map(|(key, value)| (key.to_doc(), Some(value))),
1360 )
1361 }
1362
1363 fn tuple(&mut self, elements: &'a [TypedExpr]) -> Document<'a> {
1364 self.not_in_tail_position(Some(Ordering::Strict), |this| {
1365 array(elements.iter().map(|element| this.wrap_expression(element)))
1366 })
1367 }
1368
1369 fn call(&mut self, fun: &'a TypedExpr, arguments: &'a [TypedCallArg]) -> Document<'a> {
1370 let arguments = arguments
1371 .iter()
1372 .map(|element| {
1373 self.not_in_tail_position(Some(Ordering::Strict), |this| {
1374 this.wrap_expression(&element.value)
1375 })
1376 })
1377 .collect_vec();
1378
1379 self.call_with_doc_arguments(fun, arguments)
1380 }
1381
1382 fn call_with_doc_arguments(
1383 &mut self,
1384 fun: &'a TypedExpr,
1385 arguments: Vec<Document<'a>>,
1386 ) -> Document<'a> {
1387 match fun {
1388 // Qualified record construction
1389 TypedExpr::ModuleSelect {
1390 constructor: ModuleValueConstructor::Record { name, .. },
1391 module_alias,
1392 ..
1393 } => self.wrap_return(construct_record(Some(module_alias), name, arguments)),
1394
1395 // Record construction
1396 TypedExpr::Var {
1397 constructor:
1398 ValueConstructor {
1399 variant: ValueConstructorVariant::Record { .. },
1400 type_,
1401 ..
1402 },
1403 name,
1404 ..
1405 } => {
1406 if type_.is_result_constructor() {
1407 if name == "Ok" {
1408 self.tracker.ok_used = true;
1409 } else if name == "Error" {
1410 self.tracker.error_used = true;
1411 }
1412 }
1413 self.wrap_return(construct_record(None, name, arguments))
1414 }
1415
1416 // Tail call optimisation. If we are calling the current function
1417 // and we are in tail position we can avoid creating a new stack
1418 // frame, enabling recursion with constant memory usage.
1419 TypedExpr::Var { name, .. }
1420 if self.function_name == *name
1421 && self.current_function.can_recurse()
1422 && self.function_position.is_tail()
1423 && self.current_scope_vars.get(name) == Some(&0) =>
1424 {
1425 let mut docs = Vec::with_capacity(arguments.len() * 4);
1426 // Record that tail recursion is happening so that we know to
1427 // render the loop at the top level of the function.
1428 self.tail_recursion_used = true;
1429
1430 for (i, (element, argument)) in arguments
1431 .into_iter()
1432 .zip(&self.function_arguments)
1433 .enumerate()
1434 {
1435 if i != 0 {
1436 docs.push(line());
1437 }
1438 // Create an assignment for each variable created by the function arguments
1439 if let Some(name) = argument {
1440 docs.push("loop$".to_doc());
1441 docs.push(name.to_doc());
1442 docs.push(" = ".to_doc());
1443 }
1444 // Render the value given to the function. Even if it is not
1445 // assigned we still render it because the expression may
1446 // have some side effects.
1447 docs.push(element);
1448 docs.push(";".to_doc());
1449 }
1450 docs.to_doc()
1451 }
1452
1453 TypedExpr::Int { .. }
1454 | TypedExpr::Float { .. }
1455 | TypedExpr::String { .. }
1456 | TypedExpr::Block { .. }
1457 | TypedExpr::Pipeline { .. }
1458 | TypedExpr::Var { .. }
1459 | TypedExpr::Fn { .. }
1460 | TypedExpr::List { .. }
1461 | TypedExpr::Call { .. }
1462 | TypedExpr::BinOp { .. }
1463 | TypedExpr::Case { .. }
1464 | TypedExpr::RecordAccess { .. }
1465 | TypedExpr::PositionalAccess { .. }
1466 | TypedExpr::ModuleSelect { .. }
1467 | TypedExpr::Tuple { .. }
1468 | TypedExpr::TupleIndex { .. }
1469 | TypedExpr::Todo { .. }
1470 | TypedExpr::Panic { .. }
1471 | TypedExpr::Echo { .. }
1472 | TypedExpr::BitArray { .. }
1473 | TypedExpr::RecordUpdate { .. }
1474 | TypedExpr::NegateBool { .. }
1475 | TypedExpr::NegateInt { .. }
1476 | TypedExpr::Invalid { .. } => {
1477 let fun = self.not_in_tail_position(None, |this| -> Document<'_> {
1478 let is_fn_literal = matches!(fun, TypedExpr::Fn { .. });
1479 let fun = this.wrap_expression(fun);
1480 if is_fn_literal {
1481 docvec!["(", fun, ")"]
1482 } else {
1483 fun
1484 }
1485 });
1486 let arguments = call_arguments(arguments);
1487 self.wrap_return(docvec![fun, arguments])
1488 }
1489 }
1490 }
1491
1492 fn fn_(&mut self, arguments: &'a [TypedArg], body: &'a [TypedStatement]) -> Document<'a> {
1493 // New function, this is now the tail position
1494 let function_position = std::mem::replace(&mut self.function_position, Position::Tail);
1495 let scope_position = std::mem::replace(&mut self.scope_position, Position::Tail);
1496
1497 // And there's a new scope
1498 let scope = self.current_scope_vars.clone();
1499 for name in arguments.iter().flat_map(Arg::get_variable_name) {
1500 let _ = self.current_scope_vars.insert(name.clone(), 0);
1501 }
1502
1503 // This is a new function so track that so that we don't
1504 // mistakenly trigger tail call optimisation
1505 let mut current_function = CurrentFunction::Anonymous;
1506 std::mem::swap(&mut self.current_function, &mut current_function);
1507
1508 // Generate the function body
1509 let result = self.statements(body);
1510
1511 // Reset function name, scope, and tail position tracking
1512 self.function_position = function_position;
1513 self.scope_position = scope_position;
1514 self.current_scope_vars = scope;
1515 std::mem::swap(&mut self.current_function, &mut current_function);
1516
1517 docvec![
1518 docvec![
1519 fun_arguments(arguments, false),
1520 " => {",
1521 break_("", " "),
1522 result
1523 ]
1524 .nest(INDENT)
1525 .append(break_("", " "))
1526 .group(),
1527 "}",
1528 ]
1529 }
1530
1531 fn record_access(&mut self, record: &'a TypedExpr, label: &'a str) -> Document<'a> {
1532 self.not_in_tail_position(None, |this| {
1533 let record = this.wrap_expression(record);
1534 docvec![record, ".", maybe_escape_property(label)]
1535 })
1536 }
1537
1538 fn positional_access(&mut self, record: &'a TypedExpr, index: u64) -> Document<'a> {
1539 self.not_in_tail_position(None, |this| {
1540 let record = this.wrap_expression(record);
1541 docvec![record, "[", index, "]"]
1542 })
1543 }
1544
1545 fn record_update(
1546 &mut self,
1547 record: &'a Option<Box<TypedAssignment>>,
1548 constructor: &'a TypedExpr,
1549 arguments: &'a [TypedCallArg],
1550 ) -> Document<'a> {
1551 match record.as_ref() {
1552 Some(record) => docvec![
1553 self.not_in_tail_position(None, |this| this.assignment(record)),
1554 line(),
1555 self.call(constructor, arguments),
1556 ],
1557 None => self.call(constructor, arguments),
1558 }
1559 }
1560
1561 fn tuple_index(&mut self, tuple: &'a TypedExpr, index: u64) -> Document<'a> {
1562 self.not_in_tail_position(None, |this| {
1563 let tuple = this.wrap_expression(tuple);
1564 docvec![tuple, eco_format!("[{index}]")]
1565 })
1566 }
1567
1568 fn bin_op(
1569 &mut self,
1570 name: &'a BinOp,
1571 left: &'a TypedExpr,
1572 right: &'a TypedExpr,
1573 ) -> Document<'a> {
1574 match name {
1575 BinOp::And => self.print_bin_op(left, right, "&&"),
1576 BinOp::Or => self.print_bin_op(left, right, "||"),
1577 BinOp::LtInt | BinOp::LtFloat => self.print_bin_op(left, right, "<"),
1578 BinOp::LtEqInt | BinOp::LtEqFloat => self.print_bin_op(left, right, "<="),
1579 BinOp::Eq => self.equal(left, right, true),
1580 BinOp::NotEq => self.equal(left, right, false),
1581 BinOp::GtInt | BinOp::GtFloat => self.print_bin_op(left, right, ">"),
1582 BinOp::GtEqInt | BinOp::GtEqFloat => self.print_bin_op(left, right, ">="),
1583 BinOp::Concatenate | BinOp::AddInt | BinOp::AddFloat => {
1584 self.print_bin_op(left, right, "+")
1585 }
1586 BinOp::SubInt | BinOp::SubFloat => self.print_bin_op(left, right, "-"),
1587 BinOp::MultInt | BinOp::MultFloat => self.print_bin_op(left, right, "*"),
1588 BinOp::RemainderInt => self.remainder_int(left, right),
1589 BinOp::DivInt => self.div_int(left, right),
1590 BinOp::DivFloat => self.div_float(left, right),
1591 }
1592 }
1593
1594 fn div_int(&mut self, left: &'a TypedExpr, right: &'a TypedExpr) -> Document<'a> {
1595 let left_doc =
1596 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(left));
1597 let right_doc =
1598 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(right));
1599
1600 // If we have a constant value divided by zero then it's safe to replace
1601 // it directly with 0.
1602 if left.is_literal() && right.is_zero_compile_time_number() {
1603 "0".to_doc()
1604 } else if right.is_non_zero_compile_time_number() {
1605 let division = if let TypedExpr::BinOp { .. } = left {
1606 docvec![left_doc.surround("(", ")"), " / ", right_doc]
1607 } else {
1608 docvec![left_doc, " / ", right_doc]
1609 };
1610 docvec!["globalThis.Math.trunc", wrap_arguments([division])]
1611 } else {
1612 self.tracker.int_division_used = true;
1613 docvec!["divideInt", wrap_arguments([left_doc, right_doc])]
1614 }
1615 }
1616
1617 fn remainder_int(&mut self, left: &'a TypedExpr, right: &'a TypedExpr) -> Document<'a> {
1618 let left_doc =
1619 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(left));
1620 let right_doc =
1621 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(right));
1622
1623 // If we have a constant value divided by zero then it's safe to replace
1624 // it directly with 0.
1625 if left.is_literal() && right.is_zero_compile_time_number() {
1626 "0".to_doc()
1627 } else if right.is_non_zero_compile_time_number() {
1628 if let TypedExpr::BinOp { .. } = left {
1629 docvec![left_doc.surround("(", ")"), " % ", right_doc]
1630 } else {
1631 docvec![left_doc, " % ", right_doc]
1632 }
1633 } else {
1634 self.tracker.int_remainder_used = true;
1635 docvec!["remainderInt", wrap_arguments([left_doc, right_doc])]
1636 }
1637 }
1638
1639 fn div_float(&mut self, left: &'a TypedExpr, right: &'a TypedExpr) -> Document<'a> {
1640 let left_doc =
1641 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(left));
1642 let right_doc =
1643 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(right));
1644
1645 // If we have a constant value divided by zero then it's safe to replace
1646 // it directly with 0.
1647 if left.is_literal() && right.is_zero_compile_time_number() {
1648 "0.0".to_doc()
1649 } else if right.is_non_zero_compile_time_number() {
1650 if let TypedExpr::BinOp { .. } = left {
1651 docvec![left_doc.surround("(", ")"), " / ", right_doc]
1652 } else {
1653 docvec![left_doc, " / ", right_doc]
1654 }
1655 } else {
1656 self.tracker.float_division_used = true;
1657 docvec!["divideFloat", wrap_arguments([left_doc, right_doc])]
1658 }
1659 }
1660
1661 fn equal(
1662 &mut self,
1663 left: &'a TypedExpr,
1664 right: &'a TypedExpr,
1665 should_be_equal: bool,
1666 ) -> Document<'a> {
1667 // If it is a simple scalar type then we can use JS' reference identity
1668 if is_js_scalar(left.type_()) {
1669 let left_doc = self
1670 .not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(left));
1671 let right_doc = self
1672 .not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(right));
1673 let operator = if should_be_equal { " === " } else { " !== " };
1674 return docvec![left_doc, operator, right_doc];
1675 }
1676
1677 // For comparison with singleton custom types, ie, one with no fields.
1678 // If you have some code like this
1679 // ```gleam
1680 // pub type Wibble {
1681 // Wibble
1682 // Wobble
1683 // }
1684
1685 // pub fn is_wibble(w: Wibble) -> Bool {
1686 // w == Wibble
1687 // }
1688 // ```
1689 // Instead of `isEqual(w, new Wibble())`, generate `w instanceof Wibble`
1690 // because the first approach needs to construct a new Wibble, and then call the isEqual function,
1691 // which supports any shape of data, and so does a lot of extra logic which isn't necessary.
1692
1693 if let Some(doc) = self.singleton_variant_equality(left, right, should_be_equal) {
1694 return doc;
1695 }
1696
1697 if let Some(doc) = self.singleton_variant_equality(right, left, should_be_equal) {
1698 return doc;
1699 }
1700
1701 // Other types must be compared using structural equality
1702 let left =
1703 self.not_in_tail_position(Some(Ordering::Strict), |this| this.wrap_expression(left));
1704 let right =
1705 self.not_in_tail_position(Some(Ordering::Strict), |this| this.wrap_expression(right));
1706
1707 self.prelude_equal_call(should_be_equal, left, right)
1708 }
1709
1710 fn singleton_variant_equality(
1711 &mut self,
1712 left: &'a TypedExpr,
1713 right: &'a TypedExpr,
1714 should_be_equal: bool,
1715 ) -> Option<Document<'a>> {
1716 match right {
1717 TypedExpr::Var {
1718 constructor:
1719 ValueConstructor {
1720 variant: ValueConstructorVariant::Record { arity: 0, name, .. },
1721 ..
1722 },
1723 ..
1724 } => {
1725 let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
1726 this.wrap_expression(left)
1727 });
1728 Some(self.singleton_equal(left_doc, None, name, should_be_equal))
1729 }
1730 TypedExpr::ModuleSelect {
1731 module_alias,
1732 constructor: ModuleValueConstructor::Record { arity: 0, name, .. },
1733 ..
1734 } => {
1735 let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
1736 this.wrap_expression(left)
1737 });
1738 Some(self.singleton_equal(left_doc, Some(module_alias), name, should_be_equal))
1739 }
1740 TypedExpr::Int { .. }
1741 | TypedExpr::Float { .. }
1742 | TypedExpr::String { .. }
1743 | TypedExpr::Block { .. }
1744 | TypedExpr::Pipeline { .. }
1745 | TypedExpr::Var { .. }
1746 | TypedExpr::Fn { .. }
1747 | TypedExpr::List { .. }
1748 | TypedExpr::Call { .. }
1749 | TypedExpr::BinOp { .. }
1750 | TypedExpr::Case { .. }
1751 | TypedExpr::RecordAccess { .. }
1752 | TypedExpr::PositionalAccess { .. }
1753 | TypedExpr::ModuleSelect { .. }
1754 | TypedExpr::Tuple { .. }
1755 | TypedExpr::TupleIndex { .. }
1756 | TypedExpr::Todo { .. }
1757 | TypedExpr::Panic { .. }
1758 | TypedExpr::Echo { .. }
1759 | TypedExpr::BitArray { .. }
1760 | TypedExpr::RecordUpdate { .. }
1761 | TypedExpr::NegateBool { .. }
1762 | TypedExpr::NegateInt { .. }
1763 | TypedExpr::Invalid { .. } => None,
1764 }
1765 }
1766
1767 fn singleton_equal(
1768 &self,
1769 value: Document<'a>,
1770 module: Option<&'a str>,
1771 name: &'a str,
1772 should_be_equal: bool,
1773 ) -> Document<'a> {
1774 let record = if let Some(module) = module {
1775 docvec!["$", module, ".", name]
1776 } else {
1777 name.to_doc()
1778 };
1779
1780 if should_be_equal {
1781 docvec![value, " instanceof ", record]
1782 } else {
1783 docvec!["!(", value, " instanceof ", record, ")"]
1784 }
1785 }
1786
1787 fn equal_with_doc_operands(
1788 &mut self,
1789 left: Document<'a>,
1790 right: Document<'a>,
1791 type_: Arc<Type>,
1792 should_be_equal: bool,
1793 ) -> Document<'a> {
1794 // If it is a simple scalar type then we can use JS' reference identity
1795 if is_js_scalar(type_) {
1796 let operator = if should_be_equal { " === " } else { " !== " };
1797 return docvec![left, operator, right];
1798 }
1799
1800 // Other types must be compared using structural equality
1801 self.prelude_equal_call(should_be_equal, left, right)
1802 }
1803
1804 pub(super) fn prelude_equal_call(
1805 &mut self,
1806 should_be_equal: bool,
1807 left: Document<'a>,
1808 right: Document<'a>,
1809 ) -> Document<'a> {
1810 // Record that we need to import the prelude's isEqual function into the module
1811 self.tracker.object_equality_used = true;
1812 // Construct the call
1813 let arguments = wrap_arguments([left, right]);
1814 let operator = if should_be_equal {
1815 "isEqual"
1816 } else {
1817 "!isEqual"
1818 };
1819 docvec![operator, arguments]
1820 }
1821
1822 fn print_bin_op(
1823 &mut self,
1824 left: &'a TypedExpr,
1825 right: &'a TypedExpr,
1826 op: &'a str,
1827 ) -> Document<'a> {
1828 let left =
1829 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(left));
1830 let right =
1831 self.not_in_tail_position(Some(Ordering::Strict), |this| this.child_expression(right));
1832 docvec![left, " ", op, " ", right]
1833 }
1834
1835 pub(super) fn bin_op_with_doc_operands(
1836 &mut self,
1837 name: BinOp,
1838 left: Document<'a>,
1839 right: Document<'a>,
1840 type_: &Arc<Type>,
1841 ) -> Document<'a> {
1842 match name {
1843 BinOp::And => docvec![left, " && ", right],
1844 BinOp::Or => docvec![left, " || ", right],
1845 BinOp::LtInt | BinOp::LtFloat => docvec![left, " < ", right],
1846 BinOp::LtEqInt | BinOp::LtEqFloat => docvec![left, " <= ", right],
1847 BinOp::Eq => self.equal_with_doc_operands(left, right, type_.clone(), true),
1848 BinOp::NotEq => self.equal_with_doc_operands(left, right, type_.clone(), false),
1849 BinOp::GtInt | BinOp::GtFloat => docvec![left, " > ", right],
1850 BinOp::GtEqInt | BinOp::GtEqFloat => docvec![left, " >= ", right],
1851 BinOp::Concatenate | BinOp::AddInt | BinOp::AddFloat => {
1852 docvec![left, " + ", right]
1853 }
1854 BinOp::SubInt | BinOp::SubFloat => docvec![left, " - ", right],
1855 BinOp::MultInt | BinOp::MultFloat => docvec![left, " * ", right],
1856 BinOp::RemainderInt => {
1857 self.tracker.int_remainder_used = true;
1858 docvec!["remainderInt", wrap_arguments([left, right])]
1859 }
1860 BinOp::DivInt => {
1861 self.tracker.int_division_used = true;
1862 docvec!["divideInt", wrap_arguments([left, right])]
1863 }
1864 BinOp::DivFloat => {
1865 self.tracker.float_division_used = true;
1866 docvec!["divideFloat", wrap_arguments([left, right])]
1867 }
1868 }
1869 }
1870
1871 fn todo(&mut self, message: Option<&'a TypedExpr>, location: &'a SrcSpan) -> Document<'a> {
1872 let message = match message {
1873 Some(m) => self.not_in_tail_position(None, |this| this.wrap_expression(m)),
1874 None => string("`todo` expression evaluated. This code has not yet been implemented."),
1875 };
1876 self.throw_error("todo", &message, *location, vec![])
1877 }
1878
1879 fn panic(&mut self, location: &'a SrcSpan, message: Option<&'a TypedExpr>) -> Document<'a> {
1880 let message = match message {
1881 Some(m) => self.not_in_tail_position(None, |this| this.wrap_expression(m)),
1882 None => string("`panic` expression evaluated."),
1883 };
1884 self.throw_error("panic", &message, *location, vec![])
1885 }
1886
1887 pub(crate) fn throw_error<Fields>(
1888 &mut self,
1889 error_name: &'a str,
1890 message: &Document<'a>,
1891 location: SrcSpan,
1892 fields: Fields,
1893 ) -> Document<'a>
1894 where
1895 Fields: IntoIterator<Item = (&'a str, Document<'a>)>,
1896 {
1897 self.tracker.make_error_used = true;
1898 let module = self.module_name.clone().to_doc().surround('"', '"');
1899 let function = self.function_name.clone().to_doc().surround("\"", "\"");
1900 let line = self.line_numbers.line_number(location.start).to_doc();
1901 let fields = wrap_object(fields.into_iter().map(|(k, v)| (k.to_doc(), Some(v))));
1902
1903 docvec![
1904 "throw makeError",
1905 wrap_arguments([
1906 string(error_name),
1907 "FILEPATH".to_doc(),
1908 module,
1909 line,
1910 function,
1911 message.clone(),
1912 fields
1913 ]),
1914 ]
1915 }
1916
1917 fn module_select(
1918 &mut self,
1919 module: &'a str,
1920 label: &'a EcoString,
1921 constructor: &'a ModuleValueConstructor,
1922 ) -> Document<'a> {
1923 match constructor {
1924 ModuleValueConstructor::Fn { .. } | ModuleValueConstructor::Constant { .. } => {
1925 docvec!["$", module, ".", maybe_escape_identifier(label)]
1926 }
1927
1928 ModuleValueConstructor::Record {
1929 name, arity, type_, ..
1930 } => record_constructor(type_.clone(), Some(module), name, *arity, self.tracker),
1931 }
1932 }
1933
1934 fn echo(
1935 &mut self,
1936 expression: Document<'a>,
1937 message: Option<&'a TypedExpr>,
1938 location: &'a SrcSpan,
1939 ) -> Document<'a> {
1940 self.tracker.echo_used = true;
1941
1942 let message = match message {
1943 Some(message) => self
1944 .not_in_tail_position(Some(Ordering::Strict), |this| this.wrap_expression(message)),
1945 None => "undefined".to_doc(),
1946 };
1947
1948 let echo_arguments = call_arguments(vec![
1949 expression,
1950 message,
1951 self.src_path.clone().to_doc(),
1952 self.line_numbers.line_number(location.start).to_doc(),
1953 ]);
1954 self.wrap_return(docvec!["echo", echo_arguments])
1955 }
1956
1957 pub(crate) fn constant_expression(
1958 &mut self,
1959 context: Context,
1960 expression: &'a TypedConstant,
1961 ) -> Document<'a> {
1962 match expression {
1963 Constant::Int { value, .. } => int(value),
1964 Constant::Float { value, .. } => float(value),
1965 Constant::String { value, .. } => string(value),
1966 Constant::Tuple { elements, .. } => array(
1967 elements
1968 .iter()
1969 .map(|element| self.constant_expression(context, element)),
1970 ),
1971
1972 Constant::List { elements, .. } => {
1973 self.tracker.list_used = true;
1974 let list = list(
1975 elements
1976 .iter()
1977 .map(|element| self.constant_expression(context, element)),
1978 );
1979
1980 match context {
1981 Context::Constant => docvec!["/* @__PURE__ */ ", list],
1982 Context::Guard => list,
1983 }
1984 }
1985
1986 Constant::Record { type_, name, .. } if type_.is_bool() && name == "True" => {
1987 "true".to_doc()
1988 }
1989 Constant::Record { type_, name, .. } if type_.is_bool() && name == "False" => {
1990 "false".to_doc()
1991 }
1992 Constant::Record { type_, .. } if type_.is_nil() => "undefined".to_doc(),
1993
1994 Constant::Record {
1995 arguments,
1996 module,
1997 name,
1998 tag,
1999 type_,
2000 ..
2001 } => {
2002 if module.is_none() && type_.is_result() {
2003 if tag == "Ok" {
2004 self.tracker.ok_used = true;
2005 } else {
2006 self.tracker.error_used = true;
2007 }
2008 }
2009
2010 // If there's no arguments and the type is a function that takes
2011 // arguments then this is the constructor being referenced, not the
2012 // function being called.
2013 if let Some(arity) = type_.fn_arity()
2014 && arguments.is_empty()
2015 && arity != 0
2016 {
2017 let arity = arity as u16;
2018 return record_constructor(type_.clone(), None, name, arity, self.tracker);
2019 }
2020
2021 // Record updates are fully expanded during type checking, so we just handle arguments
2022 let field_values = arguments
2023 .iter()
2024 .map(|argument| self.constant_expression(context, &argument.value))
2025 .collect_vec();
2026
2027 let constructor = construct_record(
2028 module.as_ref().map(|(module, _)| module.as_str()),
2029 name,
2030 field_values,
2031 );
2032 match context {
2033 Context::Constant => docvec!["/* @__PURE__ */ ", constructor],
2034 Context::Guard => constructor,
2035 }
2036 }
2037 Constant::BitArray { segments, .. } => {
2038 let bit_array = self.constant_bit_array(segments, context);
2039 match context {
2040 Context::Constant => docvec!["/* @__PURE__ */ ", bit_array],
2041 Context::Guard => bit_array,
2042 }
2043 }
2044
2045 Constant::Var { name, module, .. } => {
2046 match (module, context) {
2047 (None, Context::Guard) => self.local_var(name).to_doc(),
2048 (None, Context::Constant) => maybe_escape_identifier(name).to_doc(),
2049 (Some((module, _)), _) => {
2050 // JS keywords can be accessed here, but we must escape anyway
2051 // as we escape when exporting such names in the first place,
2052 // and the imported name has to match the exported name.
2053 docvec!["$", module, ".", maybe_escape_identifier(name)]
2054 }
2055 }
2056 }
2057
2058 Constant::StringConcatenation { left, right, .. } => {
2059 let left = self.constant_expression(context, left);
2060 let right = self.constant_expression(context, right);
2061 docvec![left, " + ", right]
2062 }
2063
2064 Constant::RecordUpdate { .. } => {
2065 panic!("record updates should not reach code generation")
2066 }
2067
2068 Constant::Invalid { .. } => {
2069 panic!("invalid constants should not reach code generation")
2070 }
2071 }
2072 }
2073
2074 fn constant_bit_array(
2075 &mut self,
2076 segments: &'a [TypedConstantBitArraySegment],
2077 context: Context,
2078 ) -> Document<'a> {
2079 self.tracker.bit_array_literal_used = true;
2080 let segments_array = array(segments.iter().map(|segment| {
2081 let value = match context {
2082 Context::Constant => self.constant_expression(context, &segment.value),
2083 Context::Guard => self.guard_constant_expression(&segment.value),
2084 };
2085
2086 let details = self.constant_bit_array_segment_details(segment, context);
2087
2088 match details.type_ {
2089 BitArraySegmentType::BitArray => {
2090 if segment.size().is_some() {
2091 self.tracker.bit_array_slice_used = true;
2092 docvec!["bitArraySlice(", value, ", 0, ", details.size, ")"]
2093 } else {
2094 value
2095 }
2096 }
2097 BitArraySegmentType::Int => match (details.size_value, segment.value.as_ref()) {
2098 (Some(size_value), Constant::Int { int_value, .. })
2099 if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into()
2100 && (&size_value % BigInt::from(8) == BigInt::ZERO) =>
2101 {
2102 let bytes = bit_array_segment_int_value_to_bytes(
2103 int_value.clone(),
2104 size_value,
2105 segment.endianness(),
2106 );
2107
2108 u8_slice(&bytes)
2109 }
2110
2111 (Some(size_value), _) if size_value == 8.into() => value,
2112
2113 (Some(size_value), _) if size_value <= 0.into() => nil(),
2114
2115 _ => {
2116 self.tracker.sized_integer_segment_used = true;
2117 let size = details.size;
2118 let is_big = bool(segment.endianness().is_big());
2119 docvec!["sizedInt(", value, ", ", size, ", ", is_big, ")"]
2120 }
2121 },
2122 BitArraySegmentType::Float => {
2123 self.tracker.float_bit_array_segment_used = true;
2124 let size = details.size;
2125 let is_big = bool(details.endianness.is_big());
2126 docvec!["sizedFloat(", value, ", ", size, ", ", is_big, ")"]
2127 }
2128 BitArraySegmentType::String(StringEncoding::Utf8) => {
2129 self.tracker.string_bit_array_segment_used = true;
2130 docvec!["stringBits(", value, ")"]
2131 }
2132 BitArraySegmentType::String(StringEncoding::Utf16) => {
2133 self.tracker.string_utf16_bit_array_segment_used = true;
2134 let is_big = bool(details.endianness.is_big());
2135 docvec!["stringToUtf16(", value, ", ", is_big, ")"]
2136 }
2137 BitArraySegmentType::String(StringEncoding::Utf32) => {
2138 self.tracker.string_utf32_bit_array_segment_used = true;
2139 let is_big = bool(details.endianness.is_big());
2140 docvec!["stringToUtf32(", value, ", ", is_big, ")"]
2141 }
2142 BitArraySegmentType::UtfCodepoint(StringEncoding::Utf8) => {
2143 self.tracker.codepoint_bit_array_segment_used = true;
2144 docvec!["codepointBits(", value, ")"]
2145 }
2146 BitArraySegmentType::UtfCodepoint(StringEncoding::Utf16) => {
2147 self.tracker.codepoint_utf16_bit_array_segment_used = true;
2148 let is_big = bool(details.endianness.is_big());
2149 docvec!["codepointToUtf16(", value, ", ", is_big, ")"]
2150 }
2151 BitArraySegmentType::UtfCodepoint(StringEncoding::Utf32) => {
2152 self.tracker.codepoint_utf32_bit_array_segment_used = true;
2153 let is_big = bool(details.endianness.is_big());
2154 docvec!["codepointToUtf32(", value, ", ", is_big, ")"]
2155 }
2156 }
2157 }));
2158
2159 docvec!["toBitArray(", segments_array, ")"]
2160 }
2161
2162 fn constant_bit_array_segment_details(
2163 &mut self,
2164 segment: &'a TypedConstantBitArraySegment,
2165 context: Context,
2166 ) -> BitArraySegmentDetails<'a> {
2167 let size = segment.size();
2168 let unit = segment.unit();
2169 let (size_value, size) = match size {
2170 Some(Constant::Int { int_value, .. }) => {
2171 let size_value = int_value * unit;
2172 let size = eco_format!("{}", size_value).to_doc();
2173 (Some(size_value), size)
2174 }
2175
2176 Some(size) => {
2177 let mut size = match context {
2178 Context::Constant => self.constant_expression(context, size),
2179 Context::Guard => self.guard_constant_expression(size),
2180 };
2181 if unit != 1 {
2182 size = size.group().append(" * ".to_doc().append(unit.to_doc()));
2183 }
2184
2185 (None, size)
2186 }
2187
2188 None => {
2189 let size_value: usize = if segment.type_.is_int() { 8 } else { 64 };
2190 (Some(BigInt::from(size_value)), docvec![size_value])
2191 }
2192 };
2193
2194 let type_ = BitArraySegmentType::from_segment(segment);
2195
2196 BitArraySegmentDetails {
2197 type_,
2198 size,
2199 size_value,
2200 endianness: segment.endianness(),
2201 }
2202 }
2203
2204 pub(crate) fn guard(&mut self, guard: &'a TypedClauseGuard) -> Document<'a> {
2205 match guard {
2206 ClauseGuard::Block { value, .. } => self.guard(value).surround("(", ")"),
2207
2208 ClauseGuard::BinaryOperator {
2209 left,
2210 right,
2211 operator,
2212 ..
2213 } => {
2214 let left_document = self.wrapped_guard(left);
2215 let right_document = self.wrapped_guard(right);
2216
2217 let operator = match operator {
2218 BinOp::Eq if is_js_scalar(left.type_()) => "===",
2219 BinOp::NotEq if is_js_scalar(left.type_()) => "!==",
2220 BinOp::Eq | BinOp::NotEq => {
2221 let should_be_equal = *operator == BinOp::Eq;
2222
2223 // Handle singleton equality optimization for guards
2224 if let Some(doc) =
2225 self.singleton_variant_guard_equality(left, right, should_be_equal)
2226 {
2227 return doc;
2228 }
2229
2230 if let Some(doc) =
2231 self.singleton_variant_guard_equality(right, left, should_be_equal)
2232 {
2233 return doc;
2234 }
2235
2236 let left_doc = self.guard(left);
2237 let right_doc = self.guard(right);
2238 return self.prelude_equal_call(should_be_equal, left_doc, right_doc);
2239 }
2240
2241 BinOp::GtFloat | BinOp::GtInt => ">",
2242 BinOp::GtEqFloat | BinOp::GtEqInt => ">=",
2243 BinOp::LtFloat | BinOp::LtInt => "<",
2244 BinOp::LtEqFloat | BinOp::LtEqInt => "<=",
2245
2246 BinOp::AddFloat | BinOp::AddInt | BinOp::Concatenate => "+",
2247 BinOp::SubFloat | BinOp::SubInt => "-",
2248 BinOp::MultFloat | BinOp::MultInt => "*",
2249
2250 BinOp::DivFloat => {
2251 self.tracker.float_division_used = true;
2252 return docvec![
2253 "divideFloat",
2254 wrap_arguments([left_document, right_document])
2255 ];
2256 }
2257
2258 BinOp::DivInt => {
2259 self.tracker.int_division_used = true;
2260 return docvec![
2261 "divideInt",
2262 wrap_arguments([left_document, right_document])
2263 ];
2264 }
2265
2266 BinOp::RemainderInt => {
2267 self.tracker.int_remainder_used = true;
2268 return docvec![
2269 "remainderInt",
2270 wrap_arguments([left_document, right_document])
2271 ];
2272 }
2273
2274 BinOp::And => "&&",
2275 BinOp::Or => "||",
2276 };
2277
2278 docvec![left_document, " ", operator, " ", right_document]
2279 }
2280
2281 ClauseGuard::Var { name, .. } => self.local_var(name).to_doc(),
2282
2283 ClauseGuard::TupleIndex { tuple, index, .. } => {
2284 docvec![self.guard(tuple,), "[", index, "]"]
2285 }
2286
2287 ClauseGuard::FieldAccess {
2288 label, container, ..
2289 } => docvec![self.guard(container), ".", maybe_escape_property(label)],
2290
2291 ClauseGuard::ModuleSelect {
2292 module_alias,
2293 label,
2294 ..
2295 } => docvec!["$", module_alias, ".", label],
2296
2297 ClauseGuard::Not { expression, .. } => docvec!["!", self.guard(expression,)],
2298
2299 ClauseGuard::Constant(constant) => self.guard_constant_expression(constant),
2300 }
2301 }
2302
2303 fn singleton_variant_guard_equality(
2304 &mut self,
2305 left: &'a TypedClauseGuard,
2306 right: &'a TypedClauseGuard,
2307 should_be_equal: bool,
2308 ) -> Option<Document<'a>> {
2309 if let ClauseGuard::Constant(Constant::Record {
2310 record_constructor: Some(constructor),
2311 module,
2312 name,
2313 ..
2314 }) = right
2315 && let ValueConstructorVariant::Record { arity: 0, .. } = constructor.variant
2316 {
2317 let left_doc = self.guard(left);
2318 return Some(self.singleton_equal(
2319 left_doc,
2320 module.as_ref().map(|(module, _)| module.as_str()),
2321 name,
2322 should_be_equal,
2323 ));
2324 }
2325 None
2326 }
2327
2328 fn wrapped_guard(&mut self, guard: &'a TypedClauseGuard) -> Document<'a> {
2329 match guard {
2330 ClauseGuard::Var { .. }
2331 | ClauseGuard::TupleIndex { .. }
2332 | ClauseGuard::Constant(_)
2333 | ClauseGuard::Not { .. }
2334 | ClauseGuard::FieldAccess { .. }
2335 | ClauseGuard::Block { .. } => self.guard(guard),
2336
2337 ClauseGuard::BinaryOperator { .. } | ClauseGuard::ModuleSelect { .. } => {
2338 docvec!["(", self.guard(guard), ")"]
2339 }
2340 }
2341 }
2342
2343 fn guard_constant_expression(&mut self, expression: &'a TypedConstant) -> Document<'a> {
2344 match expression {
2345 Constant::Tuple { elements, .. } => array(
2346 elements
2347 .iter()
2348 .map(|element| self.guard_constant_expression(element)),
2349 ),
2350
2351 Constant::List { elements, .. } => {
2352 self.tracker.list_used = true;
2353 list(
2354 elements
2355 .iter()
2356 .map(|element| self.guard_constant_expression(element)),
2357 )
2358 }
2359 Constant::Record { type_, name, .. } if type_.is_bool() && name == "True" => {
2360 "true".to_doc()
2361 }
2362 Constant::Record { type_, name, .. } if type_.is_bool() && name == "False" => {
2363 "false".to_doc()
2364 }
2365 Constant::Record { type_, .. } if type_.is_nil() => "undefined".to_doc(),
2366
2367 Constant::Record {
2368 arguments,
2369 module,
2370 name,
2371 tag,
2372 type_,
2373 ..
2374 } => {
2375 if module.is_none() && type_.is_result() {
2376 if tag == "Ok" {
2377 self.tracker.ok_used = true;
2378 } else {
2379 self.tracker.error_used = true;
2380 }
2381 }
2382
2383 // If there's no arguments and the type is a function that takes
2384 // arguments then this is the constructor being referenced, not the
2385 // function being called.
2386 if let Some(arity) = type_.fn_arity()
2387 && arguments.is_empty()
2388 && arity != 0
2389 {
2390 let arity = arity as u16;
2391 return record_constructor(type_.clone(), None, name, arity, self.tracker);
2392 }
2393
2394 // Record updates are fully expanded during type checking, so we just
2395 // handle arguments
2396 let field_values = arguments
2397 .iter()
2398 .map(|argument| self.guard_constant_expression(&argument.value))
2399 .collect_vec();
2400 construct_record(
2401 module.as_ref().map(|(module, _)| module.as_str()),
2402 name,
2403 field_values,
2404 )
2405 }
2406
2407 Constant::BitArray { segments, .. } => {
2408 self.constant_bit_array(segments, Context::Guard)
2409 }
2410
2411 Constant::Var { name, .. } => self.local_var(name).to_doc(),
2412
2413 Constant::Int { .. }
2414 | Constant::Float { .. }
2415 | Constant::String { .. }
2416 | Constant::RecordUpdate { .. }
2417 | Constant::StringConcatenation { .. }
2418 | Constant::Invalid { .. } => self.constant_expression(Context::Guard, expression),
2419 }
2420 }
2421}
2422
2423#[derive(Clone, Copy)]
2424enum AssertExpression {
2425 Literal,
2426 Expression,
2427 Unevaluated,
2428}
2429
2430impl AssertExpression {
2431 fn from_expression(expression: &TypedExpr) -> Self {
2432 if expression.is_literal() {
2433 Self::Literal
2434 } else {
2435 Self::Expression
2436 }
2437 }
2438}
2439
2440pub fn int(value: &str) -> Document<'_> {
2441 eco_string_int(value.into())
2442}
2443
2444pub fn eco_string_int<'a>(value: EcoString) -> Document<'a> {
2445 let mut out = EcoString::with_capacity(value.len());
2446
2447 if value.starts_with('-') {
2448 out.push('-');
2449 } else if value.starts_with('+') {
2450 out.push('+');
2451 };
2452 let value = value.trim_start_matches(['+', '-'].as_ref());
2453
2454 let value = if value.starts_with("0x") {
2455 out.push_str("0x");
2456 value.trim_start_matches("0x")
2457 } else if value.starts_with("0o") {
2458 out.push_str("0o");
2459 value.trim_start_matches("0o")
2460 } else if value.starts_with("0b") {
2461 out.push_str("0b");
2462 value.trim_start_matches("0b")
2463 } else {
2464 value
2465 };
2466
2467 let value = value.trim_start_matches(['0', '_']);
2468 if value.is_empty() {
2469 out.push('0');
2470 }
2471
2472 out.push_str(value);
2473
2474 out.to_doc()
2475}
2476
2477pub fn float(value: &str) -> Document<'_> {
2478 let mut out = EcoString::with_capacity(value.len());
2479
2480 if value.starts_with('-') {
2481 out.push('-');
2482 } else if value.starts_with('+') {
2483 out.push('+');
2484 };
2485 let value = value.trim_start_matches(['+', '-'].as_ref());
2486
2487 let value = value.trim_start_matches(['0', '_']);
2488 if value.starts_with(['.', 'e', 'E']) {
2489 out.push('0');
2490 }
2491 out.push_str(value);
2492
2493 out.to_doc()
2494}
2495
2496pub fn float_from_value(value: f64) -> Document<'static> {
2497 if value.is_infinite() {
2498 if value.is_sign_positive() {
2499 "Infinity".to_doc()
2500 } else {
2501 "-Infinity".to_doc()
2502 }
2503 } else if value.is_nan() {
2504 // NOTE: this case is probably unnecessary, as this function is only
2505 // invoked with `LiteralFloatValue` values, which cannot be nan.
2506 "NaN".to_doc()
2507 } else {
2508 value.to_doc()
2509 }
2510}
2511
2512/// The context where the constant expression is used, it might be inside a
2513/// function call, or in the definition of another constant.
2514///
2515/// Based on the context we might want to annotate pure function calls as
2516/// "@__PURE__".
2517///
2518#[derive(Debug, Clone, Copy)]
2519pub enum Context {
2520 Constant,
2521 Guard,
2522}
2523
2524#[derive(Debug)]
2525struct BitArraySegmentDetails<'a> {
2526 type_: BitArraySegmentType,
2527 size: Document<'a>,
2528 /// The size of the bit array segment stored as a BigInt.
2529 /// This has a value when the segment's size is known at compile time.
2530 size_value: Option<BigInt>,
2531 endianness: Endianness,
2532}
2533
2534#[derive(Debug, Clone, Copy)]
2535enum BitArraySegmentType {
2536 BitArray,
2537 Int,
2538 Float,
2539 String(StringEncoding),
2540 UtfCodepoint(StringEncoding),
2541}
2542
2543impl BitArraySegmentType {
2544 fn from_segment<Value>(segment: &BitArraySegment<Value, Arc<Type>>) -> Self {
2545 if segment.type_.is_int() {
2546 BitArraySegmentType::Int
2547 } else if segment.type_.is_float() {
2548 BitArraySegmentType::Float
2549 } else if segment.type_.is_bit_array() {
2550 BitArraySegmentType::BitArray
2551 } else if segment.type_.is_string() {
2552 let encoding = if segment.has_utf16_option() {
2553 StringEncoding::Utf16
2554 } else if segment.has_utf32_option() {
2555 StringEncoding::Utf32
2556 } else {
2557 StringEncoding::Utf8
2558 };
2559 BitArraySegmentType::String(encoding)
2560 } else if segment.type_.is_utf_codepoint() {
2561 let encoding = if segment.has_utf16_codepoint_option() {
2562 StringEncoding::Utf16
2563 } else if segment.has_utf32_codepoint_option() {
2564 StringEncoding::Utf32
2565 } else {
2566 StringEncoding::Utf8
2567 };
2568 BitArraySegmentType::UtfCodepoint(encoding)
2569 } else {
2570 panic!(
2571 "Invalid bit array segment type reached code generation: {:?}",
2572 segment.type_
2573 );
2574 }
2575 }
2576}
2577
2578pub fn string(value: &str) -> Document<'_> {
2579 if value.contains('\n') {
2580 EcoString::from(value.replace('\n', r"\n"))
2581 .to_doc()
2582 .surround("\"", "\"")
2583 } else {
2584 value.to_doc().surround("\"", "\"")
2585 }
2586}
2587
2588pub(crate) fn array<'a, Elements: IntoIterator<Item = Document<'a>>>(
2589 elements: Elements,
2590) -> Document<'a> {
2591 let elements = Itertools::intersperse(elements.into_iter(), break_(",", ", ")).collect_vec();
2592 if elements.is_empty() {
2593 // Do not add a trailing comma since that adds an 'undefined' element
2594 "[]".to_doc()
2595 } else {
2596 docvec![
2597 "[",
2598 docvec![break_("", ""), elements].nest(INDENT),
2599 break_(",", ""),
2600 "]"
2601 ]
2602 .group()
2603 }
2604}
2605
2606pub(crate) fn list<'a, I: IntoIterator<Item = Document<'a>>>(elements: I) -> Document<'a>
2607where
2608 I::IntoIter: DoubleEndedIterator + ExactSizeIterator,
2609{
2610 let array = array(elements);
2611 docvec!["toList(", array, ")"]
2612}
2613
2614fn prepend<'a, I: IntoIterator<Item = Document<'a>>>(
2615 elements: I,
2616 tail: Document<'a>,
2617) -> Document<'a>
2618where
2619 I::IntoIter: DoubleEndedIterator + ExactSizeIterator,
2620{
2621 elements.into_iter().rev().fold(tail, |tail, element| {
2622 let arguments = call_arguments([element, tail]);
2623 docvec!["listPrepend", arguments]
2624 })
2625}
2626
2627fn call_arguments<'a, Elements: IntoIterator<Item = Document<'a>>>(
2628 elements: Elements,
2629) -> Document<'a> {
2630 let elements = Itertools::intersperse(elements.into_iter(), break_(",", ", "))
2631 .collect_vec()
2632 .to_doc();
2633 if elements.is_empty() {
2634 return "()".to_doc();
2635 }
2636 docvec![
2637 "(",
2638 docvec![break_("", ""), elements].nest(INDENT),
2639 break_(",", ""),
2640 ")"
2641 ]
2642 .group()
2643}
2644
2645pub(crate) fn construct_record<'a>(
2646 module: Option<&'a str>,
2647 name: &'a str,
2648 arguments: impl IntoIterator<Item = Document<'a>>,
2649) -> Document<'a> {
2650 let mut any_arguments = false;
2651 let arguments = join(
2652 arguments.into_iter().inspect(|_| {
2653 any_arguments = true;
2654 }),
2655 break_(",", ", "),
2656 );
2657 let arguments = docvec![break_("", ""), arguments].nest(INDENT);
2658 let name = if let Some(module) = module {
2659 docvec!["$", module, ".", name]
2660 } else {
2661 name.to_doc()
2662 };
2663 if any_arguments {
2664 docvec!["new ", name, "(", arguments, break_(",", ""), ")"].group()
2665 } else {
2666 docvec!["new ", name, "()"]
2667 }
2668}
2669
2670impl TypedExpr {
2671 fn handles_own_return(&self) -> bool {
2672 match self {
2673 TypedExpr::Todo { .. }
2674 | TypedExpr::Call { .. }
2675 | TypedExpr::Case { .. }
2676 | TypedExpr::Panic { .. }
2677 | TypedExpr::Block { .. }
2678 | TypedExpr::Echo { .. }
2679 | TypedExpr::Pipeline { .. }
2680 | TypedExpr::RecordUpdate { .. } => true,
2681
2682 TypedExpr::Int { .. }
2683 | TypedExpr::Float { .. }
2684 | TypedExpr::String { .. }
2685 | TypedExpr::Var { .. }
2686 | TypedExpr::Fn { .. }
2687 | TypedExpr::List { .. }
2688 | TypedExpr::BinOp { .. }
2689 | TypedExpr::RecordAccess { .. }
2690 | TypedExpr::PositionalAccess { .. }
2691 | TypedExpr::ModuleSelect { .. }
2692 | TypedExpr::Tuple { .. }
2693 | TypedExpr::TupleIndex { .. }
2694 | TypedExpr::BitArray { .. }
2695 | TypedExpr::NegateBool { .. }
2696 | TypedExpr::NegateInt { .. }
2697 | TypedExpr::Invalid { .. } => false,
2698 }
2699 }
2700}
2701
2702impl BinOp {
2703 fn is_operator_to_wrap(&self) -> bool {
2704 match self {
2705 BinOp::And
2706 | BinOp::Or
2707 | BinOp::Eq
2708 | BinOp::NotEq
2709 | BinOp::LtInt
2710 | BinOp::LtEqInt
2711 | BinOp::LtFloat
2712 | BinOp::LtEqFloat
2713 | BinOp::GtEqInt
2714 | BinOp::GtInt
2715 | BinOp::GtEqFloat
2716 | BinOp::GtFloat
2717 | BinOp::AddInt
2718 | BinOp::AddFloat
2719 | BinOp::SubInt
2720 | BinOp::SubFloat
2721 | BinOp::MultFloat
2722 | BinOp::DivInt
2723 | BinOp::DivFloat
2724 | BinOp::RemainderInt
2725 | BinOp::Concatenate => true,
2726 BinOp::MultInt => false,
2727 }
2728 }
2729}
2730
2731pub fn is_js_scalar(t: Arc<Type>) -> bool {
2732 t.is_int() || t.is_float() || t.is_bool() || t.is_nil() || t.is_string()
2733}
2734
2735fn requires_semicolon(statement: &TypedStatement) -> bool {
2736 match statement {
2737 Statement::Expression(
2738 TypedExpr::Int { .. }
2739 | TypedExpr::Fn { .. }
2740 | TypedExpr::Var { .. }
2741 | TypedExpr::List { .. }
2742 | TypedExpr::Call { .. }
2743 | TypedExpr::Echo { .. }
2744 | TypedExpr::Float { .. }
2745 | TypedExpr::String { .. }
2746 | TypedExpr::BinOp { .. }
2747 | TypedExpr::Tuple { .. }
2748 | TypedExpr::NegateInt { .. }
2749 | TypedExpr::BitArray { .. }
2750 | TypedExpr::TupleIndex { .. }
2751 | TypedExpr::NegateBool { .. }
2752 | TypedExpr::RecordAccess { .. }
2753 | TypedExpr::PositionalAccess { .. }
2754 | TypedExpr::ModuleSelect { .. }
2755 | TypedExpr::Block { .. },
2756 ) => true,
2757
2758 Statement::Expression(
2759 TypedExpr::Todo { .. }
2760 | TypedExpr::Case { .. }
2761 | TypedExpr::Panic { .. }
2762 | TypedExpr::Pipeline { .. }
2763 | TypedExpr::RecordUpdate { .. }
2764 | TypedExpr::Invalid { .. },
2765 ) => false,
2766
2767 Statement::Assignment(_) => false,
2768 Statement::Use(_) => false,
2769 Statement::Assert(_) => false,
2770 }
2771}
2772
2773/// Wrap a document in an immediately invoked function expression
2774fn immediately_invoked_function_expression_document(document: Document<'_>) -> Document<'_> {
2775 docvec![
2776 docvec!["(() => {", break_("", " "), document].nest(INDENT),
2777 break_("", " "),
2778 "})()",
2779 ]
2780 .group()
2781}
2782
2783pub(crate) fn record_constructor<'a>(
2784 type_: Arc<Type>,
2785 qualifier: Option<&'a str>,
2786 name: &'a str,
2787 arity: u16,
2788 tracker: &mut UsageTracker,
2789) -> Document<'a> {
2790 if qualifier.is_none() && type_.is_result_constructor() {
2791 if name == "Ok" {
2792 tracker.ok_used = true;
2793 } else if name == "Error" {
2794 tracker.error_used = true;
2795 }
2796 }
2797 if type_.is_bool() && name == "True" {
2798 "true".to_doc()
2799 } else if type_.is_bool() {
2800 "false".to_doc()
2801 } else if type_.is_nil() {
2802 "undefined".to_doc()
2803 } else if arity == 0 {
2804 match qualifier {
2805 Some(module) => docvec!["new $", module, ".", name, "()"],
2806 None => docvec!["new ", name, "()"],
2807 }
2808 } else {
2809 let vars = (0..arity).map(|i| eco_format!("var{i}").to_doc());
2810 let body = docvec![
2811 "return ",
2812 construct_record(qualifier, name, vars.clone()),
2813 ";"
2814 ];
2815 docvec![
2816 docvec![wrap_arguments(vars), " => {", break_("", " "), body]
2817 .nest(INDENT)
2818 .append(break_("", " "))
2819 .group(),
2820 "}",
2821 ]
2822 }
2823}
2824
2825fn u8_slice<'a>(bytes: &[u8]) -> Document<'a> {
2826 let s: EcoString = bytes
2827 .iter()
2828 .map(u8::to_string)
2829 .collect::<Vec<_>>()
2830 .join(", ")
2831 .into();
2832
2833 docvec![s]
2834}