// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Gleam contributors

use num_bigint::BigInt;
use vec1::Vec1;

use super::{decision::ASSIGNMENT_VAR, *};
use crate::{
    ast::*,
    exhaustiveness::StringEncoding,
    line_numbers::LineNumbers,
    type_::{
        ModuleValueConstructor, Type, TypedCallArg, ValueConstructor, ValueConstructorVariant,
    },
};
use pretty_arena::*;
use std::sync::Arc;

#[derive(Debug, Clone)]
pub enum Position {
    /// We are compiling the last expression in a function, meaning that it should
    /// use `return` to return the value it produces from the function.
    Tail,
    /// We are inside a function, but the value of this expression isn't being
    /// used, so we don't need to do anything with the returned value.
    Statement,
    /// The value of this expression needs to be used inside another expression,
    /// so we need to use the value that is returned by this expression.
    Expression(Ordering),
    /// We are compiling an expression inside a block, meaning we must assign
    /// to the `_block` variable at the end of the scope, because blocks are not
    /// expressions in JS.
    /// Since JS doesn't have variable shadowing, we must store the name of the
    /// variable being used, which will include the incrementing counter.
    /// For example, `block$2`
    Assign(EcoString),
}

impl Position {
    /// Returns `true` if the position is [`Tail`].
    ///
    /// [`Tail`]: Position::Tail
    #[must_use]
    pub fn is_tail(&self) -> bool {
        matches!(self, Self::Tail)
    }

    #[must_use]
    pub fn ordering(&self) -> Ordering {
        match self {
            Self::Expression(ordering) => *ordering,
            Self::Tail | Self::Assign(_) | Self::Statement => Ordering::Loose,
        }
    }
}

#[derive(Debug, Clone, Copy)]
/// Determines whether we can lift blocks into statement level instead of using
/// immediately invoked function expressions. Consider the following piece of code:
///
/// ```gleam
/// some_function(function_with_side_effect(), {
///   let a = 10
///   other_function_with_side_effects(a)
/// })
/// ```
/// Here, if we lift the block that is the second argument of the function, we
/// would end up running `other_function_with_side_effects` before
/// `function_with_side_effects`. This would be invalid, as code in Gleam should be
/// evaluated left-to-right, top-to-bottom. In this case, the ordering would be
/// `Strict`, indicating that we cannot lift the block.
///
/// However, in this example:
///
/// ```gleam
/// let value = !{
///   let value = False
///   some_function_with_side_effect()
///   value
/// }
/// ```
/// The only expression is the block, meaning it can be safely lifted without
/// changing the evaluation order of the program. So the ordering is `Loose`.
///
pub enum Ordering {
    Strict,
    Loose,
}

/// Tracking where the current function is a module function or an anonymous function.
#[derive(Debug)]
enum CurrentFunction {
    /// The current function is a module function
    ///
    /// ```gleam
    /// pub fn main() -> Nil {
    ///   // we are here
    /// }
    /// ```
    Module,

    /// The current function is a module function, but one of its arguments shadows
    /// the reference to itself so it cannot recurse.
    ///
    /// ```gleam
    /// pub fn main(main: fn() -> Nil) -> Nil {
    ///   // we are here
    /// }
    /// ```
    ModuleWithShadowingArgument,

    /// The current function is an anonymous function
    ///
    /// ```gleam
    /// pub fn main() -> Nil {
    ///   fn() {
    ///     // we are here
    ///   }
    /// }
    /// ```
    Anonymous,
}

impl CurrentFunction {
    #[inline]
    fn can_recurse(&self) -> bool {
        match self {
            CurrentFunction::Module => true,
            CurrentFunction::ModuleWithShadowingArgument => false,
            CurrentFunction::Anonymous => false,
        }
    }
}

/// The variables in scope while generating the code for a function.
///
/// User variables are tracked in a map keyed by name, so a shadowed name can be
/// given a unique JavaScript identifier. The compiler synthesised variables
/// (the `$` case subject, `_pipe`, `_block`, ...) are held in their own fields
/// instead. They can't be referenced by user code, and a branch that generates
/// directly into its enclosing scope needs to restore the user variables while
/// leaving the synthesised counters advanced, so that later code doesn't
/// redeclare one of them.
#[derive(Debug, Clone, Default)]
pub(crate) struct Scope {
    user_variables: im::HashMap<EcoString, usize>,
    /// The highest suffix handed out for each user variable still declared in
    /// the current JS scope, kept across a directly matching `case` branch so a
    /// variable that leaked out of it is not redeclared by a later `let`.
    high_water: im::HashMap<EcoString, usize>,
    assignment: Option<usize>,
    pipe: Option<usize>,
    block: Option<usize>,
    use_assignment: Option<usize>,
    record_update: Option<usize>,
    capture: Option<usize>,
    assert_subject: Option<usize>,
    assert_fail: Option<usize>,
}

impl Scope {
    fn new(user_variables: im::HashMap<EcoString, usize>) -> Self {
        Self {
            user_variables,
            ..Self::default()
        }
    }

    /// The counter for a variable name, whether user or synthesised.
    fn counter(&self, name: &str) -> Option<usize> {
        match name {
            ASSIGNMENT_VAR => self.assignment,
            PIPE_VARIABLE => self.pipe,
            BLOCK_VARIABLE => self.block,
            USE_ASSIGNMENT_VARIABLE => self.use_assignment,
            RECORD_UPDATE_VARIABLE => self.record_update,
            CAPTURE_VARIABLE => self.capture,
            ASSERT_SUBJECT_VARIABLE => self.assert_subject,
            ASSERT_FAIL_VARIABLE => self.assert_fail,
            _ => self.user_variables.get(name).copied(),
        }
    }

    /// Set the counter for a variable name, whether user or synthesised.
    pub(crate) fn set_counter(&mut self, name: &EcoString, value: usize) {
        match name.as_str() {
            ASSIGNMENT_VAR => self.assignment = Some(value),
            PIPE_VARIABLE => self.pipe = Some(value),
            BLOCK_VARIABLE => self.block = Some(value),
            USE_ASSIGNMENT_VARIABLE => self.use_assignment = Some(value),
            RECORD_UPDATE_VARIABLE => self.record_update = Some(value),
            CAPTURE_VARIABLE => self.capture = Some(value),
            ASSERT_SUBJECT_VARIABLE => self.assert_subject = Some(value),
            ASSERT_FAIL_VARIABLE => self.assert_fail = Some(value),
            _ => {
                let _ = self.user_variables.insert(name.clone(), value);
            }
        }
    }

    /// Advance the counter for a name to its next suffix, skipping any suffix
    /// already handed out for it in the current scope so a name that leaked out
    /// of a directly matching branch can't be redeclared.
    fn advance_counter(&mut self, name: &EcoString) {
        let in_scope = self.counter(name).map_or(0, |i| i + 1);
        let high_water = self.high_water.get(name).map_or(0, |i| i + 1);
        let next = in_scope.max(high_water);
        self.set_counter(name, next);
        // Only user variables leak out of a directly matching branch; the
        // synthesised counters survive the restore on their own.
        if self.user_variables.contains_key(name) {
            let _ = self.high_water.insert(name.clone(), next);
        }
    }

    /// The user variables currently in scope.
    pub(crate) fn user_variables(&self) -> &im::HashMap<EcoString, usize> {
        &self.user_variables
    }

    /// Restore previously saved user variables, reverting any counters advanced
    /// during the branch to their earlier values. Variables introduced in the
    /// branch with no earlier binding are kept, and the synthesised counters are
    /// left untouched.
    pub(crate) fn restore_user_variables(&mut self, previous: &im::HashMap<EcoString, usize>) {
        self.user_variables.extend(previous.clone());
    }
}

#[derive(Debug)]
pub(crate) struct Generator<'module, 'ast, 'doc> {
    module_name: EcoString,
    src_path: EcoString,
    line_numbers: &'module LineNumbers,
    function_name: EcoString,
    function_arguments: Vec<Option<&'module EcoString>>,
    current_function: CurrentFunction,
    pub current_scope: Scope,
    pub function_position: Position,
    pub scope_position: Position,
    // We register whether these features are used within an expression so that
    // the module generator can output a suitable function if it is needed.
    pub tracker: &'module mut UsageTracker,
    // We track whether tail call recursion is used so that we can render a loop
    // at the top level of the function to use in place of pushing new stack
    // frames.
    pub tail_recursion_used: bool,
    /// Statements to be compiled when lifting blocks into statement scope.
    /// For example, when compiling the following code:
    /// ```gleam
    /// let a = {
    ///   let b = 1
    ///   b + 1
    /// }
    /// ```
    /// There will be 2 items in `statement_level`: The first will be `let _block;`
    /// The second will be the generated code for the block being assigned to `a`.
    /// This lets use return `_block` as the value that the block evaluated to,
    /// while still including the necessary code in the output at the right place.
    ///
    /// Once the `let` statement has compiled its value, it will add anything accumulated
    /// in `statement_level` to the generated code, so it will result in:
    ///
    /// ```javascript
    /// let _block;
    /// {...}
    /// let a = _block;
    /// ```
    ///
    statement_level: Vec<Document<'ast, 'doc>>,

    /// This will be true if we've generated a `let assert` statement that we know
    /// is guaranteed to throw.
    /// This means we can stop code generation for all the following statements
    /// in the same block!
    pub let_assert_always_panics: bool,

    pub source_map_builder: Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>,
}

impl<'module, 'a, 'doc> Generator<'module, 'a, 'doc> {
    #[allow(clippy::too_many_arguments)] // TODO: FIXME
    pub fn new(
        module_name: EcoString,
        src_path: EcoString,
        line_numbers: &'module LineNumbers,
        function_name: EcoString,
        function_arguments: Vec<Option<&'module EcoString>>,
        tracker: &'module mut UsageTracker,
        initial_scope_vars: im::HashMap<EcoString, usize>,
        source_map_builder: Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>,
    ) -> Self {
        let mut current_scope = Scope::new(initial_scope_vars);
        let mut current_function = CurrentFunction::Module;
        for &name in function_arguments.iter().flatten() {
            // Initialise the function arguments
            current_scope.set_counter(name, 0);

            // If any of the function arguments shadow the current function then
            // recursion is no longer possible.
            if function_name.as_ref() == name {
                current_function = CurrentFunction::ModuleWithShadowingArgument;
            }
        }
        Self {
            tracker,
            module_name,
            src_path,
            line_numbers,
            function_name,
            function_arguments,
            tail_recursion_used: false,
            current_scope,
            current_function,
            function_position: Position::Tail,
            scope_position: Position::Tail,
            statement_level: Vec::new(),
            let_assert_always_panics: false,
            source_map_builder,
        }
    }

    pub fn local_var(&mut self, name: &EcoString) -> EcoString {
        match self.current_scope.counter(name) {
            None => {
                self.current_scope.set_counter(name, 0);
                maybe_escape_identifier(name)
            }
            Some(0) => maybe_escape_identifier(name),
            Some(n) if name == "$" => eco_format!("${n}"),
            Some(n) => eco_format!("{name}${n}"),
        }
    }

    pub fn next_local_var(&mut self, name: &EcoString) -> EcoString {
        self.current_scope.advance_counter(name);
        self.local_var(name)
    }

    pub fn function_body(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        body: &'a [TypedStatement],
        arguments: &'a [TypedArg],
    ) -> Document<'a, 'doc> {
        let body = self.statements(arena, body);
        if self.tail_recursion_used {
            self.tail_call_loop(arena, body, arguments)
        } else {
            body
        }
    }

    fn tail_call_loop(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        body: Document<'a, 'doc>,
        arguments: &'a [TypedArg],
    ) -> Document<'a, 'doc> {
        let loop_assignments = arena.concat(arguments.iter().flat_map(|arg| {
            arg.get_variable_name().map(|name| {
                let var = maybe_escape_identifier(name);
                docvec![
                    arena,
                    self.source_map_tracker(arena, arg.location.start),
                    LET_SPACE_DOCUMENT,
                    var,
                    SPACE_EQUAL_LOOP_DOLLAR_DOCUMENT,
                    name,
                    SEMICOLON_DOCUMENT,
                    LINE_DOCUMENT
                ]
            })
        }));
        docvec![
            arena,
            WHILE_TRUE_OPEN_PAREN_DOCUMENT,
            docvec![arena, LINE_DOCUMENT, loop_assignments, body].nest(arena, INDENT),
            LINE_DOCUMENT,
            CLOSE_CURLY_DOCUMENT
        ]
    }

    fn statement(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        statement: &'a TypedStatement,
    ) -> Document<'a, 'doc> {
        let expression_doc = match statement {
            Statement::Expression(expression) => self.expression(arena, expression),
            Statement::Assignment(assignment) => self.assignment(arena, assignment),
            Statement::Use(use_) => self.expression(arena, &use_.call),
            Statement::Assert(assert) => self.assert(arena, assert),
        };
        self.add_statement_level(arena, expression_doc)
    }

    fn add_statement_level(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        expression: Document<'a, 'doc>,
    ) -> Document<'a, 'doc> {
        if self.statement_level.is_empty() {
            expression
        } else {
            let mut statements = std::mem::take(&mut self.statement_level);
            statements.push(expression);
            arena.join(statements, LINE_DOCUMENT)
        }
    }

    pub fn expression(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        expression: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        let mut document = match expression {
            TypedExpr::String { value, .. } => string(arena, value),

            TypedExpr::Int { value, .. } => int(arena, value),
            TypedExpr::Float { float_value, .. } => float_from_value(arena, float_value.value()),

            TypedExpr::List { elements, tail, .. } => {
                self.not_in_tail_position(Some(Ordering::Strict), |this| match tail {
                    Some(tail) => {
                        this.tracker.prepend_used = true;
                        let tail = this.wrap_expression(arena, tail);
                        prepend(
                            arena,
                            elements
                                .iter()
                                .map(|element| this.wrap_expression(arena, element)),
                            tail,
                        )
                    }
                    None if elements.is_empty() => this.empty_list(),
                    None => {
                        this.tracker.list_used = true;
                        list(
                            arena,
                            elements
                                .iter()
                                .map(|element| this.wrap_expression(arena, element)),
                        )
                    }
                })
            }

            TypedExpr::Tuple { elements, .. } => self.tuple(arena, elements),
            TypedExpr::TupleIndex { tuple, index, .. } => self.tuple_index(arena, tuple, *index),

            TypedExpr::Case {
                subjects,
                clauses,
                compiled_case,
                ..
            } => decision::case(arena, compiled_case, clauses, subjects, self),

            TypedExpr::Call { fun, arguments, .. } => self.call(arena, fun, arguments),
            TypedExpr::Fn {
                arguments,
                body,
                kind,
                ..
            } => self.fn_(arena, arguments, body, kind),

            TypedExpr::RecordAccess { record, label, .. } => {
                self.record_access(arena, record, label)
            }

            TypedExpr::PositionalAccess { record, index, .. } => {
                self.positional_access(arena, record, *index)
            }

            TypedExpr::RecordUpdate {
                updated_record_assigned_name,
                updated_record,
                constructor,
                arguments,
                ..
            } => self.record_update(
                arena,
                updated_record_assigned_name,
                updated_record,
                constructor,
                arguments,
            ),

            TypedExpr::Var {
                name, constructor, ..
            } => self.variable(arena, name, constructor),

            TypedExpr::Pipeline {
                first_value,
                assignments,
                finally,
                ..
            } => self.pipeline(arena, first_value, assignments.as_slice(), finally),

            TypedExpr::Block { statements, .. } => self.block(arena, statements),

            TypedExpr::BinOp {
                operator,
                left,
                right,
                ..
            } => self.bin_op(arena, operator, left, right),

            TypedExpr::Todo {
                message, location, ..
            } => self.todo(arena, message.as_ref().map(|m| &**m), location),

            TypedExpr::Panic {
                location, message, ..
            } => self.panic(arena, location, message.as_ref().map(|m| &**m)),

            TypedExpr::BitArray { segments, .. } => self.bit_array(arena, segments),

            TypedExpr::ModuleSelect {
                module_alias,
                label,
                constructor,
                ..
            } => self.module_select(arena, module_alias, label, constructor),

            TypedExpr::NegateBool { value, .. } => {
                self.negate_with(arena, EXCLAMATION_MARK_DOCUMENT, value)
            }

            TypedExpr::NegateInt { value, .. } => {
                self.negate_with(arena, MINUS_SPACE_DOCUMENT, value)
            }

            TypedExpr::Echo {
                expression,
                message,
                location,
                ..
            } => {
                let expression = expression
                    .as_ref()
                    .expect("echo with no expression outside of pipe");
                let expresion_doc =
                    self.not_in_tail_position(None, |this| this.wrap_expression(arena, expression));
                self.echo(arena, expresion_doc, message.as_deref(), location)
            }

            TypedExpr::Invalid { .. } => {
                panic!("invalid expressions should not reach code generation")
            }
        };
        if let Position::Statement = self.scope_position
            && expression_requires_semicolon(expression)
        {
            document = document.append(arena, SEMICOLON_DOCUMENT);
        }
        if expression.handles_own_return() {
            docvec![
                arena,
                self.source_map_tracker(arena, expression.location().start),
                document
            ]
        } else {
            docvec![
                arena,
                self.source_map_tracker(arena, expression.location().start),
                self.wrap_return(arena, document)
            ]
        }
    }

    /// Return the singleton empty list; all empty lists are the same underlying
    /// reference, which makes comparison faster.
    fn empty_list(&mut self) -> Document<'a, 'doc> {
        self.tracker.list_empty_const_used = true;
        DOLLAR_LIST_DOLLAR_EMPTY_DOLLAR_CONST_DOCUMENT
    }

    fn negate_with(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        with: Document<'a, 'doc>,
        value: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        self.not_in_tail_position(None, |this| {
            docvec![arena, with, this.wrap_expression(arena, value)]
        })
    }

    fn bit_array(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        segments: &'a [TypedExprBitArraySegment],
    ) -> Document<'a, 'doc> {
        self.tracker.bit_array_literal_used = true;

        // Collect all the values used in segments.
        let segments_array = array(
            arena,
            segments.iter().map(|segment| {
                let value = self.not_in_tail_position(Some(Ordering::Strict), |this| {
                    this.wrap_expression(arena, &segment.value)
                });

                let details = self.bit_array_segment_details(arena, segment);

                match details.type_ {
                    BitArraySegmentType::BitArray => {
                        if segment.size().is_some() {
                            self.tracker.bit_array_slice_used = true;
                            docvec![
                                arena,
                                BIT_ARRAY_SLICE_OPEN_PAREN_DOCUMENT,
                                value,
                                COMMA_ZERO_COMMA_SPACE,
                                details.size,
                                CLOSE_PAREN_DOCUMENT
                            ]
                        } else {
                            value
                        }
                    }
                    BitArraySegmentType::Int => {
                        match (details.size_value, segment.value.as_ref()) {
                            (Some(size_value), TypedExpr::Int { int_value, .. })
                                if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into()
                                    && (&size_value % BigInt::from(8) == BigInt::ZERO) =>
                            {
                                let bytes = bit_array_segment_int_value_to_bytes(
                                    int_value.clone(),
                                    size_value,
                                    segment.endianness(),
                                );

                                u8_slice(arena, &bytes)
                            }

                            (Some(size_value), _) if size_value == 8.into() => value,

                            (Some(size_value), _) if size_value <= 0.into() => EMPTY_DOCUMENT,

                            _ => {
                                self.tracker.sized_integer_segment_used = true;
                                let size = details.size;
                                let is_big = bool(segment.endianness().is_big());
                                docvec![
                                    arena,
                                    SIZED_INT_OPEN_PAREN_DOCUMENT,
                                    value,
                                    COMMA_SPACE_DOCUMENT,
                                    size,
                                    COMMA_SPACE_DOCUMENT,
                                    is_big,
                                    CLOSE_PAREN_DOCUMENT
                                ]
                            }
                        }
                    }
                    BitArraySegmentType::Float => {
                        self.tracker.float_bit_array_segment_used = true;
                        let size = details.size;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            SIZED_FLOAT_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            size,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::String(StringEncoding::Utf8) => {
                        self.tracker.string_bit_array_segment_used = true;
                        docvec![
                            arena,
                            STRING_BITS_OPEN_PAREN_DOCUMENT,
                            value,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::String(StringEncoding::Utf16) => {
                        self.tracker.string_utf16_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            STRING_TO_UTF16_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::String(StringEncoding::Utf32) => {
                        self.tracker.string_utf32_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            STRING_TO_UTF32_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::UtfCodepoint(StringEncoding::Utf8) => {
                        self.tracker.codepoint_bit_array_segment_used = true;
                        docvec![
                            arena,
                            CODEPOINT_BITS_OPEN_PAREN_DOCUMENT,
                            value,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::UtfCodepoint(StringEncoding::Utf16) => {
                        self.tracker.codepoint_utf16_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            CODEPOINT_TO_UTF16_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::UtfCodepoint(StringEncoding::Utf32) => {
                        self.tracker.codepoint_utf32_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            CODEPOINT_TO_UTF32_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                }
            }),
        );

        docvec![
            arena,
            TO_BIT_ARRAY_OPEN_PAREN_DOCUMENT,
            segments_array,
            CLOSE_PAREN_DOCUMENT
        ]
    }

    fn bit_array_segment_details(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        segment: &'a TypedExprBitArraySegment,
    ) -> BitArraySegmentDetails<'a, 'doc> {
        let size = segment.size();
        let unit = segment.unit();
        let (size_value, size) = match size {
            Some(TypedExpr::Int { int_value, .. }) => {
                let size_value = int_value * unit;
                let size = eco_format!("{size_value}").to_doc(arena);
                (Some(size_value), size)
            }
            Some(size) => {
                let mut size = self.not_in_tail_position(Some(Ordering::Strict), |this| {
                    this.wrap_expression(arena, size)
                });

                if unit != 1 {
                    size = size.group(arena).append(
                        arena,
                        SPACE_TIMES_SPACE_DOCUMENT.append(arena, unit.to_doc(arena)),
                    );
                }

                (None, size)
            }

            None => {
                let size_value: usize = if segment.type_.is_int() { 8 } else { 64 };
                (Some(BigInt::from(size_value)), size_value.to_doc(arena))
            }
        };

        let type_ = BitArraySegmentType::from_segment(segment);

        BitArraySegmentDetails {
            type_,
            size,
            size_value,
            endianness: segment.endianness(),
        }
    }

    pub fn wrap_return(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        document: Document<'a, 'doc>,
    ) -> Document<'a, 'doc> {
        match &self.scope_position {
            Position::Tail => docvec![arena, RETURN_SPACE_DOCUMENT, document, SEMICOLON_DOCUMENT],
            Position::Expression(_) | Position::Statement => document,
            Position::Assign(name) => docvec![
                arena,
                name.clone(),
                SPACE_EQUAL_SPACE_DOCUMENT,
                document,
                SEMICOLON_DOCUMENT
            ],
        }
    }

    pub fn not_in_tail_position<CompileFn, Output>(
        &mut self,
        // If ordering is None, it is inherited from the parent scope.
        // It will be None in cases like `!x`, where `x` can be lifted
        // only if the ordering is already loose.
        ordering: Option<Ordering>,
        compile: CompileFn,
    ) -> Output
    where
        CompileFn: Fn(&mut Self) -> Output,
    {
        let new_ordering = ordering.unwrap_or(self.scope_position.ordering());

        let function_position = std::mem::replace(
            &mut self.function_position,
            Position::Expression(new_ordering),
        );
        let scope_position =
            std::mem::replace(&mut self.scope_position, Position::Expression(new_ordering));

        let result = compile(self);

        self.function_position = function_position;
        self.scope_position = scope_position;
        result
    }

    /// Use the `_block` variable if the expression is JS statement.
    pub fn wrap_expression(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        expression: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        match (expression, &self.scope_position) {
            (_, Position::Tail | Position::Assign(_)) => self.expression(arena, expression),
            (
                TypedExpr::Panic { .. }
                | TypedExpr::Todo { .. }
                | TypedExpr::Case { .. }
                | TypedExpr::Pipeline { .. }
                | TypedExpr::RecordUpdate {
                    // Record updates that assign a variable generate multiple statements
                    updated_record_assigned_name: Some(_),
                    ..
                },
                Position::Expression(Ordering::Loose),
            ) => self.wrap_block(arena, |this| this.expression(arena, expression)),
            (
                TypedExpr::Panic { .. }
                | TypedExpr::Todo { .. }
                | TypedExpr::Case { .. }
                | TypedExpr::Pipeline { .. }
                | TypedExpr::RecordUpdate {
                    // Record updates that assign a variable generate multiple statements
                    updated_record_assigned_name: Some(_),
                    ..
                },
                Position::Expression(Ordering::Strict),
            ) => self.immediately_invoked_function_expression(
                arena,
                expression,
                |this, expression| this.expression(arena, expression),
            ),
            _ => self.expression(arena, expression),
        }
    }

    /// Wrap an expression using the `_block` variable if required due to being
    /// a JS statement, or in parens if required due to being an operator or
    /// a function literal.
    pub fn child_expression(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        expression: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        match expression {
            TypedExpr::BinOp { operator, .. } if operator.is_operator_to_wrap() => {}
            TypedExpr::Fn { .. } => {}

            TypedExpr::Int { .. }
            | TypedExpr::Float { .. }
            | TypedExpr::String { .. }
            | TypedExpr::Block { .. }
            | TypedExpr::Pipeline { .. }
            | TypedExpr::Var { .. }
            | TypedExpr::List { .. }
            | TypedExpr::Call { .. }
            | TypedExpr::BinOp { .. }
            | TypedExpr::Case { .. }
            | TypedExpr::RecordAccess { .. }
            | TypedExpr::PositionalAccess { .. }
            | TypedExpr::ModuleSelect { .. }
            | TypedExpr::Tuple { .. }
            | TypedExpr::TupleIndex { .. }
            | TypedExpr::Todo { .. }
            | TypedExpr::Panic { .. }
            | TypedExpr::Echo { .. }
            | TypedExpr::BitArray { .. }
            | TypedExpr::RecordUpdate { .. }
            | TypedExpr::NegateBool { .. }
            | TypedExpr::NegateInt { .. }
            | TypedExpr::Invalid { .. } => return self.wrap_expression(arena, expression),
        }

        let document = self.expression(arena, expression);
        match &self.scope_position {
            // Here the document is a return statement: `return <expr>;`
            // or an assignment: `_block = <expr>;`
            Position::Tail | Position::Assign(_) | Position::Statement => document,
            Position::Expression(_) => {
                docvec![arena, OPEN_PAREN_DOCUMENT, document, CLOSE_PAREN_DOCUMENT]
            }
        }
    }

    /// Wrap an expression in an immediately invoked function expression
    fn immediately_invoked_function_expression<T, ToDoc>(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        statements: &'a T,
        to_doc: ToDoc,
    ) -> Document<'a, 'doc>
    where
        ToDoc: FnOnce(&mut Self, &'a T) -> Document<'a, 'doc>,
    {
        // Save initial state
        let scope_position = std::mem::replace(&mut self.scope_position, Position::Tail);
        let statement_level = std::mem::take(&mut self.statement_level);

        // Set state for in this iife
        let current_scope = self.current_scope.clone();

        // Generate the expression
        let result = to_doc(self, statements);
        let doc = self.add_statement_level(arena, result);
        let doc = immediately_invoked_function_expression_document(arena, doc);

        // Reset
        self.current_scope = current_scope;
        self.scope_position = scope_position;
        self.statement_level = statement_level;

        self.wrap_return(arena, doc)
    }

    fn wrap_block<CompileFn>(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        compile: CompileFn,
    ) -> Document<'a, 'doc>
    where
        CompileFn: Fn(&mut Self) -> Document<'a, 'doc>,
    {
        let block_variable = self.next_local_var(&BLOCK_VARIABLE.into());

        // Save initial state
        let scope_position = std::mem::replace(
            &mut self.scope_position,
            Position::Assign(block_variable.clone()),
        );
        let function_position = std::mem::replace(
            &mut self.function_position,
            Position::Expression(Ordering::Strict),
        );

        // Generate the expression
        let statement_doc = compile(self);

        // Reset
        self.scope_position = scope_position;
        self.function_position = function_position;

        self.statement_level.push(docvec![
            arena,
            LET_SPACE_DOCUMENT,
            block_variable.clone(),
            SEMICOLON_DOCUMENT
        ]);
        self.statement_level.push(statement_doc);

        self.wrap_return(arena, block_variable.to_doc(arena))
    }

    fn variable(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        name: &'a EcoString,
        constructor: &'a ValueConstructor,
    ) -> Document<'a, 'doc> {
        match &constructor.variant {
            ValueConstructorVariant::Record {
                arity,
                name: variant_name,
                ..
            } => {
                let type_ = constructor.type_.clone();
                self.record_constructor(arena, type_, None, variant_name, name, *arity)
            }
            ValueConstructorVariant::ModuleFn { .. }
            | ValueConstructorVariant::ModuleConstant { .. }
            | ValueConstructorVariant::LocalVariable { .. } => self.local_var(name).to_doc(arena),
        }
    }

    fn pipeline(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        first_value: &'a TypedPipelineAssignment,
        assignments: &'a [(TypedPipelineAssignment, PipelineAssignmentKind)],
        finally: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        let count = assignments.len();
        let mut documents = Vec::with_capacity((count + 2) * 2);

        let all_assignments = std::iter::once(first_value)
            .chain(assignments.iter().map(|(assignment, _kind)| assignment));

        let mut latest_local_var: Option<EcoString> = None;
        for assignment in all_assignments {
            // An echo in a pipeline won't result in an assignment, instead it
            // just prints the previous variable assigned in the pipeline.
            if let TypedExpr::Echo {
                expression: None,
                message,
                location,
                ..
            } = assignment.value.as_ref()
            {
                documents.push(self.not_in_tail_position(Some(Ordering::Strict), |this| {
                    let var = latest_local_var
                        .as_ref()
                        .expect("echo with no previous step in a pipe");
                    this.echo(arena, var.to_doc(arena), message.as_deref(), location)
                }));
                documents.push(SEMICOLON_DOCUMENT);
            } else {
                // Otherwise we assign the intermediate pipe value to a variable.
                let assignment_document =
                    self.not_in_tail_position(Some(Ordering::Strict), |this| {
                        this.simple_variable_assignment(
                            arena,
                            &assignment.name,
                            &assignment.value,
                            assignment.location,
                        )
                    });
                documents.push(self.add_statement_level(arena, assignment_document));
                latest_local_var = Some(self.local_var(&assignment.name));
            }

            documents.push(LINE_DOCUMENT);
        }

        if let TypedExpr::Echo {
            expression: None,
            message,
            location,
            ..
        } = finally
        {
            let var = latest_local_var.expect("echo with no previous step in a pipe");
            documents.push(self.echo(arena, var.to_doc(arena), message.as_deref(), location));
            match &self.scope_position {
                Position::Statement => documents.push(SEMICOLON_DOCUMENT),
                Position::Expression(_) | Position::Tail | Position::Assign(_) => {}
            }
        } else {
            let finally_doc = self.expression(arena, finally);
            documents.push(self.add_statement_level(arena, finally_doc));
        }

        arena.concat(documents).force_break(arena)
    }

    pub(crate) fn expression_flattening_blocks(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        expression: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        if let TypedExpr::Block { statements, .. } = expression {
            self.statements(arena, statements)
        } else {
            self.expression(arena, expression)
        }
    }

    fn block(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        statements: &'a Vec1<TypedStatement>,
    ) -> Document<'a, 'doc> {
        if statements.len() == 1 {
            match statements.first() {
                Statement::Expression(expression) => {
                    return self.child_expression(arena, expression);
                }

                Statement::Assignment(assignment) => match &assignment.kind {
                    AssignmentKind::Let | AssignmentKind::Generated => {
                        return self.child_expression(arena, &assignment.value);
                    }
                    // We can't just return the right-hand side of a `let assert`
                    // assignment; we still need to check that the pattern matches.
                    AssignmentKind::Assert { .. } => {}
                },

                Statement::Use(use_) => return self.child_expression(arena, &use_.call),

                // Similar to `let assert`, we can't immediately return the value
                // that is asserted; we have to actually perform the assertion.
                Statement::Assert(_) => {}
            }
        }
        match &self.scope_position {
            Position::Tail | Position::Assign(_) | Position::Statement => {
                self.block_document(arena, statements)
            }
            Position::Expression(Ordering::Strict) => self.immediately_invoked_function_expression(
                arena,
                statements,
                |this, statements| this.statements(arena, statements),
            ),
            Position::Expression(Ordering::Loose) => self.wrap_block(arena, |this| {
                // Save previous scope
                let current_scope = this.current_scope.clone();

                let document = this.block_document(arena, statements);

                // Restore previous state
                this.current_scope = current_scope;

                document
            }),
        }
    }

    fn block_document(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        statements: &'a Vec1<TypedStatement>,
    ) -> Document<'a, 'doc> {
        let statements = self.statements(arena, statements);
        docvec![
            arena,
            OPEN_CURLY_DOCUMENT,
            docvec![arena, LINE_DOCUMENT, statements].nest(arena, INDENT),
            LINE_DOCUMENT,
            CLOSE_CURLY_DOCUMENT
        ]
    }

    fn statements(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        statements: &'a [TypedStatement],
    ) -> Document<'a, 'doc> {
        // If there are any statements that need to be printed at statement level, that's
        // for an outer scope so we don't want to print them inside this one.
        let statement_level = std::mem::take(&mut self.statement_level);
        let count = statements.len();
        let mut documents = Vec::with_capacity(count * 3);
        for (i, statement) in statements.iter().enumerate() {
            if i + 1 < count {
                let function_position =
                    std::mem::replace(&mut self.function_position, Position::Statement);
                let scope_position =
                    std::mem::replace(&mut self.scope_position, Position::Statement);

                documents.push(self.statement(arena, statement));

                self.function_position = function_position;
                self.scope_position = scope_position;

                documents.push(LINE_DOCUMENT);
            } else {
                documents.push(self.statement(arena, statement));
            }

            // If we've generated code for a statement that always throws, we
            // can skip code generation for all the following ones.
            if self.let_assert_always_panics {
                self.let_assert_always_panics = false;
                break;
            }
        }
        self.statement_level = statement_level;
        if count == 1 {
            arena.concat(documents)
        } else {
            arena.concat(documents).force_break(arena)
        }
    }

    fn simple_variable_assignment(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        name: &'a EcoString,
        value: &'a TypedExpr,
        location: SrcSpan,
    ) -> Document<'a, 'doc> {
        // Subject must be rendered before the variable for variable numbering
        let subject = self.not_in_tail_position(Some(Ordering::Loose), |this| {
            this.wrap_expression(arena, value)
        });
        let js_name = self.next_local_var(name);
        let assignment = docvec![
            arena,
            self.source_map_tracker(arena, location.start),
            LET_SPACE_DOCUMENT,
            js_name.clone(),
            SPACE_EQUAL_SPACE_DOCUMENT,
            subject,
            SEMICOLON_DOCUMENT
        ];
        let assignment = match &self.scope_position {
            Position::Expression(_) | Position::Statement => assignment,
            Position::Tail => docvec![
                arena,
                assignment,
                LINE_DOCUMENT,
                RETURN_SPACE_DOCUMENT,
                js_name,
                SEMICOLON_DOCUMENT
            ],
            Position::Assign(block_variable) => docvec![
                arena,
                assignment,
                LINE_DOCUMENT,
                block_variable.clone(),
                SPACE_EQUAL_SPACE_DOCUMENT,
                js_name,
                SEMICOLON_DOCUMENT
            ],
        };

        assignment.force_break(arena)
    }

    fn assignment(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        assignment: &'a TypedAssignment,
    ) -> Document<'a, 'doc> {
        let TypedAssignment {
            pattern,
            kind,
            value,
            compiled_case,
            location,
            annotation: _,
        } = assignment;

        // In case the pattern is just a variable, we special case it to
        // generate just a simple assignment instead of using the decision tree
        // for the code generation step.
        if let TypedPattern::Variable { name, .. } = pattern {
            return self.simple_variable_assignment(arena, name, value, *location);
        }

        docvec![
            arena,
            self.source_map_tracker(arena, location.start),
            decision::let_(arena, compiled_case, value, kind, self, pattern)
        ]
    }

    fn assert(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        assert: &'a TypedAssert,
    ) -> Document<'a, 'doc> {
        let TypedAssert {
            location,
            value,
            message,
        } = assert;

        let message = match message {
            Some(message) => self.not_in_tail_position(Some(Ordering::Strict), |this| {
                this.expression(arena, message)
            }),
            None => string(arena, "Assertion failed."),
        };

        let check = self.not_in_tail_position(Some(Ordering::Loose), |this| {
            this.assert_check(arena, value, &message, *location)
        });

        match &self.scope_position {
            Position::Expression(_) | Position::Statement => check,
            Position::Tail | Position::Assign(_) => {
                docvec![
                    arena,
                    check,
                    LINE_DOCUMENT,
                    self.wrap_return(arena, UNDEFINED_DOCUMENT)
                ]
            }
        }
    }

    fn assert_check(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        subject: &'a TypedExpr,
        message: &Document<'a, 'doc>,
        location: SrcSpan,
    ) -> Document<'a, 'doc> {
        let (subject_document, mut fields) = match subject {
            TypedExpr::Call { fun, arguments, .. } => {
                let argument_variables = arguments
                    .iter()
                    .map(|element| {
                        self.not_in_tail_position(Some(Ordering::Strict), |this| {
                            this.assign_to_variable(arena, &element.value)
                        })
                    })
                    .collect_vec();
                (
                    self.call_with_doc_arguments(arena, fun, argument_variables.clone()),
                    vec![
                        ("kind", string(arena, "function_call")),
                        (
                            "arguments",
                            array(
                                arena,
                                argument_variables.into_iter().zip(arguments).map(
                                    |(variable, argument)| {
                                        self.asserted_expression(
                                            arena,
                                            AssertExpression::from_expression(&argument.value),
                                            Some(variable),
                                            argument.location(),
                                        )
                                    },
                                ),
                            ),
                        ),
                    ],
                )
            }

            TypedExpr::BinOp {
                operator,
                left,
                right,
                ..
            } => {
                match operator {
                    BinOp::And => return self.assert_and(arena, left, right, message, location),
                    BinOp::Or => return self.assert_or(arena, left, right, message, location),
                    BinOp::Eq
                    | BinOp::NotEq
                    | BinOp::LtInt
                    | BinOp::LtEqInt
                    | BinOp::LtFloat
                    | BinOp::LtEqFloat
                    | BinOp::GtEqInt
                    | BinOp::GtInt
                    | BinOp::GtEqFloat
                    | BinOp::GtFloat
                    | BinOp::AddInt
                    | BinOp::AddFloat
                    | BinOp::SubInt
                    | BinOp::SubFloat
                    | BinOp::MultInt
                    | BinOp::MultFloat
                    | BinOp::DivInt
                    | BinOp::DivFloat
                    | BinOp::RemainderInt
                    | BinOp::Concatenate => {}
                }

                let left_document = self.not_in_tail_position(Some(Ordering::Loose), |this| {
                    this.assign_to_variable(arena, left)
                });
                let right_document = self.not_in_tail_position(Some(Ordering::Loose), |this| {
                    this.assign_to_variable(arena, right)
                });

                (
                    self.bin_op_with_doc_operands(
                        arena,
                        *operator,
                        left_document,
                        right_document,
                        &left.type_(),
                    )
                    .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT),
                    vec![
                        ("kind", string(arena, "binary_operator")),
                        ("operator", string(arena, operator.name())),
                        (
                            "left",
                            self.asserted_expression(
                                arena,
                                AssertExpression::from_expression(left),
                                Some(left_document),
                                left.location(),
                            ),
                        ),
                        (
                            "right",
                            self.asserted_expression(
                                arena,
                                AssertExpression::from_expression(right),
                                Some(right_document),
                                right.location(),
                            ),
                        ),
                    ],
                )
            }

            TypedExpr::Int { .. }
            | TypedExpr::Float { .. }
            | TypedExpr::String { .. }
            | TypedExpr::Block { .. }
            | TypedExpr::Pipeline { .. }
            | TypedExpr::Var { .. }
            | TypedExpr::Fn { .. }
            | TypedExpr::List { .. }
            | TypedExpr::Case { .. }
            | TypedExpr::RecordAccess { .. }
            | TypedExpr::PositionalAccess { .. }
            | TypedExpr::ModuleSelect { .. }
            | TypedExpr::Tuple { .. }
            | TypedExpr::TupleIndex { .. }
            | TypedExpr::Todo { .. }
            | TypedExpr::Panic { .. }
            | TypedExpr::Echo { .. }
            | TypedExpr::BitArray { .. }
            | TypedExpr::RecordUpdate { .. }
            | TypedExpr::NegateBool { .. }
            | TypedExpr::NegateInt { .. }
            | TypedExpr::Invalid { .. } => (
                self.wrap_expression(arena, subject),
                vec![
                    ("kind", string(arena, "expression")),
                    (
                        "expression",
                        self.asserted_expression(
                            arena,
                            AssertExpression::from_expression(subject),
                            Some(FALSE_LOWERCASE_DOCUMENT),
                            subject.location(),
                        ),
                    ),
                ],
            ),
        };

        fields.push(("start", location.start.to_doc(arena)));
        fields.push(("end", subject.location().end.to_doc(arena)));
        fields.push(("expression_start", subject.location().start.to_doc(arena)));

        docvec![
            arena,
            self.source_map_tracker(arena, location.start),
            IF_SPACE_OPEN_PAREN_DOCUMENT,
            docvec![arena, EXCLAMATION_MARK_DOCUMENT, subject_document].nest(arena, INDENT),
            EMPTY_BREAK_DOCUMENT,
            CLOSE_PAREN_SPACE_OPEN_CURLY_DOCUMENT,
            docvec![
                arena,
                LINE_DOCUMENT,
                self.throw_error(arena, "assert", message, location, fields),
            ]
            .nest(arena, INDENT),
            LINE_DOCUMENT,
            CLOSE_CURLY_DOCUMENT,
        ]
        .group(arena)
    }

    fn negate_bool_expression(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        value: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        match value {
            TypedExpr::BinOp {
                operator,
                left,
                right,
                ..
            } => match operator {
                BinOp::And => self.print_bin_op(arena, left, right, "||"),
                BinOp::Or => self.print_bin_op(arena, left, right, "&&"),
                BinOp::Eq => self.equal(arena, left, right, false),
                BinOp::NotEq => self.equal(arena, left, right, true),
                BinOp::LtInt | BinOp::LtFloat => self.print_bin_op(arena, left, right, ">="),
                BinOp::LtEqInt | BinOp::LtEqFloat => self.print_bin_op(arena, left, right, ">"),
                BinOp::GtInt | BinOp::GtFloat => self.print_bin_op(arena, left, right, "<="),
                BinOp::GtEqInt | BinOp::GtEqFloat => self.print_bin_op(arena, left, right, "<"),
                BinOp::AddInt
                | BinOp::AddFloat
                | BinOp::SubInt
                | BinOp::SubFloat
                | BinOp::MultInt
                | BinOp::MultFloat
                | BinOp::DivInt
                | BinOp::DivFloat
                | BinOp::RemainderInt
                | BinOp::Concatenate => unreachable!("type checking should make this impossible"),
            },
            TypedExpr::NegateBool { value, .. } => self.wrap_expression(arena, value),
            TypedExpr::Int { .. }
            | TypedExpr::Float { .. }
            | TypedExpr::String { .. }
            | TypedExpr::Block { .. }
            | TypedExpr::Pipeline { .. }
            | TypedExpr::Var { .. }
            | TypedExpr::Fn { .. }
            | TypedExpr::List { .. }
            | TypedExpr::Call { .. }
            | TypedExpr::Case { .. }
            | TypedExpr::RecordAccess { .. }
            | TypedExpr::PositionalAccess { .. }
            | TypedExpr::ModuleSelect { .. }
            | TypedExpr::Tuple { .. }
            | TypedExpr::TupleIndex { .. }
            | TypedExpr::Todo { .. }
            | TypedExpr::Panic { .. }
            | TypedExpr::Echo { .. }
            | TypedExpr::BitArray { .. }
            | TypedExpr::RecordUpdate { .. }
            | TypedExpr::NegateInt { .. }
            | TypedExpr::Invalid { .. } => docvec![
                arena,
                EXCLAMATION_MARK_DOCUMENT,
                self.wrap_expression(arena, value)
            ],
        }
    }

    /// In Gleam, the `&&` operator is short-circuiting, meaning that we can't
    /// pre-evaluate both sides of it, and use them in the exception that is
    /// thrown.
    /// Instead, we need to implement this short-circuiting logic ourself.
    ///
    /// If we short-circuit, we must leave the second expression unevaluated,
    /// and signal that using the `unevaluated` variant, as detailed in the
    /// exception format. For the first expression, we know it must be `false`,
    /// otherwise we would have continued by evaluating the second expression.
    ///
    /// Similarly, if we do evaluate the second expression and fail, we know
    /// that the first expression must have evaluated to `true`, and the second
    /// to `false`. This way, we avoid needing to evaluate either expression
    /// twice.
    ///
    /// The generated code then looks something like this:
    /// ```javascript
    /// if (expr1) {
    ///   if (!expr2) {
    ///     <throw exception>
    ///   }
    /// } else {
    ///   <throw exception>
    /// }
    /// ```
    ///
    fn assert_and(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
        message: &Document<'a, 'doc>,
        location: SrcSpan,
    ) -> Document<'a, 'doc> {
        let left_kind = AssertExpression::from_expression(left);
        let right_kind = AssertExpression::from_expression(right);

        let fields_if_short_circuiting = vec![
            ("kind", string(arena, "binary_operator")),
            ("operator", string(arena, "&&")),
            (
                "left",
                self.asserted_expression(
                    arena,
                    left_kind,
                    Some(FALSE_LOWERCASE_DOCUMENT),
                    left.location(),
                ),
            ),
            (
                "right",
                self.asserted_expression(
                    arena,
                    AssertExpression::Unevaluated,
                    None,
                    right.location(),
                ),
            ),
            ("start", location.start.to_doc(arena)),
            ("end", right.location().end.to_doc(arena)),
            ("expression_start", left.location().start.to_doc(arena)),
        ];

        let fields = vec![
            ("kind", string(arena, "binary_operator")),
            ("operator", string(arena, "&&")),
            (
                "left",
                self.asserted_expression(
                    arena,
                    left_kind,
                    Some(TRUE_LOWERCASE_DOCUMENT),
                    left.location(),
                ),
            ),
            (
                "right",
                self.asserted_expression(
                    arena,
                    right_kind,
                    Some(FALSE_LOWERCASE_DOCUMENT),
                    right.location(),
                ),
            ),
            ("start", location.start.to_doc(arena)),
            ("end", right.location().end.to_doc(arena)),
            ("expression_start", left.location().start.to_doc(arena)),
        ];

        let left_value = self.not_in_tail_position(Some(Ordering::Loose), |this| {
            this.wrap_expression(arena, left)
        });

        let right_value = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.negate_bool_expression(arena, right)
        });

        let right_check = docvec![
            arena,
            LINE_DOCUMENT,
            IF_SPACE_OPEN_PAREN_DOCUMENT,
            right_value.nest(arena, INDENT),
            CLOSE_PAREN_SPACE_OPEN_CURLY_DOCUMENT,
            docvec![
                arena,
                LINE_DOCUMENT,
                self.throw_error(arena, "assert", message, location, fields)
            ]
            .nest(arena, INDENT),
            LINE_DOCUMENT,
            CLOSE_CURLY_DOCUMENT,
        ];

        docvec![
            arena,
            self.source_map_tracker(arena, location.start),
            IF_SPACE_OPEN_PAREN_DOCUMENT,
            left_value.nest(arena, INDENT),
            CLOSE_PAREN_SPACE_OPEN_CURLY_DOCUMENT,
            right_check.nest(arena, INDENT),
            LINE_DOCUMENT,
            CLOSE_CURLY_ELSE_OPEN_CURLY_DOCUMENT,
            docvec![
                arena,
                LINE_DOCUMENT,
                self.throw_error(
                    arena,
                    "assert",
                    message,
                    location,
                    fields_if_short_circuiting
                )
            ]
            .nest(arena, INDENT),
            LINE_DOCUMENT,
            CLOSE_CURLY_DOCUMENT
        ]
    }

    /// Similar to `&&`, `||` is also short-circuiting in Gleam. However, if `||`
    /// short-circuits, that's because the first expression evaluated to `true`,
    /// meaning the whole assertion succeeds. This allows us to directly use the
    /// `||` operator in JavaScript.
    ///
    /// The only difference is that due to the nature of `||`, if the assertion fails,
    /// we know that both sides must have evaluated to `false`, so we don't
    /// need to store the values of them in variables beforehand.
    fn assert_or(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
        message: &Document<'a, 'doc>,
        location: SrcSpan,
    ) -> Document<'a, 'doc> {
        let fields = vec![
            ("kind", string(arena, "binary_operator")),
            ("operator", string(arena, "||")),
            (
                "left",
                self.asserted_expression(
                    arena,
                    AssertExpression::from_expression(left),
                    Some(FALSE_LOWERCASE_DOCUMENT),
                    left.location(),
                ),
            ),
            (
                "right",
                self.asserted_expression(
                    arena,
                    AssertExpression::from_expression(right),
                    Some(FALSE_LOWERCASE_DOCUMENT),
                    right.location(),
                ),
            ),
            ("start", location.start.to_doc(arena)),
            ("end", right.location().end.to_doc(arena)),
            ("expression_start", left.location().start.to_doc(arena)),
        ];

        let left_value = self.not_in_tail_position(Some(Ordering::Loose), |this| {
            this.child_expression(arena, left)
        });

        let right_value = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, right)
        });

        docvec![
            arena,
            LINE_DOCUMENT,
            self.source_map_tracker(arena, location.start),
            IF_SPACE_OPEN_PAREN_DOCUMENT,
            docvec![
                arena,
                EXCLAMATION_MARK_OPEN_PAREN_DOCUMENT,
                left_value,
                SPACE_DOUBLE_VERTICAL_BAR_SPACE_DOCUMENT,
                right_value,
                CLOSE_PAREN_DOCUMENT
            ]
            .nest(arena, INDENT),
            CLOSE_PAREN_SPACE_OPEN_CURLY_DOCUMENT,
            docvec![
                arena,
                LINE_DOCUMENT,
                self.throw_error(arena, "assert", message, location, fields)
            ]
            .nest(arena, INDENT),
            LINE_DOCUMENT,
            CLOSE_CURLY_DOCUMENT,
        ]
    }

    fn assign_to_variable(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        value: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        if let TypedExpr::Var { .. } = value {
            self.expression(arena, value)
        } else {
            let value = self.wrap_expression(arena, value);
            let variable = self.next_local_var(&ASSIGNMENT_VAR.into());
            let assignment = docvec![
                arena,
                LET_SPACE_DOCUMENT,
                variable.clone(),
                SPACE_EQUAL_SPACE_DOCUMENT,
                value,
                SEMICOLON_DOCUMENT
            ];
            self.statement_level.push(assignment);
            variable.to_doc(arena)
        }
    }

    fn asserted_expression(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        kind: AssertExpression,
        value: Option<Document<'a, 'doc>>,
        location: SrcSpan,
    ) -> Document<'a, 'doc> {
        let kind = match kind {
            AssertExpression::Literal => string(arena, "literal"),
            AssertExpression::Expression => string(arena, "expression"),
            AssertExpression::Unevaluated => string(arena, "unevaluated"),
        };

        let start = location.start.to_doc(arena);
        let end = location.end.to_doc(arena);
        let items = if let Some(value) = value {
            vec![
                ("kind", kind),
                ("value", value),
                ("start", start),
                ("end", end),
            ]
        } else {
            vec![("kind", kind), ("start", start), ("end", end)]
        };

        wrap_object(
            arena,
            items
                .into_iter()
                .map(|(key, value)| (key.to_doc(arena), Some(value))),
        )
    }

    fn tuple(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        elements: &'a [TypedExpr],
    ) -> Document<'a, 'doc> {
        self.not_in_tail_position(Some(Ordering::Strict), |this| {
            array(
                arena,
                elements
                    .iter()
                    .map(|element| this.wrap_expression(arena, element)),
            )
        })
    }

    fn call(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        fun: &'a TypedExpr,
        arguments: &'a [TypedCallArg],
    ) -> Document<'a, 'doc> {
        let arguments = arguments
            .iter()
            .map(|element| {
                self.not_in_tail_position(Some(Ordering::Strict), |this| {
                    this.wrap_expression(arena, &element.value)
                })
            })
            .collect_vec();

        self.call_with_doc_arguments(arena, fun, arguments)
    }

    fn call_with_doc_arguments(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        fun: &'a TypedExpr,
        arguments: Vec<Document<'a, 'doc>>,
    ) -> Document<'a, 'doc> {
        match fun {
            // Qualified record construction
            TypedExpr::ModuleSelect {
                constructor: ModuleValueConstructor::Record { name, .. },
                module_alias,
                ..
            } => self.wrap_return(
                arena,
                construct_record(arena, Some(module_alias), name, arguments),
            ),

            // Record construction
            TypedExpr::Var {
                constructor:
                    ValueConstructor {
                        variant: ValueConstructorVariant::Record { .. },
                        type_,
                        ..
                    },
                name,
                ..
            } => {
                if type_.is_result_constructor() {
                    if name == "Ok" {
                        self.tracker.ok_used = true;
                    } else if name == "Error" {
                        self.tracker.error_used = true;
                    }
                }
                self.wrap_return(arena, construct_record(arena, None, name, arguments))
            }

            // Tail call optimisation. If we are calling the current function
            // and we are in tail position we can avoid creating a new stack
            // frame, enabling recursion with constant memory usage.
            TypedExpr::Var { name, .. }
                if self.function_name == *name
                    && self.current_function.can_recurse()
                    && self.function_position.is_tail()
                    && self.current_scope.counter(name) == Some(0) =>
            {
                // Record that tail recursion is happening so that we know to
                // render the loop at the top level of the function.
                self.tail_recursion_used = true;
                arena.concat(
                    arguments
                        .into_iter()
                        .zip(&self.function_arguments)
                        .enumerate()
                        .map(|(i, (element, argument))| {
                            let mut doc = EMPTY_DOCUMENT;

                            if i != 0 {
                                doc = doc.append(arena, LINE_DOCUMENT);
                            }
                            // Create an assignment for each variable created by the function arguments
                            if let Some(name) = argument {
                                doc = docvec![
                                    arena,
                                    doc,
                                    LOOP_DOLLAR_DOCUMENT,
                                    name.to_doc(arena),
                                    SPACE_EQUAL_SPACE_DOCUMENT
                                ]
                            }
                            // Render the value given to the function. Even if it is not
                            // assigned we still render it because the expression may
                            // have some side effects.
                            docvec![arena, doc, element, SEMICOLON_DOCUMENT]
                        }),
                )
            }

            TypedExpr::Int { .. }
            | TypedExpr::Float { .. }
            | TypedExpr::String { .. }
            | TypedExpr::Block { .. }
            | TypedExpr::Pipeline { .. }
            | TypedExpr::Var { .. }
            | TypedExpr::Fn { .. }
            | TypedExpr::List { .. }
            | TypedExpr::Call { .. }
            | TypedExpr::BinOp { .. }
            | TypedExpr::Case { .. }
            | TypedExpr::RecordAccess { .. }
            | TypedExpr::PositionalAccess { .. }
            | TypedExpr::ModuleSelect { .. }
            | TypedExpr::Tuple { .. }
            | TypedExpr::TupleIndex { .. }
            | TypedExpr::Todo { .. }
            | TypedExpr::Panic { .. }
            | TypedExpr::Echo { .. }
            | TypedExpr::BitArray { .. }
            | TypedExpr::RecordUpdate { .. }
            | TypedExpr::NegateBool { .. }
            | TypedExpr::NegateInt { .. }
            | TypedExpr::Invalid { .. } => {
                let fun = self.not_in_tail_position(None, |this| -> Document<'_, '_> {
                    let is_fn_literal = matches!(fun, TypedExpr::Fn { .. });
                    let fun = this.wrap_expression(arena, fun);
                    if is_fn_literal {
                        docvec![arena, OPEN_PAREN_DOCUMENT, fun, CLOSE_PAREN_DOCUMENT]
                    } else {
                        fun
                    }
                });
                let arguments = call_arguments(arena, arguments);
                self.wrap_return(arena, docvec![arena, fun, arguments])
            }
        }
    }

    fn fn_(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        arguments: &'a [TypedArg],
        body: &'a [TypedStatement],
        kind: &FunctionLiteralKind,
    ) -> Document<'a, 'doc> {
        // New function, this is now the tail position
        let function_position = std::mem::replace(&mut self.function_position, Position::Tail);
        let scope_position = std::mem::replace(&mut self.scope_position, Position::Tail);

        // And there's a new scope
        let scope = self.current_scope.clone();
        for name in arguments.iter().flat_map(Arg::get_variable_name) {
            self.current_scope.set_counter(name, 0);
        }

        // This is a new function so track that so that we don't
        // mistakenly trigger tail call optimisation
        let mut current_function = CurrentFunction::Anonymous;
        std::mem::swap(&mut self.current_function, &mut current_function);

        // Generate the function body
        let result = self.statements(arena, body);

        // Reset function name, scope, and tail position tracking
        self.function_position = function_position;
        self.scope_position = scope_position;
        self.current_scope = scope;
        std::mem::swap(&mut self.current_function, &mut current_function);

        let mut docs = EMPTY_DOCUMENT;

        // If the function is a use then we need to add a source map tracker
        // before the result to denote that the function is created by the use
        if let FunctionLiteralKind::Use { location } = kind {
            docs = docs.append(arena, self.source_map_tracker(arena, location.start));
        }
        docs = docs.append(arena, fun_arguments(arena, arguments, false));
        docs = docs.append(arena, SPACE_EQUAL_ARROW_SPACE_OPEN_CURLY_DOCUMENT);
        docs = docs.append(arena, BREAKABLE_SPACE_DOCUMENT);
        docs = docs.append(arena, result);

        docvec![
            arena,
            docs.nest(arena, INDENT)
                .append(arena, BREAKABLE_SPACE_DOCUMENT)
                .group(arena),
            CLOSE_CURLY_DOCUMENT,
        ]
    }

    fn record_access(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        record: &'a TypedExpr,
        label: &'a str,
    ) -> Document<'a, 'doc> {
        self.not_in_tail_position(None, |this| {
            let record = this.wrap_expression(arena, record);
            docvec![arena, record, DOT_DOCUMENT, maybe_escape_property(label)]
        })
    }

    fn positional_access(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        record: &'a TypedExpr,
        index: u64,
    ) -> Document<'a, 'doc> {
        self.not_in_tail_position(None, |this| {
            let record = this.wrap_expression(arena, record);
            docvec![
                arena,
                record,
                OPEN_SQUARE_DOCUMENT,
                index,
                CLOSE_SQUARE_DOCUMENT
            ]
        })
    }

    fn record_update(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        updated_record_assigned_name: &'a Option<EcoString>,
        updated_record: &'a TypedExpr,
        constructor: &'a TypedExpr,
        arguments: &'a [TypedCallArg],
    ) -> Document<'a, 'doc> {
        match updated_record_assigned_name.as_ref() {
            Some(name) => {
                docvec![
                    arena,
                    self.not_in_tail_position(None, |this| this.simple_variable_assignment(
                        arena,
                        name,
                        updated_record,
                        updated_record.location(),
                    )),
                    LINE_DOCUMENT,
                    self.call(arena, constructor, arguments),
                ]
            }
            None => self.call(arena, constructor, arguments),
        }
    }

    fn tuple_index(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        tuple: &'a TypedExpr,
        index: u64,
    ) -> Document<'a, 'doc> {
        self.not_in_tail_position(None, |this| {
            let tuple = this.wrap_expression(arena, tuple);
            docvec![arena, tuple, eco_format!("[{index}]")]
        })
    }

    fn bin_op(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        name: &'a BinOp,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        match name {
            BinOp::And => self.print_bin_op(arena, left, right, "&&"),
            BinOp::Or => self.print_bin_op(arena, left, right, "||"),
            BinOp::LtInt | BinOp::LtFloat => self.print_bin_op(arena, left, right, "<"),
            BinOp::LtEqInt | BinOp::LtEqFloat => self.print_bin_op(arena, left, right, "<="),
            BinOp::Eq => self.equal(arena, left, right, true),
            BinOp::NotEq => self.equal(arena, left, right, false),
            BinOp::GtInt | BinOp::GtFloat => self.print_bin_op(arena, left, right, ">"),
            BinOp::GtEqInt | BinOp::GtEqFloat => self.print_bin_op(arena, left, right, ">="),
            BinOp::Concatenate | BinOp::AddInt | BinOp::AddFloat => {
                self.print_bin_op(arena, left, right, "+")
            }
            BinOp::SubInt | BinOp::SubFloat => self.print_bin_op(arena, left, right, "-"),
            BinOp::MultInt | BinOp::MultFloat => self.print_bin_op(arena, left, right, "*"),
            BinOp::RemainderInt => self.remainder_int(arena, left, right),
            BinOp::DivInt => self.div_int(arena, left, right),
            BinOp::DivFloat => self.div_float(arena, left, right),
        }
    }

    fn div_int(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, left)
        });
        let right_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, right)
        });

        // If we have a constant value divided by zero then it's safe to replace
        // it directly with 0.
        if left.is_literal() && right.is_zero_compile_time_number() {
            "0".to_doc(arena)
        } else if right.is_non_zero_compile_time_number() {
            let division = if let TypedExpr::BinOp { .. } = left {
                docvec![
                    arena,
                    left_doc.surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT),
                    SPACE_SLASH_SPACE_DOCUMENT,
                    right_doc
                ]
            } else {
                docvec![arena, left_doc, SPACE_SLASH_SPACE_DOCUMENT, right_doc]
            };
            docvec![
                arena,
                GLOBAL_THIS_DOT_MATH_DOT_TRUNC_DOCUMENT,
                wrap_arguments(arena, [division])
            ]
        } else {
            self.tracker.int_division_used = true;
            docvec![
                arena,
                DIVIDE_INT_DOCUMENT,
                wrap_arguments(arena, [left_doc, right_doc])
            ]
        }
    }

    fn remainder_int(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, left)
        });
        let right_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, right)
        });

        // If we have a constant value divided by zero then it's safe to replace
        // it directly with 0.
        if left.is_literal() && right.is_zero_compile_time_number() {
            "0".to_doc(arena)
        } else if right.is_non_zero_compile_time_number() {
            if let TypedExpr::BinOp { .. } = left {
                docvec![
                    arena,
                    left_doc.surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT),
                    SPACE_MODULO_SPACE_DOCUMENT,
                    right_doc
                ]
            } else {
                docvec![arena, left_doc, SPACE_MODULO_SPACE_DOCUMENT, right_doc]
            }
        } else {
            self.tracker.int_remainder_used = true;
            docvec![
                arena,
                "remainderInt",
                wrap_arguments(arena, [left_doc, right_doc])
            ]
        }
    }

    fn div_float(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
    ) -> Document<'a, 'doc> {
        let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, left)
        });
        let right_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, right)
        });

        // If we have a constant value divided by zero then it's safe to replace
        // it directly with 0.
        if left.is_literal() && right.is_zero_compile_time_number() {
            "0.0".to_doc(arena)
        } else if right.is_non_zero_compile_time_number() {
            if let TypedExpr::BinOp { .. } = left {
                docvec![
                    arena,
                    left_doc.surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT),
                    SPACE_SLASH_SPACE_DOCUMENT,
                    right_doc
                ]
            } else {
                docvec![arena, left_doc, SPACE_SLASH_SPACE_DOCUMENT, right_doc]
            }
        } else {
            self.tracker.float_division_used = true;
            docvec![
                arena,
                "divideFloat",
                wrap_arguments(arena, [left_doc, right_doc])
            ]
        }
    }

    fn equal(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
        should_be_equal: bool,
    ) -> Document<'a, 'doc> {
        // If it is a simple scalar type then we can use JS' reference identity
        if is_js_scalar(left.type_()) {
            let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
                this.child_expression(arena, left)
            });
            let right_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
                this.child_expression(arena, right)
            });
            let operator = if should_be_equal {
                SPACE_TRIPLE_EQUAL_SPACE_DOCUMENT
            } else {
                SPACE_EXCLAMATION_MARK_DOUBLE_EQUAL_SPACE_DOCUMENT
            };
            return docvec![arena, left_doc, operator, right_doc];
        }

        // For comparison with singleton custom types, ie, one with no fields.
        // If you have some code like this
        // ```gleam
        //  pub type Wibble {
        //    Wibble
        //    Wobble
        //  }
        //
        //  pub fn is_wibble(w: Wibble) -> Bool {
        //    w == Wibble
        //  }
        // ```
        // Instead of `isEqual(w, new Wibble())`, generate `w instanceof Wibble`
        // because the first approach needs to construct a new Wibble, and then call the isEqual function,
        // which supports any shape of data, and so does a lot of extra logic which isn't necessary.

        if let Some(doc) = self.singleton_variant_equality(arena, left, right, should_be_equal) {
            return doc;
        }

        if let Some(doc) = self.singleton_variant_equality(arena, right, left, should_be_equal) {
            return doc;
        }

        // Other types must be compared using structural equality
        let left = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.wrap_expression(arena, left)
        });
        let right = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.wrap_expression(arena, right)
        });

        self.prelude_equal_call(arena, should_be_equal, left, right)
    }

    fn singleton_variant_equality(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
        should_be_equal: bool,
    ) -> Option<Document<'a, 'doc>> {
        match right {
            TypedExpr::Var {
                name,
                constructor:
                    ValueConstructor {
                        variant:
                            ValueConstructorVariant::Record {
                                arity: 0,
                                name: variant_name,
                                ..
                            },
                        ..
                    },
                ..
            } => {
                let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
                    this.wrap_expression(arena, left)
                });
                Some(self.singleton_equal(
                    arena,
                    left_doc,
                    None,
                    name.clone(),
                    should_be_equal,
                    variant_name.clone(),
                    right.type_(),
                ))
            }
            TypedExpr::ModuleSelect {
                module_alias,
                constructor: ModuleValueConstructor::Record { arity: 0, name, .. },
                ..
            } => {
                let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
                    this.wrap_expression(arena, left)
                });
                Some(self.singleton_equal(
                    arena,
                    left_doc,
                    Some(module_alias),
                    name.clone(),
                    should_be_equal,
                    name.clone(),
                    right.type_(),
                ))
            }
            // Empty lists are implemented as a variant with no fields, so we can
            // use `instanceof` for a faster check.
            TypedExpr::List { elements, .. } if elements.is_empty() => {
                let left_doc = self.not_in_tail_position(Some(Ordering::Strict), |this| {
                    this.wrap_expression(arena, left)
                });
                self.tracker.list_empty_class_used = true;
                Some(self.singleton_equal(
                    arena,
                    left_doc,
                    None,
                    "$Empty".into(),
                    should_be_equal,
                    "Empty".into(),
                    right.type_(),
                ))
            }
            TypedExpr::Int { .. }
            | TypedExpr::Float { .. }
            | TypedExpr::String { .. }
            | TypedExpr::Block { .. }
            | TypedExpr::Pipeline { .. }
            | TypedExpr::Var { .. }
            | TypedExpr::Fn { .. }
            | TypedExpr::List { .. }
            | TypedExpr::Call { .. }
            | TypedExpr::BinOp { .. }
            | TypedExpr::Case { .. }
            | TypedExpr::RecordAccess { .. }
            | TypedExpr::PositionalAccess { .. }
            | TypedExpr::ModuleSelect { .. }
            | TypedExpr::Tuple { .. }
            | TypedExpr::TupleIndex { .. }
            | TypedExpr::Todo { .. }
            | TypedExpr::Panic { .. }
            | TypedExpr::Echo { .. }
            | TypedExpr::BitArray { .. }
            | TypedExpr::RecordUpdate { .. }
            | TypedExpr::NegateBool { .. }
            | TypedExpr::NegateInt { .. }
            | TypedExpr::Invalid { .. } => None,
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn singleton_equal(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        value: Document<'a, 'doc>,
        module: Option<&'a str>,
        name: EcoString,
        should_be_equal: bool,
        variant_name: EcoString,
        type_: Arc<Type>,
    ) -> Document<'a, 'doc> {
        // If we're using this variant unqualified, register it as used so that
        // we know to import it. This `instanceof` check only happens if the
        // variant has no fields, so we don't need to check that here.
        if module.is_none()
            && let Some((package, module, type_name)) = type_.named_type_name_and_package()
        {
            _ = self
                .tracker
                .variants_used_in_instanceof
                .insert(TypeVariant {
                    package,
                    module,
                    type_name,
                    name: variant_name,
                });
        }
        let record = if let Some(module) = module {
            docvec![arena, DOLLAR_DOCUMENT, module, DOT_DOCUMENT, name]
        } else {
            name.to_doc(arena)
        };

        if should_be_equal {
            docvec![arena, value, SPACE_INSTANCE_OF_SPACE_DOCUMENT, record]
        } else {
            docvec![
                arena,
                EXCLAMATION_MARK_OPEN_PAREN_DOCUMENT,
                value,
                SPACE_INSTANCE_OF_SPACE_DOCUMENT,
                record,
                CLOSE_PAREN_DOCUMENT
            ]
        }
    }

    fn equal_with_doc_operands(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: Document<'a, 'doc>,
        right: Document<'a, 'doc>,
        type_: Arc<Type>,
        should_be_equal: bool,
    ) -> Document<'a, 'doc> {
        // If it is a simple scalar type then we can use JS' reference identity
        if is_js_scalar(type_) {
            let operator = if should_be_equal {
                SPACE_TRIPLE_EQUAL_SPACE_DOCUMENT
            } else {
                SPACE_EXCLAMATION_MARK_DOUBLE_EQUAL_SPACE_DOCUMENT
            };
            return docvec![arena, left, operator, right];
        }

        // Other types must be compared using structural equality
        self.prelude_equal_call(arena, should_be_equal, left, right)
    }

    pub(super) fn prelude_equal_call(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        should_be_equal: bool,
        left: Document<'a, 'doc>,
        right: Document<'a, 'doc>,
    ) -> Document<'a, 'doc> {
        // Record that we need to import the prelude's isEqual function into the module
        self.tracker.object_equality_used = true;
        // Construct the call
        let arguments = wrap_arguments(arena, [left, right]);
        let operator = if should_be_equal {
            IS_EQUAL_DOCUMENT
        } else {
            EXCLAMATION_MARK_IS_EQUAL_DOCUMENT
        };
        docvec![arena, operator, arguments]
    }

    fn print_bin_op(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedExpr,
        right: &'a TypedExpr,
        op: &'a str,
    ) -> Document<'a, 'doc> {
        let left = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, left)
        });
        let right = self.not_in_tail_position(Some(Ordering::Strict), |this| {
            this.child_expression(arena, right)
        });
        docvec![arena, left, SPACE_DOCUMENT, op, SPACE_DOCUMENT, right]
    }

    pub(super) fn bin_op_with_doc_operands(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        name: BinOp,
        left: Document<'a, 'doc>,
        right: Document<'a, 'doc>,
        type_: &Arc<Type>,
    ) -> Document<'a, 'doc> {
        match name {
            BinOp::And => docvec![arena, left, SPACE_DOUBLE_AMPERSAND_SPACE_DOCUMENT, right],
            BinOp::Or => docvec![arena, left, SPACE_DOUBLE_VERTICAL_BAR_SPACE_DOCUMENT, right],
            BinOp::LtInt | BinOp::LtFloat => {
                docvec![arena, left, SPACE_LT_INT_SPACE_DOCUMENT, right]
            }
            BinOp::LtEqInt | BinOp::LtEqFloat => {
                docvec![arena, left, SPACE_LT_EQ_INT_SPACE_DOCUMENT, right]
            }
            BinOp::Eq => self.equal_with_doc_operands(arena, left, right, type_.clone(), true),
            BinOp::NotEq => self.equal_with_doc_operands(arena, left, right, type_.clone(), false),
            BinOp::GtInt | BinOp::GtFloat => {
                docvec![arena, left, SPACE_GT_INT_SPACE_DOCUMENT, right]
            }
            BinOp::GtEqInt | BinOp::GtEqFloat => {
                docvec![arena, left, SPACE_GT_EQ_INT_SPACE_DOCUMENT, right]
            }
            BinOp::Concatenate | BinOp::AddInt | BinOp::AddFloat => {
                docvec![arena, left, SPACE_PLUS_SPACE_DOCUMENT, right]
            }
            BinOp::SubInt | BinOp::SubFloat => {
                docvec![arena, left, SPACE_MINUS_SPACE_DOCUMENT, right]
            }
            BinOp::MultInt | BinOp::MultFloat => {
                docvec![arena, left, SPACE_TIMES_SPACE_DOCUMENT, right]
            }
            BinOp::RemainderInt => {
                self.tracker.int_remainder_used = true;
                docvec![
                    arena,
                    CAMEL_CASE_REMAINDER_INT_DOCUMENT,
                    wrap_arguments(arena, [left, right])
                ]
            }
            BinOp::DivInt => {
                self.tracker.int_division_used = true;
                docvec![
                    arena,
                    DIVIDE_INT_DOCUMENT,
                    wrap_arguments(arena, [left, right])
                ]
            }
            BinOp::DivFloat => {
                self.tracker.float_division_used = true;
                docvec![
                    arena,
                    DIVIDE_FLOAT_DOCUMENT,
                    wrap_arguments(arena, [left, right])
                ]
            }
        }
    }

    fn todo(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        message: Option<&'a TypedExpr>,
        location: &'a SrcSpan,
    ) -> Document<'a, 'doc> {
        let message = match message {
            Some(m) => self.not_in_tail_position(None, |this| this.wrap_expression(arena, m)),
            None => string(
                arena,
                "`todo` expression evaluated. This code has not yet been implemented.",
            ),
        };
        self.throw_error(arena, "todo", &message, *location, vec![])
    }

    fn panic(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        location: &'a SrcSpan,
        message: Option<&'a TypedExpr>,
    ) -> Document<'a, 'doc> {
        let message = match message {
            Some(m) => self.not_in_tail_position(None, |this| this.wrap_expression(arena, m)),
            None => string(arena, "`panic` expression evaluated."),
        };
        self.throw_error(arena, "panic", &message, *location, vec![])
    }

    pub(crate) fn throw_error<Fields>(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        error_name: &'a str,
        message: &Document<'a, 'doc>,
        location: SrcSpan,
        fields: Fields,
    ) -> Document<'a, 'doc>
    where
        Fields: IntoIterator<Item = (&'a str, Document<'a, 'doc>)>,
    {
        self.tracker.make_error_used = true;
        let module = self.module_name.clone().to_doc(arena).surround(
            arena,
            DOUBLE_QUOTE_DOCUMENT,
            DOUBLE_QUOTE_DOCUMENT,
        );
        let function = self.function_name.clone().to_doc(arena).surround(
            arena,
            DOUBLE_QUOTE_DOCUMENT,
            DOUBLE_QUOTE_DOCUMENT,
        );
        let line = self.line_numbers.line_number(location.start).to_doc(arena);
        let fields = wrap_object(
            arena,
            fields.into_iter().map(|(k, v)| (k.to_doc(arena), Some(v))),
        );

        docvec![
            arena,
            self.source_map_tracker(arena, location.start),
            THROW_MAKE_ERROR_DOCUMENT,
            wrap_arguments(
                arena,
                [
                    string(arena, error_name),
                    FILEPATH_UPPERCASE_DOCUMENT,
                    module,
                    line,
                    function,
                    *message,
                    fields
                ]
            ),
        ]
    }

    fn module_select(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        module: &'a str,
        label: &'a EcoString,
        constructor: &'a ModuleValueConstructor,
    ) -> Document<'a, 'doc> {
        match constructor {
            ModuleValueConstructor::Fn { .. } | ModuleValueConstructor::Constant { .. } => {
                docvec![
                    arena,
                    DOLLAR_DOCUMENT,
                    module,
                    DOT_DOCUMENT,
                    maybe_escape_identifier(label)
                ]
            }

            ModuleValueConstructor::Record {
                name, arity, type_, ..
            } => self.record_constructor(arena, type_.clone(), Some(module), name, name, *arity),
        }
    }

    fn echo(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        expression: Document<'a, 'doc>,
        message: Option<&'a TypedExpr>,
        location: &'a SrcSpan,
    ) -> Document<'a, 'doc> {
        self.tracker.echo_used = true;

        let message = match message {
            Some(message) => self.not_in_tail_position(Some(Ordering::Strict), |this| {
                this.wrap_expression(arena, message)
            }),
            None => UNDEFINED_DOCUMENT,
        };

        let echo_arguments = call_arguments(
            arena,
            vec![
                expression,
                message,
                self.src_path.clone().to_doc(arena),
                self.line_numbers.line_number(location.start).to_doc(arena),
            ],
        );
        self.wrap_return(arena, docvec![arena, ECHO_DOCUMENT, echo_arguments])
    }

    pub(crate) fn constant_expression(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        context: Context,
        expression: &'a TypedConstant,
    ) -> Document<'a, 'doc> {
        match expression {
            Constant::Int { value, .. } => int(arena, value),
            Constant::Float { value, .. } => float(arena, value),
            Constant::String { value, .. } => string(arena, value),
            Constant::Tuple { elements, .. } => array(
                arena,
                elements
                    .iter()
                    .map(|element| self.constant_expression(arena, context, element)),
            ),

            Constant::List { elements, tail, .. } => {
                if tail.is_none() && elements.is_empty() {
                    return self.empty_list();
                }

                self.tracker.list_used = true;
                let list = match tail {
                    // There's no tail in the list, we join all the elements and
                    // call it a day.
                    None => list(
                        arena,
                        elements
                            .iter()
                            .map(|element| self.constant_expression(arena, context, element)),
                    ),

                    Some(tail) => match tail.list_elements() {
                        // There's a tail in the list whose elements are all
                        // known at compile time. In this case we replace the
                        // tail with those elements and create a single flat
                        // list.
                        Some(tail_elements) => list(
                            arena,
                            elements
                                .iter()
                                .chain(tail_elements)
                                .map(|element| self.constant_expression(arena, context, element)),
                        ),
                        // There's a tail in the list but we can't really tell
                        // what its elements are at compile time. This means we
                        // have to prepend to this list.
                        None => {
                            self.tracker.prepend_used = true;
                            let tail = self.constant_expression(arena, context, tail);
                            prepend(
                                arena,
                                elements.iter().map(|element| {
                                    self.constant_expression(arena, context, element)
                                }),
                                tail,
                            )
                        }
                    },
                };
                match context {
                    Context::Constant => docvec![arena, PURE_JAVASCRIPT_COMMENT_DOCUMENT, list],
                    Context::Guard => list,
                }
            }

            Constant::Record { type_, name, .. } if type_.is_bool() && name == "True" => {
                TRUE_LOWERCASE_DOCUMENT
            }
            Constant::Record { type_, name, .. } if type_.is_bool() && name == "False" => {
                FALSE_LOWERCASE_DOCUMENT
            }
            Constant::Record { type_, .. } if type_.is_nil() => UNDEFINED_DOCUMENT,

            Constant::Record {
                arguments,
                module,
                name,
                type_,
                ..
            } => {
                let tag = expression
                    .constant_record_tag()
                    .expect("record without inferred constructor made it to code generation");

                if module.is_none() && type_.is_result() {
                    if tag == "Ok" {
                        self.tracker.ok_used = true;
                    } else {
                        self.tracker.error_used = true;
                    }
                }

                // If there's no arguments, then this is either a constructor
                // which takes arguments being referenced rather than called,
                // or a variant with no fields at all.
                if arguments.is_none() {
                    // If the constructor is not a function that takes arguments,
                    // it effectively has zero arity.
                    let arity = type_.fn_arity().unwrap_or(0) as u16;
                    return self.record_constructor(
                        arena,
                        type_.clone(),
                        module.as_ref().map(|(name, _)| name.as_str()),
                        &tag,
                        name,
                        arity,
                    );
                }

                // Otherwise we're always constructing a record! Record updates
                // are fully expanded during type checking, so we just need to
                // handle the arguments here.
                let field_values = arguments
                    .iter()
                    .flatten()
                    .map(|argument| self.constant_expression(arena, context, &argument.value))
                    .collect_vec();

                let constructor = construct_record(
                    arena,
                    module.as_ref().map(|(module, _)| module.as_str()),
                    name,
                    field_values,
                );
                match context {
                    Context::Constant => {
                        docvec![arena, PURE_JAVASCRIPT_COMMENT_DOCUMENT, constructor]
                    }
                    Context::Guard => constructor,
                }
            }
            Constant::BitArray { segments, .. } => {
                let bit_array = self.constant_bit_array(arena, segments, context);
                match context {
                    Context::Constant => {
                        docvec![arena, PURE_JAVASCRIPT_COMMENT_DOCUMENT, bit_array]
                    }
                    Context::Guard => bit_array,
                }
            }

            Constant::Var { name, module, .. } => {
                match (module, context) {
                    (None, Context::Guard) => self.local_var(name).to_doc(arena),
                    (None, Context::Constant) => maybe_escape_identifier(name).to_doc(arena),
                    (Some((module, _)), _) => {
                        // JS keywords can be accessed here, but we must escape anyway
                        // as we escape when exporting such names in the first place,
                        // and the imported name has to match the exported name.
                        docvec![
                            arena,
                            DOLLAR_DOCUMENT,
                            module,
                            DOT_DOCUMENT,
                            maybe_escape_identifier(name)
                        ]
                    }
                }
            }

            Constant::StringConcatenation { left, right, .. } => {
                let left = self.constant_expression(arena, context, left);
                let right = self.constant_expression(arena, context, right);
                docvec![arena, left, SPACE_PLUS_SPACE_DOCUMENT, right]
            }

            Constant::RecordUpdate { .. } => {
                panic!("record updates should not reach code generation")
            }
            Constant::Todo { .. } => {
                panic!("todo constants should not reach code generation")
            }
            Constant::Invalid { .. } => {
                panic!("invalid constants should not reach code generation")
            }
        }
    }

    fn constant_bit_array(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        segments: &'a [TypedConstantBitArraySegment],
        context: Context,
    ) -> Document<'a, 'doc> {
        self.tracker.bit_array_literal_used = true;
        let segments_array = array(
            arena,
            segments.iter().map(|segment| {
                let value = match context {
                    Context::Constant => self.constant_expression(arena, context, &segment.value),
                    Context::Guard => self.guard_constant_expression(arena, &segment.value),
                };

                let details = self.constant_bit_array_segment_details(arena, segment, context);

                match details.type_ {
                    BitArraySegmentType::BitArray => {
                        if segment.size().is_some() {
                            self.tracker.bit_array_slice_used = true;
                            docvec![
                                arena,
                                BIT_ARRAY_SLICE_OPEN_PAREN_DOCUMENT,
                                value,
                                COMMA_ZERO_COMMA_SPACE,
                                details.size,
                                CLOSE_PAREN_DOCUMENT
                            ]
                        } else {
                            value
                        }
                    }
                    BitArraySegmentType::Int => {
                        match (details.size_value, segment.value.as_ref()) {
                            (Some(size_value), Constant::Int { int_value, .. })
                                if size_value <= SAFE_INT_SEGMENT_MAX_SIZE.into()
                                    && (&size_value % BigInt::from(8) == BigInt::ZERO) =>
                            {
                                let bytes = bit_array_segment_int_value_to_bytes(
                                    int_value.clone(),
                                    size_value,
                                    segment.endianness(),
                                );

                                u8_slice(arena, &bytes)
                            }

                            (Some(size_value), _) if size_value == 8.into() => value,

                            (Some(size_value), _) if size_value <= 0.into() => EMPTY_DOCUMENT,

                            _ => {
                                self.tracker.sized_integer_segment_used = true;
                                let size = details.size;
                                let is_big = bool(segment.endianness().is_big());
                                docvec![
                                    arena,
                                    SIZED_INT_OPEN_PAREN_DOCUMENT,
                                    value,
                                    COMMA_SPACE_DOCUMENT,
                                    size,
                                    COMMA_SPACE_DOCUMENT,
                                    is_big,
                                    CLOSE_PAREN_DOCUMENT
                                ]
                            }
                        }
                    }
                    BitArraySegmentType::Float => {
                        self.tracker.float_bit_array_segment_used = true;
                        let size = details.size;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            SIZED_FLOAT_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            size,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::String(StringEncoding::Utf8) => {
                        self.tracker.string_bit_array_segment_used = true;
                        docvec![
                            arena,
                            STRING_BITS_OPEN_PAREN_DOCUMENT,
                            value,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::String(StringEncoding::Utf16) => {
                        self.tracker.string_utf16_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            STRING_TO_UTF16_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::String(StringEncoding::Utf32) => {
                        self.tracker.string_utf32_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            STRING_TO_UTF32_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::UtfCodepoint(StringEncoding::Utf8) => {
                        self.tracker.codepoint_bit_array_segment_used = true;
                        docvec![
                            arena,
                            CODEPOINT_BITS_OPEN_PAREN_DOCUMENT,
                            value,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::UtfCodepoint(StringEncoding::Utf16) => {
                        self.tracker.codepoint_utf16_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            CODEPOINT_TO_UTF16_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                    BitArraySegmentType::UtfCodepoint(StringEncoding::Utf32) => {
                        self.tracker.codepoint_utf32_bit_array_segment_used = true;
                        let is_big = bool(details.endianness.is_big());
                        docvec![
                            arena,
                            CODEPOINT_TO_UTF32_OPEN_PAREN_DOCUMENT,
                            value,
                            COMMA_SPACE_DOCUMENT,
                            is_big,
                            CLOSE_PAREN_DOCUMENT
                        ]
                    }
                }
            }),
        );

        docvec![
            arena,
            TO_BIT_ARRAY_OPEN_PAREN_DOCUMENT,
            segments_array,
            CLOSE_PAREN_DOCUMENT
        ]
    }

    fn constant_bit_array_segment_details(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        segment: &'a TypedConstantBitArraySegment,
        context: Context,
    ) -> BitArraySegmentDetails<'a, 'doc> {
        let size = segment.size();
        let unit = segment.unit();
        let (size_value, size) = match size {
            Some(Constant::Int { int_value, .. }) => {
                let size_value = int_value * unit;
                let size = eco_format!("{size_value}").to_doc(arena);
                (Some(size_value), size)
            }

            Some(size) => {
                let mut size = match context {
                    Context::Constant => self.constant_expression(arena, context, size),
                    Context::Guard => self.guard_constant_expression(arena, size),
                };
                if unit != 1 {
                    size = size.group(arena).append(
                        arena,
                        SPACE_TIMES_SPACE_DOCUMENT.append(arena, unit.to_doc(arena)),
                    );
                }

                (None, size)
            }

            None => {
                let size_value: usize = if segment.type_.is_int() { 8 } else { 64 };
                (Some(BigInt::from(size_value)), size_value.to_doc(arena))
            }
        };

        let type_ = BitArraySegmentType::from_segment(segment);

        BitArraySegmentDetails {
            type_,
            size,
            size_value,
            endianness: segment.endianness(),
        }
    }

    pub(crate) fn guard(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        guard: &'a TypedClauseGuard,
    ) -> Document<'a, 'doc> {
        match guard {
            ClauseGuard::Invalid { .. } => unreachable!("invalid guard made it to code generation"),

            ClauseGuard::Block { value, .. } => {
                self.guard(arena, value)
                    .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT)
            }

            ClauseGuard::BinaryOperator {
                left,
                right,
                operator,
                ..
            } => {
                let operator = match operator {
                    BinOp::Eq if is_js_scalar(left.type_()) => TRIPLE_EQUAL_DOCUMENT,
                    BinOp::NotEq if is_js_scalar(left.type_()) => {
                        EXCLAMATION_MARK_DOUBLE_EQUAL_DOCUMENT
                    }
                    BinOp::Eq | BinOp::NotEq => {
                        let should_be_equal = *operator == BinOp::Eq;

                        // Handle singleton equality optimization for guards
                        if let Some(doc) = self.singleton_variant_guard_equality(
                            arena,
                            left,
                            right,
                            should_be_equal,
                        ) {
                            return doc;
                        }

                        if let Some(doc) = self.singleton_variant_guard_equality(
                            arena,
                            right,
                            left,
                            should_be_equal,
                        ) {
                            return doc;
                        }

                        let left_doc = self.guard(arena, left);
                        let right_doc = self.guard(arena, right);
                        return self.prelude_equal_call(
                            arena,
                            should_be_equal,
                            left_doc,
                            right_doc,
                        );
                    }

                    BinOp::GtFloat | BinOp::GtInt => GT_INT_DOCUMENT,
                    BinOp::GtEqFloat | BinOp::GtEqInt => GT_EQ_INT_DOCUMENT,
                    BinOp::LtFloat | BinOp::LtInt => LT_INT_DOCUMENT,
                    BinOp::LtEqFloat | BinOp::LtEqInt => LT_EQ_INT_DOCUMENT,

                    BinOp::AddFloat | BinOp::AddInt | BinOp::Concatenate => ADD_INT_DOCUMENT,
                    BinOp::SubFloat | BinOp::SubInt => SUB_INT_DOCUMENT,
                    BinOp::MultFloat | BinOp::MultInt => MULT_INT_DOCUMENT,

                    BinOp::DivFloat => {
                        self.tracker.float_division_used = true;

                        return docvec![
                            arena,
                            DIVIDE_FLOAT_DOCUMENT,
                            wrap_arguments(
                                arena,
                                [self.guard(arena, left), self.guard(arena, right)]
                            )
                        ];
                    }

                    BinOp::DivInt => {
                        self.tracker.int_division_used = true;
                        return docvec![
                            arena,
                            DIVIDE_INT_DOCUMENT,
                            wrap_arguments(
                                arena,
                                [self.guard(arena, left), self.guard(arena, right)]
                            )
                        ];
                    }

                    BinOp::RemainderInt => {
                        self.tracker.int_remainder_used = true;
                        return docvec![
                            arena,
                            CAMEL_CASE_REMAINDER_INT_DOCUMENT,
                            wrap_arguments(
                                arena,
                                [self.guard(arena, left), self.guard(arena, right)]
                            )
                        ];
                    }

                    BinOp::And => AND_DOCUMENT,
                    BinOp::Or => OR_DOCUMENT,
                };

                let left_document = self.wrapped_guard(arena, left);
                let right_document = self.wrapped_guard(arena, right);

                docvec![
                    arena,
                    left_document,
                    SPACE_DOCUMENT,
                    operator,
                    SPACE_DOCUMENT,
                    right_document
                ]
            }

            ClauseGuard::Var { name, .. } => self.local_var(name).to_doc(arena),

            ClauseGuard::TupleIndex { tuple, index, .. } => {
                docvec![
                    arena,
                    self.guard(arena, tuple,),
                    OPEN_SQUARE_DOCUMENT,
                    *index,
                    CLOSE_SQUARE_DOCUMENT
                ]
            }

            ClauseGuard::FieldAccess {
                label, container, ..
            } => docvec![
                arena,
                self.guard(arena, container),
                DOT_DOCUMENT,
                maybe_escape_property(label)
            ],

            ClauseGuard::ModuleSelect {
                module_alias,
                label,
                ..
            } => docvec![arena, DOLLAR_DOCUMENT, module_alias, DOT_DOCUMENT, label],

            ClauseGuard::Not { expression, .. } => {
                docvec![
                    arena,
                    EXCLAMATION_MARK_DOCUMENT,
                    self.guard(arena, expression,)
                ]
            }

            ClauseGuard::Constant(constant) => self.guard_constant_expression(arena, constant),
        }
    }

    fn singleton_variant_guard_equality(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        left: &'a TypedClauseGuard,
        right: &'a TypedClauseGuard,
        should_be_equal: bool,
    ) -> Option<Document<'a, 'doc>> {
        match right {
            ClauseGuard::Constant(Constant::Record {
                record_constructor: Some(constructor),
                module,
                name,
                ..
            }) if let ValueConstructorVariant::Record {
                arity: 0,
                name: variant_name,
                ..
            } = &constructor.variant =>
            {
                let left_doc = self.guard(arena, left);
                Some(self.singleton_equal(
                    arena,
                    left_doc,
                    module.as_ref().map(|(module, _)| module.as_str()),
                    name.clone(),
                    should_be_equal,
                    variant_name.clone(),
                    right.type_(),
                ))
            }
            ClauseGuard::Constant(Constant::List {
                elements,
                tail: None,
                ..
            }) if elements.is_empty() => {
                let left_doc = self.guard(arena, left);
                self.tracker.list_empty_class_used = true;
                Some(self.singleton_equal(
                    arena,
                    left_doc,
                    None,
                    "$Empty".into(),
                    should_be_equal,
                    "Empty".into(),
                    right.type_(),
                ))
            }
            ClauseGuard::Block { .. }
            | ClauseGuard::BinaryOperator { .. }
            | ClauseGuard::Not { .. }
            | ClauseGuard::Var { .. }
            | ClauseGuard::TupleIndex { .. }
            | ClauseGuard::FieldAccess { .. }
            | ClauseGuard::ModuleSelect { .. }
            | ClauseGuard::Constant(_)
            | ClauseGuard::Invalid { .. } => None,
        }
    }

    fn wrapped_guard(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        guard: &'a TypedClauseGuard,
    ) -> Document<'a, 'doc> {
        match guard {
            ClauseGuard::Invalid { .. } => unreachable!("invalid guard made it to code generation"),
            ClauseGuard::Var { .. }
            | ClauseGuard::TupleIndex { .. }
            | ClauseGuard::Constant(_)
            | ClauseGuard::Not { .. }
            | ClauseGuard::FieldAccess { .. }
            | ClauseGuard::Block { .. } => self.guard(arena, guard),

            ClauseGuard::BinaryOperator { .. } | ClauseGuard::ModuleSelect { .. } => {
                docvec![
                    arena,
                    OPEN_PAREN_DOCUMENT,
                    self.guard(arena, guard),
                    CLOSE_PAREN_DOCUMENT
                ]
            }
        }
    }

    fn guard_constant_expression(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        expression: &'a TypedConstant,
    ) -> Document<'a, 'doc> {
        match expression {
            Constant::Tuple { elements, .. } => array(
                arena,
                elements
                    .iter()
                    .map(|element| self.guard_constant_expression(arena, element)),
            ),

            Constant::Record { type_, name, .. } if type_.is_bool() && name == "True" => {
                TRUE_LOWERCASE_DOCUMENT
            }
            Constant::Record { type_, name, .. } if type_.is_bool() && name == "False" => {
                FALSE_LOWERCASE_DOCUMENT
            }
            Constant::Record { type_, .. } if type_.is_nil() => UNDEFINED_DOCUMENT,

            Constant::BitArray { segments, .. } => {
                self.constant_bit_array(arena, segments, Context::Guard)
            }

            Constant::Var { name, .. } => self.local_var(name).to_doc(arena),

            Constant::Record { .. }
            | Constant::Int { .. }
            | Constant::Float { .. }
            | Constant::String { .. }
            | Constant::List { .. }
            | Constant::RecordUpdate { .. }
            | Constant::StringConcatenation { .. }
            | Constant::Todo { .. }
            | Constant::Invalid { .. } => {
                self.constant_expression(arena, Context::Guard, expression)
            }
        }
    }

    pub fn source_map_tracker(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        start_index: u32,
    ) -> Document<'a, 'doc> {
        create_cursor_position_observer(
            arena,
            &self.source_map_builder,
            self.line_numbers,
            start_index,
        )
    }

    pub(crate) fn record_constructor(
        &mut self,
        arena: &'doc DocumentArena<'a, 'doc>,
        type_: Arc<Type>,
        qualifier: Option<&'a str>,
        variant_name: &EcoString,
        name: &'a EcoString,
        arity: u16,
    ) -> Document<'a, 'doc> {
        if qualifier.is_none() && type_.is_result_constructor() {
            if name == "Ok" {
                self.tracker.ok_used = true;
            } else if name == "Error" {
                self.tracker.error_used = true;
            }
        }
        if type_.is_bool() && name == "True" {
            TRUE_LOWERCASE_DOCUMENT
        } else if type_.is_bool() {
            FALSE_LOWERCASE_DOCUMENT
        } else if type_.is_nil() {
            UNDEFINED_DOCUMENT
        } else if arity == 0
            && let Some((package, module, type_name)) = type_.named_type_name_and_package()
        {
            // If the variant has no fields, return the singleton constant so
            // that all values of the variant are the same underlying reference,
            // and are faster to compare.
            match qualifier {
                Some(module) => docvec![
                    arena,
                    DOLLAR_DOCUMENT,
                    module,
                    DOT_DOCUMENT,
                    type_name,
                    DOLLAR_DOCUMENT,
                    name,
                    DOLLAR_CONST_DOCUMENT
                ],
                None => {
                    if module != self.module_name {
                        let alias = if name == variant_name {
                            None
                        } else {
                            Some(eco_format!("{type_name}${name}$const"))
                        };
                        // Since this constant is an implementation detail and not
                        // present in Gleam code, we need to track it so that we
                        // import it, as it doesn't appear directly in the `import`s
                        // in the source code.
                        _ = self.tracker.variant_constants_used.insert(
                            TypeVariant {
                                package,
                                module,
                                type_name: type_name.clone(),
                                name: variant_name.clone(),
                            },
                            alias,
                        );
                    }
                    docvec![
                        arena,
                        type_name,
                        DOLLAR_DOCUMENT,
                        name,
                        DOLLAR_CONST_DOCUMENT
                    ]
                }
            }
        } else {
            let vars = (0..arity).map(|i| eco_format!("var{i}").to_doc(arena));
            let body = docvec![
                arena,
                RETURN_SPACE_DOCUMENT,
                construct_record(arena, qualifier, name, vars.clone()),
                SEMICOLON_DOCUMENT
            ];
            docvec![
                arena,
                docvec![
                    arena,
                    wrap_arguments(arena, vars),
                    SPACE_EQUAL_ARROW_SPACE_OPEN_CURLY_DOCUMENT,
                    BREAKABLE_SPACE_DOCUMENT,
                    body
                ]
                .nest(arena, INDENT)
                .append(arena, BREAKABLE_SPACE_DOCUMENT)
                .group(arena),
                CLOSE_CURLY_DOCUMENT,
            ]
        }
    }
}

#[derive(Clone, Copy)]
enum AssertExpression {
    Literal,
    Expression,
    Unevaluated,
}

impl AssertExpression {
    fn from_expression(expression: &TypedExpr) -> Self {
        if expression.is_literal() {
            Self::Literal
        } else {
            Self::Expression
        }
    }
}

pub fn int<'a, 'doc>(arena: &'doc DocumentArena<'a, 'doc>, value: &'a str) -> Document<'a, 'doc> {
    eco_string_int(arena, value.into())
}

pub fn eco_string_int<'a, 'doc>(
    arena: &'doc DocumentArena<'a, 'doc>,
    value: EcoString,
) -> Document<'a, 'doc> {
    let mut out = EcoString::with_capacity(value.len());

    if value.starts_with('-') {
        out.push('-');
    } else if value.starts_with('+') {
        out.push('+');
    };
    let value = value.trim_start_matches(['+', '-'].as_ref());

    let value = if value.starts_with("0x") {
        out.push_str("0x");
        value.trim_start_matches("0x")
    } else if value.starts_with("0o") {
        out.push_str("0o");
        value.trim_start_matches("0o")
    } else if value.starts_with("0b") {
        out.push_str("0b");
        value.trim_start_matches("0b")
    } else {
        value
    };

    let value = value.trim_start_matches(['0', '_']);
    if value.is_empty() {
        out.push('0');
    }

    out.push_str(value);

    out.to_doc(arena)
}

pub fn float<'a, 'doc>(arena: &'doc DocumentArena<'a, 'doc>, value: &'a str) -> Document<'a, 'doc> {
    let mut out = EcoString::with_capacity(value.len());

    if value.starts_with('-') {
        out.push('-');
    } else if value.starts_with('+') {
        out.push('+');
    };
    let value = value.trim_start_matches(['+', '-'].as_ref());

    let value = value.trim_start_matches(['0', '_']);
    if value.starts_with(['.', 'e', 'E']) {
        out.push('0');
    }
    out.push_str(value);

    out.to_doc(arena)
}

pub fn float_from_value<'a, 'doc>(
    arena: &'doc DocumentArena<'a, 'doc>,
    value: f64,
) -> Document<'a, 'doc> {
    if value.is_infinite() {
        if value.is_sign_positive() {
            INFINITY_DOCUMENT
        } else {
            MINUS_INFINITY_DOCUMENT
        }
    } else if value.is_nan() {
        // NOTE: this case is probably unnecessary, as this function is only
        // invoked with `LiteralFloatValue` values, which cannot be nan.
        NAN_DOCUMENT
    } else {
        value.to_doc(arena)
    }
}

/// The context where the constant expression is used, it might be inside a
/// function call, or in the definition of another constant.
///
/// Based on the context we might want to annotate pure function calls as
/// "@__PURE__".
///
#[derive(Debug, Clone, Copy)]
pub enum Context {
    Constant,
    Guard,
}

#[derive(Debug)]
struct BitArraySegmentDetails<'a, 'doc> {
    type_: BitArraySegmentType,
    size: Document<'a, 'doc>,
    /// The size of the bit array segment stored as a BigInt.
    /// This has a value when the segment's size is known at compile time.
    size_value: Option<BigInt>,
    endianness: Endianness,
}

#[derive(Debug, Clone, Copy)]
enum BitArraySegmentType {
    BitArray,
    Int,
    Float,
    String(StringEncoding),
    UtfCodepoint(StringEncoding),
}

impl BitArraySegmentType {
    fn from_segment<Value>(segment: &BitArraySegment<Value, Arc<Type>>) -> Self {
        if segment.type_.is_int() {
            BitArraySegmentType::Int
        } else if segment.type_.is_float() {
            BitArraySegmentType::Float
        } else if segment.type_.is_bit_array() {
            BitArraySegmentType::BitArray
        } else if segment.type_.is_string() {
            let encoding = if segment.has_utf16_option() {
                StringEncoding::Utf16
            } else if segment.has_utf32_option() {
                StringEncoding::Utf32
            } else {
                StringEncoding::Utf8
            };
            BitArraySegmentType::String(encoding)
        } else if segment.type_.is_utf_codepoint() {
            let encoding = if segment.has_utf16_codepoint_option() {
                StringEncoding::Utf16
            } else if segment.has_utf32_codepoint_option() {
                StringEncoding::Utf32
            } else {
                StringEncoding::Utf8
            };
            BitArraySegmentType::UtfCodepoint(encoding)
        } else {
            panic!(
                "Invalid bit array segment type reached code generation: {:?}",
                segment.type_
            );
        }
    }
}

pub fn string<'a, 'doc>(
    arena: &'doc DocumentArena<'a, 'doc>,
    value: &'a str,
) -> Document<'a, 'doc> {
    if value.contains('\n') {
        EcoString::from(value.replace('\n', r"\n"))
            .to_doc(arena)
            .surround(arena, DOUBLE_QUOTE_DOCUMENT, DOUBLE_QUOTE_DOCUMENT)
    } else {
        value
            .to_doc(arena)
            .surround(arena, DOUBLE_QUOTE_DOCUMENT, DOUBLE_QUOTE_DOCUMENT)
    }
}

pub(crate) fn array<'a, 'doc, Elements: IntoIterator<Item = Document<'a, 'doc>>>(
    arena: &'doc DocumentArena<'a, 'doc>,
    elements: Elements,
) -> Document<'a, 'doc> {
    let elements = arena.join(elements, COMMA_BREAK_DOCUMENT);
    if elements.is_empty() {
        // Do not add a trailing comma since that adds an 'undefined' element
        OPEN_CLOSE_SQUARE_DOCUMENT
    } else {
        docvec![
            arena,
            OPEN_SQUARE_DOCUMENT,
            docvec![arena, EMPTY_BREAK_DOCUMENT, elements].nest(arena, INDENT),
            TRAILING_COMMA_BREAK_DOCUMENT,
            CLOSE_SQUARE_DOCUMENT
        ]
        .group(arena)
    }
}

pub(crate) fn list<'a, 'doc, I: IntoIterator<Item = Document<'a, 'doc>>>(
    arena: &'doc DocumentArena<'a, 'doc>,
    elements: I,
) -> Document<'a, 'doc>
where
    I::IntoIter: DoubleEndedIterator,
{
    let array = array(arena, elements);
    docvec![
        arena,
        TO_LIST_OPEN_PAREN_DOCUMENT,
        array,
        CLOSE_PAREN_DOCUMENT
    ]
}

fn prepend<'a, 'doc, I: IntoIterator<Item = Document<'a, 'doc>>>(
    arena: &'doc DocumentArena<'a, 'doc>,
    elements: I,
    tail: Document<'a, 'doc>,
) -> Document<'a, 'doc>
where
    I::IntoIter: DoubleEndedIterator + ExactSizeIterator,
{
    elements.into_iter().rev().fold(tail, |tail, element| {
        let arguments = call_arguments(arena, [element, tail]);
        docvec![arena, LIST_PREPEND_DOCUMENT, arguments]
    })
}

fn call_arguments<'a, 'doc, Elements: IntoIterator<Item = Document<'a, 'doc>>>(
    arena: &'doc DocumentArena<'a, 'doc>,
    elements: Elements,
) -> Document<'a, 'doc> {
    let elements = arena.join(elements, COMMA_BREAK_DOCUMENT);
    if elements.is_empty() {
        return OPEN_CLOSE_PAREN_DOCUMENT;
    }
    docvec![
        arena,
        OPEN_PAREN_DOCUMENT,
        docvec![arena, EMPTY_BREAK_DOCUMENT, elements].nest(arena, INDENT),
        TRAILING_COMMA_BREAK_DOCUMENT,
        CLOSE_PAREN_DOCUMENT
    ]
    .group(arena)
}

pub(crate) fn construct_record<'a, 'doc>(
    arena: &'doc DocumentArena<'a, 'doc>,
    module: Option<&'a str>,
    name: &'a str,
    arguments: impl IntoIterator<Item = Document<'a, 'doc>>,
) -> Document<'a, 'doc> {
    let mut any_arguments = false;
    let arguments = arena.join(
        arguments.into_iter().inspect(|_| {
            any_arguments = true;
        }),
        COMMA_BREAK_DOCUMENT,
    );
    let arguments = docvec![arena, EMPTY_BREAK_DOCUMENT, arguments].nest(arena, INDENT);
    let name = if let Some(module) = module {
        docvec![arena, DOLLAR_DOCUMENT, module, DOT_DOCUMENT, name]
    } else {
        name.to_doc(arena)
    };
    if any_arguments {
        docvec![
            arena,
            NEW_SPACE_DOCUMENT,
            name,
            OPEN_PAREN_DOCUMENT,
            arguments,
            TRAILING_COMMA_BREAK_DOCUMENT,
            CLOSE_PAREN_DOCUMENT
        ]
        .group(arena)
    } else {
        docvec![arena, NEW_SPACE_DOCUMENT, name, OPEN_CLOSE_PAREN_DOCUMENT]
    }
}

impl TypedExpr {
    fn handles_own_return(&self) -> bool {
        match self {
            TypedExpr::Todo { .. }
            | TypedExpr::Call { .. }
            | TypedExpr::Case { .. }
            | TypedExpr::Panic { .. }
            | TypedExpr::Block { .. }
            | TypedExpr::Echo { .. }
            | TypedExpr::Pipeline { .. }
            | TypedExpr::RecordUpdate { .. } => true,

            TypedExpr::Int { .. }
            | TypedExpr::Float { .. }
            | TypedExpr::String { .. }
            | TypedExpr::Var { .. }
            | TypedExpr::Fn { .. }
            | TypedExpr::List { .. }
            | TypedExpr::BinOp { .. }
            | TypedExpr::RecordAccess { .. }
            | TypedExpr::PositionalAccess { .. }
            | TypedExpr::ModuleSelect { .. }
            | TypedExpr::Tuple { .. }
            | TypedExpr::TupleIndex { .. }
            | TypedExpr::BitArray { .. }
            | TypedExpr::NegateBool { .. }
            | TypedExpr::NegateInt { .. }
            | TypedExpr::Invalid { .. } => false,
        }
    }
}

impl BinOp {
    fn is_operator_to_wrap(&self) -> bool {
        match self {
            BinOp::And
            | BinOp::Or
            | BinOp::Eq
            | BinOp::NotEq
            | BinOp::LtInt
            | BinOp::LtEqInt
            | BinOp::LtFloat
            | BinOp::LtEqFloat
            | BinOp::GtEqInt
            | BinOp::GtInt
            | BinOp::GtEqFloat
            | BinOp::GtFloat
            | BinOp::AddInt
            | BinOp::AddFloat
            | BinOp::SubInt
            | BinOp::SubFloat
            | BinOp::MultFloat
            | BinOp::DivInt
            | BinOp::DivFloat
            | BinOp::RemainderInt
            | BinOp::Concatenate => true,
            BinOp::MultInt => false,
        }
    }
}

pub fn is_js_scalar(t: Arc<Type>) -> bool {
    t.is_int() || t.is_float() || t.is_bool() || t.is_nil() || t.is_string()
}

fn expression_requires_semicolon(expression: &TypedExpr) -> bool {
    match expression {
        TypedExpr::Int { .. }
        | TypedExpr::Fn { .. }
        | TypedExpr::Var { .. }
        | TypedExpr::List { .. }
        | TypedExpr::Call { .. }
        | TypedExpr::Echo { .. }
        | TypedExpr::Float { .. }
        | TypedExpr::String { .. }
        | TypedExpr::BinOp { .. }
        | TypedExpr::Tuple { .. }
        | TypedExpr::NegateInt { .. }
        | TypedExpr::BitArray { .. }
        | TypedExpr::TupleIndex { .. }
        | TypedExpr::NegateBool { .. }
        | TypedExpr::RecordAccess { .. }
        | TypedExpr::PositionalAccess { .. }
        | TypedExpr::ModuleSelect { .. } => true,

        TypedExpr::Todo { .. }
        | TypedExpr::Case { .. }
        | TypedExpr::Panic { .. }
        | TypedExpr::Pipeline { .. }
        | TypedExpr::RecordUpdate { .. }
        | TypedExpr::Invalid { .. }
        | TypedExpr::Block { .. } => false,
    }
}

/// Wrap a document in an immediately invoked function expression
fn immediately_invoked_function_expression_document<'a, 'doc>(
    arena: &'doc DocumentArena<'a, 'doc>,
    document: Document<'a, 'doc>,
) -> Document<'a, 'doc> {
    docvec![
        arena,
        docvec![
            arena,
            OPEN_PAREN_OPEN_CLOSE_PAREN_EQUAL_ARROW_OPEN_CURLY_DOCUMENT,
            BREAKABLE_SPACE_DOCUMENT,
            document
        ]
        .nest(arena, INDENT),
        BREAKABLE_SPACE_DOCUMENT,
        CLOSE_CURLY_CLOSE_PAREN_OPEN_CLOSE_PAREN_DOCUMENT,
    ]
    .group(arena)
}

fn u8_slice<'a, 'doc>(arena: &'doc DocumentArena<'a, 'doc>, bytes: &[u8]) -> Document<'a, 'doc> {
    arena.join(
        bytes.iter().map(|byte| byte.to_doc(arena)),
        COMMA_SPACE_DOCUMENT,
    )
}
