///|
/// The compiler's `call`. A chain of calls (`a()()()`) is walked in a loop
/// from the innermost function out, with the compiler's pop order.
fn Formatter::call(
  self : Formatter,
  function : @ast.Expr,
  arguments : ArrayView[@ast.CallArg[@ast.Expr]],
  location : @span.SrcSpan,
) -> @pretty.Doc raise FormatError {
  let frames = [(arguments, location)]
  let mut fun = function
  while fun is Call(fun=f2, arguments=a2, location=l2, ..) {
    frames.push((a2, l2))
    fun = f2
  }
  let mut doc = match fun {
    Pipeline(..) => break_block(self.expr(fun))
    _ => self.expr(fun)
  }
  for k = frames.length() - 1; k >= 0; k = k - 1 {
    let (args, loc) = frames[k]
    let arity = args.length()
    doc = self.append_inlinable_wrapped_arguments(
      doc,
      args.to_owned(),
      loc,
      a => a.location,
      a => is_breakable_argument(a.value, arity),
      a => self.call_arg(a, arity),
    )
  }
  doc
}

///|
/// The compiler's `call_arg`.
fn Formatter::call_arg(
  self : Formatter,
  argument : @ast.CallArg[@ast.Expr],
  arity : Int32,
) -> @pretty.Doc raise FormatError {
  self.format_call_arg(argument, e => e.location(), e => {
    self.comma_separated_item(e, arity)
  })
}

///|
/// The compiler's `comma_separated_item`.
fn Formatter::comma_separated_item(
  self : Formatter,
  expression : @ast.Expr,
  siblings : Int32,
) -> @pretty.Doc raise FormatError {
  match expression {
    BinOp(operator~, left~, right~, ..) if siblings > 1 => {
      let comments = self.pop_comments(start_byte_index(expression))
      commented(group(self.bin_op(operator, left, right, true)), comments[:])
    }
    Pipeline(expressions~) if siblings > 1 => {
      let comments = self.pop_comments(start_byte_index(expression))
      commented(group(self.pipeline(expressions, true)), comments[:])
    }
    _ => group(self.expr(expression))
  }
}

///|
/// The compiler's `record_update_arg`.
fn Formatter::record_update_arg(
  self : Formatter,
  argument : @ast.RecordUpdateArg[@ast.Expr],
) -> @pretty.Doc raise FormatError {
  let comments = self.pop_comments(argument.location.start)
  if argument.value.location() == argument.location {
    commented(t(argument.label) + t(":"), comments[:])
  } else {
    let doc = group(t(argument.label) + t(": ") + self.expr(argument.value))
    if is_binop_expr(argument.value) || is_pipeline(argument.value) {
      nest(commented(doc, comments[:]))
    } else {
      commented(doc, comments[:])
    }
  }
}

///|
priv enum UpdatePiece {
  Record(@ast.RecordBeingUpdated[@ast.Expr])
  Argument(@ast.RecordUpdateArg[@ast.Expr])
}

///|
fn UpdatePiece::location(self : UpdatePiece) -> @span.SrcSpan {
  match self {
    Record(r) => r.location
    Argument(a) => a.location
  }
}

///|
/// The compiler's `record_update`.
fn Formatter::record_update(
  self : Formatter,
  constructor_ : @ast.Expr,
  record : @ast.RecordBeingUpdated[@ast.Expr],
  arguments : ArrayView[@ast.RecordUpdateArg[@ast.Expr]],
  location : @span.SrcSpan,
) -> @pretty.Doc raise FormatError {
  let constructor_doc = self.expr(constructor_)
  let pieces : Array[UpdatePiece] = [Record(record)]
  for a in arguments {
    pieces.push(Argument(a))
  }
  let n = pieces.length()
  self.append_inlinable_wrapped_arguments(
    constructor_doc,
    pieces,
    location,
    p => p.location(),
    p => {
      match p {
        Argument(a) => is_breakable_argument(a.value, n)
        Record(r) => is_breakable_argument(r.base, n)
      }
    },
    p => {
      match p {
        Argument(a) => self.record_update_arg(a)
        Record(r) => {
          let comments = self.pop_comments(r.location.start)
          commented(t("..") + self.expr(r.base), comments[:])
        }
      }
    },
  )
}

///|
/// The compiler's `bin_op`. A left-nested chain (`a + b + c`) is walked in
/// a loop, not by recursion: the parser accepts a chain of any length, so
/// the formatter spends no fuel and no stack per link. The pops keep the
/// compiler's order: the leftmost operand, then, for each link from the
/// innermost out, the comments before the right operand and the operand.
fn Formatter::bin_op(
  self : Formatter,
  name : @ast.BinOp,
  left : @ast.Expr,
  right : @ast.Expr,
  nest_steps : Bool,
) -> @pretty.Doc raise FormatError {
  let ops = [name]
  let rights = [right]
  let mut leaf = left
  while leaf is BinOp(operator~, left=l, right=r, ..) {
    ops.push(operator)
    rights.push(r)
    leaf = l
  }
  let n = ops.length() - 1
  let mut doc = self.bin_op_side(ops[n], leaf, nest_steps)
  for k = n; k >= 0; k = k - 1 {
    let comments = self.pop_comments(start_byte_index(rights[k]))
    let name_doc = breakable_space + commented(binop(ops[k]), comments[:])
    let right_side = self.bin_op_side(ops[k], rights[k], nest_steps)
    doc = doc +
      (if nest_steps { nest(name_doc) } else { name_doc }) +
      t(" ") +
      right_side
    if k > 0 {
      // Link k is the left side of link k - 1 (`bin_op_side`).
      doc = if can_be_grouped_with(ops[k], ops[k - 1]) {
        doc
      } else {
        operator_side(group(doc), precedence(ops[k - 1]), precedence(ops[k]))
      }
    }
  }
  doc
}

///|
/// The compiler's `bin_op_side`.
fn Formatter::bin_op_side(
  self : Formatter,
  operator : @ast.BinOp,
  side : @ast.Expr,
  nest_steps : Bool,
) -> @pretty.Doc raise FormatError {
  let side_doc = match side {
    String(value~, ..) => bin_op_string(value)
    BinOp(operator~, left~, right~, ..) =>
      self.bin_op(operator, left, right, nest_steps)
    _ => self.expr(side)
  }
  match expr_bin_op_name(side) {
    Some(side_name) if can_be_grouped_with(side_name, operator) => side_doc
    _ =>
      operator_side(
        group(side_doc),
        precedence(operator),
        expr_bin_op_precedence(side),
      )
  }
}

///|
/// The compiler's `pipeline`.
fn Formatter::pipeline(
  self : Formatter,
  expressions : ArrayView[@ast.Expr],
  nest_pipe : Bool,
) -> @pretty.Doc raise FormatError {
  let first = expressions[0]
  let first_precedence = expr_bin_op_precedence(first)
  let first_doc = group(self.expr(first))
  let first_step = operator_side(first_doc, 5, first_precedence)
  let pipeline_start = first.location().start
  let pipeline_end = expressions[expressions.length() - 1].location().end
  let try_to_keep_on_one_line = !self.spans_multiple_lines(
    pipeline_start, pipeline_end,
  )
  let steps_separator = if try_to_keep_on_one_line {
    breakable_space
  } else {
    line_doc
  }
  let stops = [first_step]
  for expression in expressions[1:] {
    let comments = self.pop_comments(expression.location().start)
    let commented_pipe = commented(t("|> "), comments[:])
    let pipe = steps_separator + commented_pipe
    let pipe = if nest_pipe { nest(pipe) } else { pipe }
    let expression_doc = self.expression_on_right_hand_side_of_pipe(
      nest_pipe, expression,
    )
    let expression_doc = operator_side(
      expression_doc,
      4,
      expr_bin_op_precedence(expression),
    )
    stops.push(pipe + expression_doc)
  }
  if try_to_keep_on_one_line {
    @pretty.concat(stops)
  } else {
    @pretty.force_break(@pretty.concat(stops))
  }
}

///|
/// The compiler's `expression_on_right_hand_side_of_pipe`.
fn Formatter::expression_on_right_hand_side_of_pipe(
  self : Formatter,
  nest_pipe : Bool,
  expression : @ast.Expr,
) -> @pretty.Doc raise FormatError {
  let doc = match expression {
    Fn(kind=Capture(..), body~, ..) =>
      self.fn_capture(body, RightHandSideOfPipe)
    _ => self.expr(expression)
  }
  if nest_pipe {
    nest(doc)
  } else {
    doc
  }
}