///|
priv struct ValueBindings {
  program : Program
  declarations : Array[Int32]
  functions : Array[Bool]
  has_writes : Array[Bool]
  alias_declarations : Array[Int32]
  function_value_declarations : Array[Int32]
  call_write_targets : Array[Int32]
}

///|
fn ValueBindings::new(
  program : Program,
  rename_top_level : Bool,
) -> ValueBindings {
  let bindings = {
    program,
    declarations: Array::make(program.symbols.length(), -1),
    functions: Array::make(program.symbols.length(), false),
    has_writes: Array::make(program.symbols.length(), false),
    alias_declarations: [],
    function_value_declarations: [],
    call_write_targets: [],
  }
  for id in 0..
        for index in 0..= 0 else {
            bindings.mark_written(declaration.a)
            continue
          }
          bindings.declarations[symbol] = id
          if node.a == DECLARATION_CONST &&
            node.c == 1 &&
            declaration.b >= 0 &&
            program.nodes[program.unwrapped_expression(declaration.b)].kind
            is Identifier {
            bindings.alias_declarations.push(id)
          }
          if node.a == DECLARATION_VAR {
            bindings.has_writes[symbol] = true
          }
        }
      FunctionDeclaration => {
        let name = program.functions[node.a].name
        guard name >= 0 else { continue }
        let symbol = program.references[name].symbol
        if bindings.declarations[symbol] >= 0 {
          bindings.has_writes[symbol] = true
        }
        bindings.declarations[symbol] = id
        let entry = program.symbols[symbol]
        bindings.functions[symbol] = entry.flags == 0 &&
          entry.link < 0 &&
          (
            program.scopes[entry.scope].kind == SCOPE_FUNCTION ||
            (entry.scope == 0 && rename_top_level)
          )
        if bindings.functions[symbol] && entry.use_count == 2 {
          bindings.function_value_declarations.push(id)
        }
      }
      Assignment => bindings.mark_written(node.b)
      Update => bindings.mark_written(node.c)
      Unary if program.operators[node.a] is Delete =>
        bindings.mark_written(node.b)
      ForIn | ForOf => bindings.mark_written(node.a)
      If => {
        bindings.mark_conditional_function(node.b)
        bindings.mark_conditional_function(node.c)
      }
      While | With => bindings.mark_conditional_function(node.b)
      DoWhile => bindings.mark_conditional_function(node.a)
      For => bindings.mark_conditional_function(node.d)
      Labeled => bindings.mark_conditional_function(node.c)
      _ => ()
    }
  }
  // A named default export escapes without an ordinary Identifier reference.
  // Its declaration must survive even when every direct local call is folded.
  for statement in program.modules {
    guard statement is ExportDefault(body) else { continue }
    let declaration = program.nodes[body]
    guard declaration.kind is FunctionDeclaration else { continue }
    let name = program.functions[declaration.a].name
    if name >= 0 {
      bindings.functions[program.references[name].symbol] = false
    }
  }
  bindings
}

///|
fn ValueBindings::mark_conditional_function(
  self : ValueBindings,
  id : Int32,
) -> Unit {
  guard id >= 0 else { return }
  let declaration = self.program.nodes[id]
  guard declaration.kind is FunctionDeclaration else { return }
  let name = self.program.functions[declaration.a].name
  guard name >= 0 else { return }
  let symbol = self.program.references[name].symbol
  self.has_writes[symbol] = true
  self.functions[symbol] = false
}

///|
fn ValueBindings::initialized_read(
  self : ValueBindings,
  id : Int32,
  before : Int32,
  scope : Int32,
) -> Bool {
  let program = self.program
  let symbol = program.identifier_symbol(id)
  guard symbol >= 0 else { return self.stable_prefix(id, before, scope) }
  if self.functions[symbol] {
    return self.stable_prefix(id, before, scope)
  }
  let entry = program.symbols[symbol]
  let environment = program.scopes[entry.scope]
  // A later case can be entered without executing earlier declarations in
  // this shared lexical environment. Source order does not prove readiness.
  guard environment.kind != SCOPE_SWITCH else { return false }
  let is_parameter = environment.kind == SCOPE_PARAMETERS ||
    environment.kind == SCOPE_ARROW_PARAMETERS
  let declaration = self.declarations[symbol]
  if entry.scope == 0 && declaration >= 0 {
    let binding = program.nodes[declaration]
    if binding.kind is VariableDeclaration && binding.a == DECLARATION_VAR {
      return false
    }
  }
  guard is_parameter || (declaration >= 0 && declaration < before) else {
    return self.stable_prefix(id, before, scope)
  }
  for current = scope {
    guard current >= 0 else { break false }
    if current == entry.scope {
      break true
    }
    let ancestor = program.scopes[current]
    guard !(ancestor.kind is (SCOPE_PARAMETERS | SCOPE_ARROW_PARAMETERS)) else {
      break false
    }
    continue ancestor.parent
  }
}

///|
/// These expressions may read or throw, but cannot change preceding values.
/// Member access and coercing operators are excluded because they can invoke
/// user code. Allocation alone does not make an object literal observable.
fn ValueBindings::has_no_side_effects(self : ValueBindings, id : Int32) -> Bool {
  let program = self.program
  guard id >= 0 else { return true }
  let node = program.nodes[id]
  match node.kind {
    Identifier => {
      let symbol = program.references[node.a].symbol
      guard symbol >= 0 else { return program.is_undefined_value(id) }
      if program.symbols[symbol].scope > 0 || program.modules.length() > 0 {
        return true
      }
      let declaration = self.declarations[symbol]
      guard declaration >= 0 else { return false }
      let binding = program.nodes[declaration]
      binding.kind is VariableDeclaration && binding.a != DECLARATION_VAR
    }
    NumberLiteral
    | StringLiteral
    | BooleanLiteral
    | NullLiteral
    | RegexLiteral
    | TemplateNoSubstitution
    | FunctionExpression
    | ArrowExpression => true
    Parenthesized | EvaluatedValue => self.has_no_side_effects(node.a)
    Unary if program.operators[node.a] is (LogicalNot | TypeOf | Void) =>
      self.has_no_side_effects(node.b)
    Binary if program.operators[node.a] is (StrictlyEqual | StrictlyNotEqual) =>
      self.has_no_side_effects(node.b) && self.has_no_side_effects(node.c)
    Logical =>
      self.has_no_side_effects(node.b) && self.has_no_side_effects(node.c)
    Conditional =>
      self.has_no_side_effects(node.a) &&
      self.has_no_side_effects(node.b) &&
      self.has_no_side_effects(node.c)
    ArrayLiteral | Sequence => {
      for index in 0.. {
      for index in 0.. false
  }
}

///|
/// These initializers neither change state nor throw. They may therefore move
/// after an effect-free read even when that read could throw for an outer TDZ.
fn ValueBindings::is_inert_initializer(
  self : ValueBindings,
  id : Int32,
  before : Int32,
  scope : Int32,
) -> Bool {
  guard id >= 0 else { return false }
  let node = self.program.nodes[id]
  match node.kind {
    Identifier => self.initialized_read(id, before, scope)
    NumberLiteral
    | ZeroLiteral
    | StringLiteral
    | BooleanLiteral
    | NullLiteral
    | RegexLiteral
    | TemplateNoSubstitution
    | FunctionExpression
    | ArrowExpression => true
    Parenthesized | EvaluatedValue =>
      self.is_inert_initializer(node.a, before, scope)
    _ => false
  }
}

///|
fn ValueBindings::mark_written(self : ValueBindings, id : Int32) -> Unit {
  guard id >= 0 else { return }
  let program = self.program
  let node = program.nodes[id]
  match node.kind {
    Call => self.call_write_targets.push(id)
    Identifier | PatternIdentifier => {
      let symbol = program.references[node.a].symbol
      if symbol >= 0 {
        self.has_writes[symbol] = true
      }
    }
    VariableDeclaration =>
      for index in 0.. self.mark_written(node.a)
    PatternProperty => self.mark_written(node.b)
    PatternArray | PatternObject =>
      for index in 0.. ()
  }
}

///|
/// A prefix may be crossed only if it cannot throw and its value cannot be
/// changed by the initializer being moved. Function bindings are hoisted;
/// lexical constants must already be initialized in this execution frame.
fn ValueBindings::stable_prefix(
  self : ValueBindings,
  id : Int32,
  before : Int32,
  scope : Int32,
) -> Bool {
  let program = self.program
  let node = program.nodes[program.unwrapped_expression(id)]
  match node.kind {
    NumberLiteral | StringLiteral | BooleanLiteral | NullLiteral => true
    Identifier => {
      let symbol = program.references[node.a].symbol
      guard symbol >= 0 && !self.has_writes[symbol] else { return false }
      let entry = program.symbols[symbol]
      guard entry.flags == 0 && entry.link < 0 else { return false }
      guard program.scopes[entry.scope].kind != SCOPE_SWITCH else {
        return false
      }
      if self.functions[symbol] {
        return true
      }
      let declaration = self.declarations[symbol]
      guard declaration >= 0 && declaration < before else { return false }
      guard program.nodes[declaration].kind is VariableDeclaration else {
        return false
      }
      for current = scope {
        guard current >= 0 else { break false }
        if current == entry.scope {
          break true
        }
        let environment = program.scopes[current]
        guard !(environment.kind is (SCOPE_PARAMETERS | SCOPE_ARROW_PARAMETERS)) else {
          break false
        }
        continue environment.parent
      }
    }
    _ => false
  }
}

///|
fn ValueBindings::inline_aliases(
  self : ValueBindings,
  rename_top_level : Bool,
) -> Unit {
  let program = self.program
  let aliases = Array::make(program.symbols.length(), -1)
  let removed : Array[Int32] = []
  for id in self.alias_declarations {
    let declaration = program.nodes[id]
    guard declaration.kind is VariableDeclaration else { continue }
    let binding = program.nodes[program.edges[declaration.b]]
    let symbol = program.identifier_symbol(binding.a)
    guard symbol >= 0 && binding.b >= 0 && !self.has_writes[symbol] else {
      continue
    }
    let entry = program.symbols[symbol]
    guard entry.scope > 0 &&
      entry.flags == 0 &&
      entry.link < 0 &&
      !program.preserved_names.contains(entry.name) else {
      continue
    }
    let source = program.identifier_symbol(binding.b)
    guard source >= 0 &&
      source != symbol &&
      self.stable_prefix(binding.b, id, entry.scope) else {
      continue
    }
    let target = if aliases[source] >= 0 { aliases[source] } else { source }
    let original = program.symbols[target]
    guard (original.scope > 0 || rename_top_level) &&
      original.flags == 0 &&
      original.link < 0 &&
      original.name != NAME_ARGUMENTS &&
      !program.preserved_names.contains(original.name) else {
      continue
    }
    aliases[symbol] = target
    removed.push(id)
  }
  guard removed.length() > 0 else { return }
  for id in removed {
    let declaration = program.nodes[id]
    let binding = program.nodes[program.edges[declaration.b]]
    let symbol = program.identifier_symbol(binding.a)
    let entry = program.symbols[symbol]
    let target = aliases[symbol]
    let original = program.symbols[target]
    program.symbols[target] = {
      ..original,
      use_count: original.use_count + entry.use_count - 2,
    }
    program.symbols[symbol] = { ..entry, flags: entry.flags | SYMBOL_ELIDED, }
    program.nodes[id] = Node::new(Elided, 0, 0, 0, 0)
  }
  for index in 0..= 0 else { continue }
    let target = aliases[reference.symbol]
    if target >= 0 {
      program.references[index] = { ..reference, symbol: target, }
    }
  }
}

///|
priv enum InitializerEffects {
  Unchecked
  Inert
  ReadOnly
  MayChange
}

///|
priv struct ValueMovement {
  bindings : ValueBindings
  symbol : Int32
  before : Int32
  scope : Int32
  initializer : Int32
  mut initializer_effects : InitializerEffects
  mut detach_receiver : Bool
}

///|
fn ValueMovement::new(
  bindings : ValueBindings,
  symbol : Int32,
  before : Int32,
  initializer : Int32,
) -> ValueMovement {
  let scope = bindings.program.symbols[symbol].scope
  {
    bindings,
    symbol,
    before,
    scope,
    initializer,
    initializer_effects: Unchecked,
    detach_receiver: false,
  }
}

///|
fn ValueMovement::can_cross_read(self : ValueMovement, id : Int32) -> Bool {
  match self.initializer_effects {
    // A direct leading use needs no classification. Cache it only when the
    // search must cross another read, while the expression arena is unchanged.
    Unchecked => {
      self.initializer_effects = if self.bindings.is_inert_initializer(
          self.initializer,
          self.before,
          self.scope,
        ) {
        Inert
      } else if self.initializer >= 0 &&
        self.bindings.has_no_side_effects(self.initializer) {
        ReadOnly
      } else {
        MayChange
      }
      self.can_cross_read(id)
    }
    Inert =>
      self.bindings.has_no_side_effects(id) ||
      self.bindings.initialized_read(id, self.before, self.scope)
    ReadOnly => self.bindings.initialized_read(id, self.before, self.scope)
    MayChange => self.bindings.stable_prefix(id, self.before, self.scope)
  }
}