///|
pub(all) enum IntExpr {
  Literal(Int32)
  Variable(Int32)
  Add(IntExpr, IntExpr)
  Subtract(IntExpr, IntExpr)
  Choose(BoolExpr, IntExpr, IntExpr)
} derive(Eq, @debug.Debug)

///|
pub(all) enum BoolExpr {
  Boolean(Bool)
  Less(IntExpr, IntExpr)
  Not(BoolExpr)
} derive(Eq, @debug.Debug)

///|
pub(all) struct Program {
  bindings : Array[IntExpr]
  result : IntExpr
} derive(Eq, @debug.Debug)

///|
fn IntExpr::evaluate(self : IntExpr, scope : Array[Int32]) -> Int32? {
  let value : Int64 = match self {
    Literal(n) => n.to_int64()
    Variable(index) => {
      guard index >= 0 && index < scope.length() else { return None }
      scope[index].to_int64()
    }
    Add(a, b) => {
      guard a.evaluate(scope) is Some(x) else { return None }
      guard b.evaluate(scope) is Some(y) else { return None }
      x.to_int64() + y.to_int64()
    }
    Subtract(a, b) => {
      guard a.evaluate(scope) is Some(x) else { return None }
      guard b.evaluate(scope) is Some(y) else { return None }
      x.to_int64() - y.to_int64()
    }
    Choose(c, a, b) => {
      guard c.evaluate(scope) is Some(condition) else { return None }
      // Validate both branches, including dead code, before accepting a program.
      guard a.evaluate(scope) is Some(x) else { return None }
      guard b.evaluate(scope) is Some(y) else { return None }
      (if condition { x } else { y }).to_int64()
    }
  }
  if value < -2147483648L || value > 2147483647L {
    None
  } else {
    Some(value.to_int())
  }
}

///|
fn BoolExpr::evaluate(self : BoolExpr, scope : Array[Int32]) -> Bool? {
  match self {
    Boolean(b) => Some(b)
    Less(a, b) => {
      guard a.evaluate(scope) is Some(x) else { return None }
      guard b.evaluate(scope) is Some(y) else { return None }
      Some(x < y)
    }
    Not(e) => e.evaluate(scope).map(b => !b)
  }
}

///|
pub fn Program::evaluate(self : Program) -> Int32? {
  let scope : Array[Int32] = []
  for binding in self.bindings {
    guard binding.evaluate(scope) is Some(value) else { return None }
    scope.push(value)
  }
  self.result.evaluate(scope)
}

///|
pub fn Program::valid(self : Program) -> Bool {
  self.evaluate() is Some(_)
}

///|
fn IntExpr::size(self : IntExpr) -> Int32 {
  match self {
    Literal(_) | Variable(_) => 1
    Add(a, b) | Subtract(a, b) => 1 + a.size() + b.size()
    Choose(c, a, b) => 1 + c.size() + a.size() + b.size()
  }
}

///|
fn BoolExpr::size(self : BoolExpr) -> Int32 {
  match self {
    Boolean(_) => 1
    Less(a, b) => 1 + a.size() + b.size()
    Not(e) => 1 + e.size()
  }
}

///|
pub fn Program::size(self : Program) -> Int32 {
  self.bindings.fold(init=self.result.size(), (n, e) => n + 1 + e.size())
}

///|
fn IntExpr::emit(self : IntExpr) -> String {
  match self {
    Literal(n) => if n < 0 { "(\{n})" } else { "\{n}" }
    Variable(index) => "v\{index}"
    Add(a, b) => "(\{a.emit()} + \{b.emit()})"
    Subtract(a, b) => "(\{a.emit()} - \{b.emit()})"
    Choose(c, a, b) => "(if \{c.emit()} { \{a.emit()} } else { \{b.emit()} })"
  }
}

///|
fn BoolExpr::emit(self : BoolExpr) -> String {
  match self {
    Boolean(b) => "\{b}"
    Less(a, b) => "(\{a.emit()} < \{b.emit()})"
    Not(e) => "(!\{e.emit()})"
  }
}

///|
pub fn Program::emit(self : Program) -> String {
  let lines = ["fn main {"]
  for i, e in self.bindings {
    lines.push("  let v\{i} : Int = \{e.emit()}")
  }
  lines.push("  println(\{self.result.emit()})")
  lines.push("}")
  lines.join("\n") + "\n"
}

///|
pub extend IntExpr with Eq::{equal, not_equal}

///|
pub extend BoolExpr with Eq::{equal, not_equal}

///|
pub extend Program with Eq::{equal, not_equal}

///|
pub extend IntExpr with @debug.Debug::{to_repr}

///|
pub extend BoolExpr with @debug.Debug::{to_repr}

///|
pub extend Program with @debug.Debug::{to_repr}