///|
/// Compiled rules are private. Each compile parses its own AST, so caller-owned
/// mutable AST arrays cannot invalidate the compiled program.
pub struct Program {
  priv rules : Map[String, Expr]
  priv generalized : Map[String, Bool]
  priv productive : Map[String, Bool]
}

///|
priv struct Analysis {
  source : Bytes
  mut work : Int32
  max_work : Int32
}

///|
fn Analysis::tick(self : Analysis, offset : Int32) -> Unit raise ParseFailure {
  if self.work >= self.max_work {
    raise ParseFailure(
      diagnostic(
        self.source,
        offset,
        "compile-budget",
        "analysis work exhausted",
      ),
    )
  }
  self.work = self.work + 1
}

///|
fn validate_references(
  e : Expr,
  rules : Map[String, Expr],
  a : Analysis,
) -> Unit raise ParseFailure {
  a.tick(e.offset())
  match e {
    Ref(n, p) =>
      if !rules.contains(n) {
        raise ParseFailure(
          diagnostic(a.source, p, "undefined-rule", "undefined rule \{n}"),
        )
      }
    Prose(_, p) =>
      raise ParseFailure(
        diagnostic(
          a.source,
          p,
          "prose-unsupported",
          "prose-val cannot be recognized",
        ),
      )
    Sequence(es, _) | Choice(es, _) =>
      for e in es {
        validate_references(e, rules, a)
      }
    Repeat(_, _, e, _) => validate_references(e, rules, a)
    _ => ()
  }
}

///|
/// Core rules are implicit defaults. Explicit '=' overrides a core default;
/// '=/‘ may extend it. Duplicate explicit '=' and extension before base fail.
fn compile_domain(
  source : Bytes,
  limits : ParseLimits,
  max_work : Int32,
  scalar : Bool,
  bindings : Array[ProseBinding],
  allow_prose : Bool,
) -> Result[Program, Diagnostic] {
  if max_work < 1 || max_work > 10000000 {
    return Err(
      diagnostic(
        source, 0, "invalid-limits", "compile work must be 1..10000000",
      ),
    )
  }
  match parse_domain(source, limits, if scalar { 2147483647 } else { 255 }) {
    Err(d) => Err(d)
    Ok(g) =>
      try {
        if g.rules.is_empty() {
          raise ParseFailure(
            diagnostic(
              source, 0, "no-rules", "compilation requires a user rule",
            ),
          )
        }
        let a = { source, work: 0, max_work, }
        let lowering : Lowering = {
          a,
          nodes: 0,
          max_nodes: limits.max_nodes,
          scalar,
        }
        let predicates : Map[String, Expr] = Map([])
        for binding in bindings {
          a.tick(0)
          for c in binding.name.iter() {
            a.tick(0)
            let n = c.to_int()
            if n < 32 || n > 126 || n == 62 {
              raise ParseFailure(
                diagnostic(
                  source, 0, "invalid-prose-name", "binding name must be valid prose text",
                ),
              )
            }
          }
          if predicates.contains(binding.name) {
            raise ParseFailure(
              diagnostic(
                source, 0, "duplicate-prose-binding", "duplicate exact prose name",
              ),
            )
          }
          predicates[binding.name] = lowering.predicate(binding.predicate)
        }
        let rules = core_rules()
        let defined : Map[String, Bool] = Map([])
        for rule in g.rules {
          let expression = lowering.expression(
            rule.expression,
            predicates,
            allow_prose,
          )

          if rule.incremental {
            match rules.get(rule.name) {
              None =>
                raise ParseFailure(
                  diagnostic(
                    source,
                    rule.offset,
                    "increment-without-base",
                    "extension before base \{rule.name}",
                  ),
                )
              Some(old) => {
                // Flatten successive extensions to keep compiler recursion bounded.
                let es = match old {
                  Choice(es, _) => es.copy()
                  _ => [old]
                }
                es.push(expression)
                rules[rule.name] = Choice(es, rule.offset)
                defined[rule.name] = true
              }
            }
          } else {
            if defined.contains(rule.name) {
              raise ParseFailure(
                diagnostic(
                  source,
                  rule.offset,
                  "duplicate-rule",
                  "duplicate base \{rule.name}",
                ),
              )
            }
            rules[rule.name] = expression
            defined[rule.name] = true
          }
        }
        for _, e in rules {
          validate_references(e, rules, a)
        }
        let productive = fixed_point(rules, false, a)
        let generalized = validate_recursion(rules, a)
        Ok({ rules, generalized, productive, })
      } catch {
        ParseFailure(d) => Err(d)
      }
  }
}

///|
/// Deterministic inventory useful to grammar-checking tools.
pub fn Program::rule_names(self : Program) -> Array[String] {
  let names = self.rules.keys().collect()
  names.sort()
  names
}

///|
/// Legacy byte compiler. Prose remains rejected unless explicitly bound using
/// compile_with_prose. Byte behavior and the numeric ceiling are unchanged.
pub fn compile(
  source : Bytes,
  limits~ : ParseLimits = default_parse_limits(),
  max_work~ : Int32 = 1000000,
) -> Result[Program, Diagnostic] {
  compile_domain(source, limits, max_work, false, [], false)
}

///|
/// Bind exact prose names to finite languages or single-symbol ranges.
/// No natural-language interpretation or user callback is executed.
pub fn compile_with_prose(
  source : Bytes,
  bindings : Array[ProseBinding],
  limits~ : ParseLimits = default_parse_limits(),
  max_work~ : Int32 = 1000000,
) -> Result[Program, Diagnostic] {
  compile_domain(source, limits, max_work, false, bindings, true)
}