///|
/// Characters that the reader gives a meaning outside a character set.
fn special(c : String) -> Bool {
  [
    "^", "$", "|", "[", "(", ")", "*", "+", "?", "{", ".", "\\",
  ].contains(c)
}

///|
priv struct Writer {
  out : StringBuilder
  limit : Int
  mut used : Int
  mut failure : String?
}

///|
fn Writer::write(self : Writer, text : String) -> Unit {
  if self.failure is Some(_) {
    return
  }
  if text.length() > self.limit - self.used {
    self.failure = Some("regex/limit")
    return
  }
  self.used += text.length()
  self.out.write_string(text)
}

///|
fn Writer::unrepresentable(self : Writer, message : String) -> Unit {
  if self.failure is None {
    self.failure = Some("regex/unrepresentable:" + message)
  }
}

///|
fn set_char(c : String) -> String {
  if c == "]" || c == "\\" || c == "-" || c == "^" {
    "\\" + c
  } else {
    c
  }
}

///|
fn class_text(kind : @model.ClassKind) -> String {
  match kind {
    Any => "."
    Shorthand(c) => "\\" + c.to_string()
    Property(name) => "\\p{" + name + "}"
    CodePoint(spelling) => spelling
    Posix(name) => "[[:" + name + ":]]"
  }
}

///|
fn anchor_text(kind : @model.AnchorKind) -> String {
  match kind {
    LineStart => "^"
    LineEnd => "$"
    InputStart => "\\A"
    InputEndBeforeNewline => "\\Z"
    InputEnd => "\\z"
    PreviousMatchEnd => "\\G"
    WordBoundary => "\\b"
    NonWordBoundary => "\\B"
  }
}

///|
/// A spelling that the reader reads back as `bounds`, or `None`.
fn bounds_text(bounds : @model.Bounds) -> String? {
  let canonical = @model.Bounds::new(
    bounds.min,
    max?=bounds.max,
    reluctant=bounds.reluctant,
  )
  if bounds.spelling == canonical.spelling {
    return Some(canonical.spelling)
  }
  match tokenize("a" + bounds.spelling, 0) {
    Ok([_, { lex: Quantifier(read), .. }]) if read == bounds => Some(bounds.spelling)
    _ => None
  }
}

///|
priv enum Job {
  Expr(Expr, Int)
  Text(String)
}

///|
/// Write a pattern from typed values. An expression that the reader cannot
/// read back to the same value gives a diagnostic instead of text.
pub fn print_pattern(
  expr : Expr,
  limits? : @envelope.Limits = @envelope.Limits::new(),
) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
  let w = Writer::{ out: StringBuilder(), limit: limits.output, used: 0, failure: None }
  // Contexts: 0 = alternative, 1 = concatenation item, 2 = quantified operand.
  let jobs : Array[Job] = [Expr(expr, 0)]
  let mut work = 0
  while jobs.pop() is Some(job) && w.failure is None {
    work += 1
    if work > limits.work {
      w.failure = Some("regex/limit")
      break
    }
    match job {
      Text(text) => w.write(text)
      Expr(e, context) =>
        match e {
          Char(c, escaped, _) => {
            let u = if c.is_empty() { 0 } else { c.code_unit_at(0).to_int() }
            let alphanumeric = is_letter_unit(u) || (u >= 48 && u <= 57)
            if code_point(c, 0).1 != c.length() || c.is_empty() {
              w.unrepresentable("a character holds one code point")
            } else if escaped {
              if alphanumeric {
                w.unrepresentable("an escaped letter or digit has another meaning")
              } else {
                w.write("\\" + c)
              }
            } else if special(c) {
              w.unrepresentable("an operator character must be escaped")
            } else {
              w.write(c)
            }
          }
          Class(kind, _) => w.write(class_text(kind))
          Backreference(n, _) =>
            if n.length() == 1 && n != "0" {
              w.write("\\" + n)
            } else {
              w.unrepresentable("a backreference has one digit from 1 to 9")
            }
          Set(negated, items, _) => {
            if items.is_empty() {
              w.unrepresentable("a character set needs an item")
            }
            w.write(if negated { "[^" } else { "[" })
            for i, item in items {
              match item {
                Char(c) =>
                  w.write(
                    if i == 0 && !negated && c == "^" {
                      "\\^"
                    } else {
                      set_char(c)
                    },
                  )
                Range(a, b) => w.write(set_char(a) + "-" + set_char(b))
                Escape(e) => w.write(e)
              }
            }
            w.write("]")
          }
          Anchor(kind, _) => w.write(anchor_text(kind))
          Concat(items) => {
            if items.length() < 2 {
              w.unrepresentable("a concatenation needs two items")
            }
            if context == 2 {
              w.unrepresentable("a quantified concatenation needs a group")
            }
            for i in 0.. {
            if items.length() < 2 {
              w.unrepresentable("an alternation needs two alternatives")
            }
            if context != 0 {
              w.unrepresentable("a choice inside a sequence needs a group")
            }
            for i in 0.. {
            let open = match kind {
              Capturing => "("
              NonCapturing => "(?:"
              Named(name) => {
                if name.is_empty() ||
                  !name.iter().all(c => is_name_char(c.to_int())) {
                  w.unrepresentable("a group name has letters only")
                }
                "(?<" + name + ">"
              }
            }
            w.write(open)
            jobs.push(Text(")"))
            jobs.push(Expr(inner, 0))
          }
          Look(look, positive, inner, _) => {
            w.write(
              match (look, positive) {
                (Ahead, true) => "(?="
                (Ahead, false) => "(?!"
                (Behind, true) => "(?<="
                (Behind, false) => "(?
            match bounds_text(bounds) {
              None => w.unrepresentable("the quantifier spelling is invalid")
              Some(text) => {
                // `x*?` reads as one reluctant quantifier, not two.
                if text.has_prefix("?") &&
                  inner is Quantified(_, { reluctant: false, .. }, _) {
                  w.unrepresentable("an optional quantifier after a greedy quantifier reads as reluctant")
                }
                jobs.push(Text(text))
                jobs.push(Expr(inner, 2))
              }
            }
        }
    }
  }
  match w.failure {
    None => {
      let text = w.out.to_string()
      // A body line must not read as a comment, directive or statement.
      let leading = text.has_prefix("'") ||
        text.has_prefix("!") ||
        text.has_prefix("/'") ||
        text.has_prefix("@") ||
        text.has_prefix(" ") ||
        text.has_prefix("\t") ||
        text.has_suffix(" ") ||
        text.has_suffix("\t") ||
        @envelope.has_preprocessor_call(text)
      let first = text.split(" ").next().map(s => s.to_owned()).unwrap_or("")
      let statement = [
        "title", "caption", "header", "footer", "legend", "skinparam", "skin", "scale",
        "hide", "show", "sprite", "rotate", "page", "minwidth", "mainframe", "