///|
fn raw_text(
  root : RawNode,
  final_fodder : Array[SourcePart],
  limits : SyntaxLimits,
) -> String raise JsonnetError {
  let loc = root.range
    .map(r => r.begin)
    .unwrap_or({ file: "", line: 1, column: 1, })
  check_syntax_limits(limits, loc)
  let writer : FormatOutput = {
    out: StringBuilder(),
    bytes: 0,
    steps: 0,
    line_start: true,
    column: 0,
    limits: { ..limits.output_limits(), output_bytes: limits.source_bytes, },
    loc,
  }
  let stack : Array[(RawPart, Int)] = [(Child(root), 0)]
  let mut nodes = 0
  while stack.pop() is Some((part, depth)) {
    writer.tick()
    match part {
      Text(part) => writer.write(part.text)
      Child(node) => {
        if depth >= limits.tree_depth || nodes >= limits.nodes {
          raise problem(Limit, loc, "raw syntax tree limit exceeded")
        }
        nodes += 1
        for i = node.parts.length() - 1; i >= 0; i = i - 1 {
          writer.tick()
          stack.push((node.parts[i], depth + 1))
        }
      }
    }
  }
  for part in final_fodder {
    writer.tick()
    writer.write(part.text)
  }
  writer.out.to_string()
}

///|
/// Serialize edited syntax exactly. Cycles/depth, nodes, steps and source bytes
/// are bounded; this does not itself validate the resulting Jsonnet grammar.
pub fn RawAst::text(
  self : RawAst,
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> String raise JsonnetError {
  raw_text(self.root, self.final_fodder, limits)
}

///|
pub fn RawNode::text(
  self : RawNode,
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> String raise JsonnetError {
  raw_text(self, [], limits)
}

///|
/// Reparse and format editable expression-node syntax with separate final trivia. Tags and
/// retained ranges are metadata; parts determine the admitted source. The input
/// node is not mutated and no expression/import is executed.
pub fn format_node(
  node : RawNode,
  final_fodder? : Array[SourcePart] = [],
  options? : FormatOptions = FormatOptions::default(),
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> String raise JsonnetError {
  let filename = node.range.map(r => r.begin.file).unwrap_or("")
  format_syntax_source(
    raw_text(node, final_fodder, limits),
    filename~,
    options~,
    limits~,
  )
}

///|
pub fn RawAst::format(
  self : RawAst,
  options? : FormatOptions = FormatOptions::default(),
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> String raise JsonnetError {
  format_node(self.root, final_fodder=self.final_fodder, options~, limits~)
}

///|
priv enum RawCloneFrame {
  Enter(RawNode, Int)
  Leave(RawNode, Int)
}

///|
/// Clone every child occurrence iteratively, preserving metadata and isolating
/// mutable arrays. Complete tree/node/work budgets also bound cyclic edits.
fn clone_raw_node(
  root : RawNode,
  limits : SyntaxLimits,
  work : FormatOutput,
) -> RawNode raise JsonnetError {
  let loc = root.range
    .map(r => r.begin)
    .unwrap_or({ file: "", line: 1, column: 1, })
  check_syntax_limits(limits, loc)
  let stack : Array[RawCloneFrame] = [Enter(root, 0)]
  let completed : Array[RawNode] = []
  let mut nodes = 0
  while stack.pop() is Some(frame) {
    work.tick()
    match frame {
      Enter(node, depth) => {
        if depth >= limits.tree_depth || nodes >= limits.nodes {
          raise problem(Limit, loc, "raw syntax tree limit exceeded")
        }
        nodes += 1
        let children : Array[RawNode] = []
        for part in node.parts {
          work.tick()
          match part {
            Child(child) => children.push(child)
            Text(_) => ()
          }
        }
        stack.push(Leave(node, children.length()))
        for i = children.length() - 1; i >= 0; i = i - 1 {
          stack.push(Enter(children[i], depth + 1))
        }
      }
      Leave(node, count) => {
        let children = completed[completed.length() - count:].to_owned()
        let mut position = 0
        let parts = node.parts.map(part => {
          match part {
            Text(part) => Text(part)
            Child(_) => {
              let child = children[position]
              position += 1
              Child(child)
            }
          }
        })
        for _ in 0.. RawNode raise JsonnetError {
  let loc = self.range
    .map(r => r.begin)
    .unwrap_or({ file: "", line: 1, column: 1, })
  let work : FormatOutput = {
    out: StringBuilder(),
    bytes: 0,
    steps: 0,
    line_start: true,
    column: 0,
    limits: limits.output_limits(),
    loc,
  }
  clone_raw_node(self, limits, work)
}

///|
pub fn RawAst::clone(
  self : RawAst,
  limits? : SyntaxLimits = SyntaxLimits::default(),
) -> RawAst raise JsonnetError {
  let loc = self.root.range
    .map(r => r.begin)
    .unwrap_or({ file: "", line: 1, column: 1, })
  let work : FormatOutput = {
    out: StringBuilder(),
    bytes: 0,
    steps: 0,
    line_start: true,
    column: 0,
    limits: limits.output_limits(),
    loc,
  }
  let root = clone_raw_node(self.root, limits, work)
  let final_fodder = self.final_fodder.map(part => {
    work.tick()
    part
  })
  { root, final_fodder, }
}