///|
/// The compiler's `UntypedExpr::start_byte_index`.
fn start_byte_index(e : @ast.Expr) -> Int32 {
  match e {
    Pipeline(expressions~) => start_byte_index(expressions[0])
    _ => e.location().start
  }
}

///|
fn expr_bin_op_precedence(e : @ast.Expr) -> Int32 {
  match e {
    BinOp(operator~, ..) => precedence(operator)
    Pipeline(..) => 5
    _ => max_precedence
  }
}

///|
fn expr_bin_op_name(e : @ast.Expr) -> @ast.BinOp? {
  match e {
    BinOp(operator~, ..) => Some(operator)
    _ => None
  }
}

///|
fn expr_can_have_multiple_per_line(e : @ast.Expr) -> Bool {
  match e {
    Int(..) | Float(..) | String(..) | Var(..) => true
    NegateBool(value~, ..) | NegateInt(value~, ..) =>
      expr_can_have_multiple_per_line(value)
    FieldAccess(container~, ..) => expr_can_have_multiple_per_line(container)
    _ => false
  }
}

///|
fn is_tuple_expr(e : @ast.Expr) -> Bool {
  e is Tuple(..)
}

///|
fn is_call(e : @ast.Expr) -> Bool {
  e is Call(..)
}

///|
fn is_binop_expr(e : @ast.Expr) -> Bool {
  e is BinOp(..)
}

///|
fn is_pipeline(e : @ast.Expr) -> Bool {
  e is Pipeline(..)
}

///|
/// The compiler's `CAPTURE_VARIABLE`.
let capture_variable : String = "_capture"

///|
fn is_capture_hole(argument : @ast.CallArg[@ast.Expr]) -> Bool {
  argument.value is Var(name~, ..) && name == capture_variable
}

///|
/// The compiler's `is_breakable_argument`: a call only when it is the only
/// argument.
fn is_breakable_argument(e : @ast.Expr, arity : Int32) -> Bool {
  match e {
    Call(..) => arity == 1
    Fn(..) | Block(..) | Case(..) | List(..) | Tuple(..) | BitArray(..) => true
    _ => false
  }
}

///|
/// What comes before an `as` message (the compiler's `PrecedingAs`).
priv enum PrecedingAs {
  Expression
  Keyword
}

///|
priv enum FnCapturePosition {
  RightHandSideOfPipe
  EverywhereElse
}

///|
/// The compiler's `expr`. One unit of fuel per nesting level.
fn Formatter::expr(
  self : Formatter,
  expression : @ast.Expr,
) -> @pretty.Doc raise FormatError {
  self.nested(op_format, () => {
    let comments = self.pop_comments(start_byte_index(expression))
    let document = match expression {
      Panic(message~, ..) =>
        self.append_as_message_expression(t("panic"), Keyword, message)
      Todo(message~, ..) =>
        self.append_as_message_expression(t("todo"), Keyword, message)
      Echo(expression~, message~, ..) => self.echo(expression, message)
      Pipeline(expressions~) => self.pipeline(expressions, false)
      Int(value~, ..) => int(value)
      Float(value~, ..) => float(value)
      String(value~, ..) => string(value)
      Block(statements~, location~) => self.block(location, statements, false)
      Var(name~, ..) => if name == capture_variable { t("_") } else { t(name) }
      TupleIndex(..) => self.access_chain(expression)
      NegateInt(value~, ..) => self.negate_int(value)
      NegateBool(value~, ..) => self.negate_bool(value)
      Fn(kind=Capture(..), body~, ..) => self.fn_capture(body, EverywhereElse)
      Fn(
        arguments~,
        return_annotation~,
        body~,
        location~,
        end_of_head_byte_index~,
        ..
      ) =>
        self.expr_fn(
          arguments, return_annotation, body, location, end_of_head_byte_index,
        )
      List(elements~, tail~, location~) => self.list(elements, tail, location)
      Call(fun~, arguments~, location~, ..) =>
        self.call(fun, arguments, location)
      BinOp(operator~, left~, right~, ..) =>
        self.bin_op(operator, left, right, false)
      Case(subjects~, clauses~, location~) =>
        self.case_(
          subjects,
          match clauses {
            Some(c) => c
            None => [][:]
          },
          location,
        )
      FieldAccess(..) => self.access_chain(expression)
      Tuple(elements~, location~) => self.tuple(elements, location)
      BitArray(segments~, location~) => {
        let docs = []
        for s in segments {
          docs.push(bit_array_segment(s, e => self.bit_array_segment_expr(e)))
        }
        let packing = self.items_sequence_packing(
          segments,
          None,
          s => s.location,
          s => expr_can_have_multiple_per_line(s.value),
          location,
        )
        self.bit_array(docs, packing, location)
      }
      RecordUpdate(constructor_~, record~, arguments~, location~, ..) =>
        self.record_update(constructor_, record, arguments, location)
    }
    commented(document, comments[:])
  })
}

///|
/// The compiler's `FieldAccess` arm and `tuple_index`, for a whole chain
/// (`a.b.0.c`) in a loop: the parser accepts a chain of any length. The
/// compiler's `tuple_index` drops the braces of `{t.1}.0`; that inner
/// expression is the base of the chain.
fn Formatter::access_chain(
  self : Formatter,
  expression : @ast.Expr,
) -> @pretty.Doc raise FormatError {
  let suffixes : Array[String] = []
  let mut base = expression
  let mut walking = true
  while walking {
    match base {
      FieldAccess(label~, container~, ..) => {
        suffixes.push(label)
        base = container
      }
      TupleIndex(tuple~, index~, ..) => {
        suffixes.push(index.to_string())
        match tuple {
          Block(statements~, ..) if statements.length() == 1 &&
            statements[0] is Expression(TupleIndex(tuple=t2, ..) as inner) &&
            !is_tuple_expr(t2) => {
            base = inner
            walking = false
          }
          _ => base = tuple
        }
      }
      _ => walking = false
    }
  }
  let mut doc = self.expr(base)
  for k = suffixes.length() - 1; k >= 0; k = k - 1 {
    doc = doc + t(".") + t(suffixes[k])
  }
  doc
}

///|
/// The compiler's `negate_bool`.
fn Formatter::negate_bool(
  self : Formatter,
  e : @ast.Expr,
) -> @pretty.Doc raise FormatError {
  match e {
    NegateBool(value~, ..) => self.expr(value)
    BinOp(..) => t("!") + wrap_block(self.expr(e))
    _ => t("!") + self.expr(e)
  }
}

///|
/// The compiler's `negate_int`.
fn Formatter::negate_int(
  self : Formatter,
  e : @ast.Expr,
) -> @pretty.Doc raise FormatError {
  match e {
    NegateInt(value~, ..) => self.expr(value)
    Int(value~, ..) if value.has_prefix("-") =>
      int(value.view(start_offset=1).to_owned())
    BinOp(..) => t("- ") + self.expr(e)
    _ => t("-") + self.expr(e)
  }
}

///|
/// The compiler's `append_as_message_expression`.
fn Formatter::append_as_message_expression(
  self : Formatter,
  doc : @pretty.Doc,
  preceding_as : PrecedingAs,
  message : @ast.Expr?,
) -> @pretty.Doc raise FormatError {
  guard message is Some(message) else { return doc }
  let comments = self.pop_comments(message.location().start)
  let comments = printed_comments(comments[:], false)
  let as_ = match preceding_as {
    Keyword => space_as
    Expression => nest(breakable_space + t("as"))
  }
  let doc = match comments {
    Some(c) =>
      group(doc) +
      as_ +
      nest(line_doc + c + line_doc + group(self.expr(message)))
    None => {
      let message_doc = match (preceding_as, message) {
        (Keyword, Block(..)) => group(self.expr(message))
        _ => nest(group(self.expr(message)))
      }
      group(doc) + as_ + t(" ") + message_doc
    }
  }
  group(doc)
}

///|
/// The compiler's `echo`.
fn Formatter::echo(
  self : Formatter,
  expression : @ast.Expr?,
  message : @ast.Expr?,
) -> @pretty.Doc raise FormatError {
  guard expression is Some(expression) else {
    return self.append_as_message_expression(t("echo"), Keyword, message)
  }
  let doc = self.expr(expression)
  if is_binop_expr(expression) || is_pipeline(expression) {
    t("echo ") +
    self.append_as_message_expression(nest(doc), Expression, message)
  } else {
    t("echo ") + self.append_as_message_expression(doc, Expression, message)
  }
}

///|
/// The compiler's `expr_fn`.
fn Formatter::expr_fn(
  self : Formatter,
  arguments : ArrayView[@ast.Arg],
  return_annotation : @ast.TypeAst?,
  body : ArrayView[@ast.Statement],
  location : @span.SrcSpan,
  end_of_head_byte_index : Int32,
) -> @pretty.Doc raise FormatError {
  let docs = []
  for a in arguments {
    docs.push(self.fn_arg(a))
  }
  let arguments_doc = @pretty.next_break_fits(
    group(self.wrap_arguments(docs, end_of_head_byte_index)),
    Disabled,
  )
  let header = t("fn") + arguments_doc
  let header = match return_annotation {
    None => header
    Some(ann) => header + t(" -> ") + self.type_ast(ann)
  }
  let statements = self.statements(body)
  let body_doc = match
    printed_comments(self.pop_comments(location.end)[:], false) {
    None => statements
    Some(c) => @pretty.force_break(statements + line_doc + c)
  }
  group(header + t(" ") + wrap_block(body_doc))
}

///|
/// The compiler's `fn_capture`: `f(_)` is `f`; on the right of a pipe a
/// first unlabelled hole is dropped.
fn Formatter::fn_capture(
  self : Formatter,
  call : ArrayView[@ast.Statement],
  position : FnCapturePosition,
) -> @pretty.Doc raise FormatError {
  // The parser builds a capture as one call statement.
  guard call.length() == 1 &&
    call[0] is Expression(Call(fun~, arguments~, location~, ..)) else {
    return @pretty.empty()
  }
  if arguments.length() == 1 &&
    is_capture_hole(arguments[0]) &&
    arguments[0].label is None {
    return self.expr(fun)
  }
  let (rest, arity) = if position is RightHandSideOfPipe &&
    arguments.length() >= 1 &&
    is_capture_hole(arguments[0]) &&
    arguments[0].label is None {
    (arguments[1:], arguments.length() - 1)
  } else {
    (arguments, arguments.length())
  }
  let expression = self.expr(fun)
  self.append_inlinable_wrapped_arguments(
    expression,
    rest.to_owned(),
    location,
    a => a.location,
    a => is_breakable_argument(a.value, arity),
    a => self.call_arg(a, arity),
  )
}