// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/src/wolf/ir/ir.dart';

/// Analyzes the scopes in a [BaseIRContainer].
///
/// This function computes the nesting of begin/end scopes in [ir]. A begin/end
/// scope is the set of instructions between a "begin" instruction (a `block`,
/// `loop`, `tryCatch`, `tryFinally`, `function`, or `instanceFunction`
/// instruction) and the `end` instruction that matches it.
Scopes analyzeScopes(BaseIRContainer ir,
    {ScopeAnalyzerEventListener? eventListener}) {
  eventListener ??= ScopeAnalyzerEventListener();
  var scopeAnalyzer = _ScopeAnalyzer(ir, eventListener);
  eventListener._scopeAnalyzer = scopeAnalyzer;
  scopeAnalyzer.run();
  eventListener._scopeAnalyzer = null;
  return Scopes._(scopeAnalyzer);
}

/// Event listener used by [analyzeScopes] to report progress information.
///
/// By itself this class does nothing; the caller of [analyzeScopes] should make
/// a derived class that overrides one or more of the `on...` methods.
base class ScopeAnalyzerEventListener {
  /// When an invocation of [analyzeScopes] is running and using this listener,
  /// the [_ScopeAnalyzer] that is performing the analysis; otherwise `null`.
  _ScopeAnalyzer? _scopeAnalyzer;

  /// Allocates a fresh state variable.
  ///
  /// Clients of the scope analyzer can use this method to allocate state
  /// variables to track pieces of state that are specific to a particular kind
  /// of analysis (e.g., a state variable could be used to track the number of
  /// elements in a list that's referred to by the code being analyzed, or to
  /// track the state of the event loop in the case of asynchronous code).
  StateVar createStateVar() => _scopeAnalyzer!.createStateVar();

  /// Called for each variable allocated by an `alloc` instruction.
  ///
  /// [index] is the index of the allocated variable (counting from zero at the
  /// start of the code being analyzed; every call to this method within the
  /// context of a single call to [analyzeScopes] will an index one greater than
  /// the previous call).
  ///
  /// [stateVar] is the state variable that the scope analyzer has allocated to
  /// track the value of the variable.
  void onAlloc({required int index, required StateVar stateVar}) {}

  /// Called when the scope analyzer has completely finished analyzing the
  /// instruction stream.
  void onFinished() {}

  /// Called prior to visiting each instruction.
  ///
  /// This call back is invoked before more specific callbacks like [onAlloc].
  void onInstruction(int address) {}

  /// Called when [_scopeAnalyzer] is about to process a "begin" instruction.
  void onPushScope({required int address, required int scope}) {}

  /// Records that the value of a state variable was potentially affected.
  ///
  /// Clients of the scope analyzer can use this method to indicate that one of
  /// the state variables returned by [createStateVar] was affected by a later
  /// instruction.
  void stateVarAffected(StateVar stateVar) =>
      _scopeAnalyzer!.stateVarAffected(stateVar);
}

/// The result of scope analysis.
///
/// See [analyzeScopes] for more information.
class Scopes {
  /// Total number of state variables that were allocated.
  ///
  /// Valid values of [StateVar.index] range from `0` to `stateVarCount-1`.
  final int stateVarCount;

  final List<StateVar> _allocIndexToStateVar;
  final List<int> _beginAddresses;
  final List<StateVar> _effects;
  final List<int> _effectCounts;
  final List<int> _effectIndices;
  final List<int> _endAddresses;
  final List<int> _lastDescendants;
  final List<int> _parents;

  Scopes._(_ScopeAnalyzer analyzer)
      : stateVarCount = analyzer.stateVarToPendingEffectsIndex.length,
        _allocIndexToStateVar = analyzer.allocIndexToStateVar,
        _beginAddresses = analyzer.beginAddresses,
        _effects = analyzer.effects,
        _effectCounts = analyzer.effectCounts,
        _effectIndices = analyzer.effectIndices,
        _endAddresses = analyzer.endAddresses,
        _lastDescendants = analyzer.lastDescendants,
        _parents = analyzer.parents;

  /// The number of scopes that was found.
  int get scopeCount => _beginAddresses.length;

  /// The state variable tracking the [allocIndex]th local variable that was
  /// allocated.
  StateVar allocIndexToStateVar(int allocIndex) =>
      _allocIndexToStateVar[allocIndex];

  /// Computes the innermost ancestor of [scope] (or [scope] itself), whose
  /// [endAddress] is greater than or equal to [address].
  ///
  /// This method may be used in conjunction with `lastScopeBefore` to find the
  /// innermost scope containing an instruction, according to the formula:
  ///
  ///     var scope = ancestorContainingAddress(
  ///         scope: lastScopeBefore(address), address: address);
  int ancestorContainingAddress({required int scope, required int address}) {
    while (endAddress(scope) < address) {
      // By validation, we know that the instruction sequence begins with
      // `function` or `instanceFunction`, so the outermost scope covers the
      // whole instruction sequence. Therefore, if `scope` is `0`, that means
      // that `address` must have been an invalid address.
      assert(scope > 0, 'invalid address');
      scope = parent(scope);
    }
    return scope;
  }

  /// The address of the "begin" instruction that opens [scope].
  ///
  /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the
  /// scope opened by the `i`th begin instruction in the IR.
  int beginAddress(int scope) => _beginAddresses[scope];

  /// Retrieves one of the state variables affected by a scope.
  ///
  /// To find the `i`th state variable affected by `scope`, use an [index] value
  /// of `effectIndex(scope) + i`.
  StateVar effectAt(int index) => _effects[index];

  /// The number of state variables affected by instructions in [scope].
  ///
  /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the
  /// scope opened by the `i`th begin instruction in the IR.
  int effectCount(int scope) => _effectCounts[scope];

  /// The first index to pass to [effectAt] to enumerate the state variables
  /// affected by instructions in [scope].
  ///
  /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the
  /// scope opened by the `i`th begin instruction in the IR.
  int effectIndex(int scope) => _effectIndices[scope];

  /// The address of the `end` instruction that closes [scope].
  ///
  /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the
  /// scope opened by the `i`th begin instruction in the IR.
  int endAddress(int scope) => _endAddresses[scope];

  /// The scope index of the last scope transitively contained within [scope].
  ///
  /// If [scope] doesn't contain any other scopes, then [scope] is returned.
  int lastDescendant(int scope) => _lastDescendants[scope];

  /// Computes the highest-numbered scope whose [beginAddress] is less than or
  /// equal to [address].
  ///
  /// Scopes are numbered in pre-order, so the returned scope will either be the
  /// innermost scope containing [address], or one of its ancestors will be (see
  /// [ancestorContainingAddress]).
  int mostRecentScope(int address) {
    assert(address >= 0);
    // By validation, we know that the instruction sequence begins with
    // `function` or `instanceFunction`, so the outermost scope covers the whole
    // instruction sequence.
    assert(beginAddress(0) == 0);
    var low = 0;
    var high = scopeCount;
    while (low < high - 1) {
      // Loop invariants
      assert(beginAddress(low) <= address);
      assert(high == scopeCount || beginAddress(high) > address);

      var mid = (low + high) ~/ 2;
      if (beginAddress(mid) <= address) {
        low = mid;
      } else {
        high = mid;
      }
    }
    return low;
  }

  /// The scope immediately containing [scope], or `-1` if [scope] is the
  /// outermost scope.
  ///
  /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the
  /// scope opened by the `i`th begin instruction in the IR.
  int parent(int scope) => _parents[scope];
}

/// A state variable tracked by the scope analyzer.
///
/// State variables are simply unique integer indices; clients that need to
/// track information about each state variable should store that information in
/// a list indexed by the state variable.
// TODO(paulberry): when extension types are supported, make this an extension
// type.
class StateVar {
  final int index;

  StateVar(this.index);

  @override
  int get hashCode => index.hashCode;

  @override
  bool operator ==(other) => other is StateVar && index == other.index;

  @override
  String toString() => index.toString();
}

/// Analyzer of the scopes, and their effects on state variables, in a
/// [BaseIRContainer].
///
/// This class computes the nesting of begin/end scopes in [ir]. A begin/end
/// scope is the set of instructions between a "begin" instruction (a `block`,
/// `loop`, `tryCatch`, `tryFinally`, `function`, or `instanceFunction`
/// instruction) and the `end` instruction that matches it. It also computes the
/// set of state variables affected by the code within each begin/end scope.
///
/// State variables correspond to local variables in the program, as well as any
/// other significant program state that needs to be tracked; the specific state
/// variables that are tracked are determined by caller of [analyzeScopes].
base class _ScopeAnalyzer {
  static const enableDebugPrints = false;
  final BaseIRContainer ir;
  final ScopeAnalyzerEventListener eventListener;

  /// Stack of scope indices for all open scopes.
  ///
  /// This stack begins with a sentinel value of `-1`, so that when [pushScope]
  /// is called for the first time (to create the outermost scope) it will set
  /// the outermost scope's parent scope to `-1`.
  final scopeIndices = [-1];

  /// Stack of state variables affected by all open scopes.
  ///
  /// The locals written in the outermost scope appear first, then the ones in
  /// the next inner scope, and so on. Locals are de-duplicated within a scope.
  final pendingEffects = <StateVar>[];

  /// Stack of indices into [pendingEffects] representing the start of each
  /// scope enclosing the current scope.
  final outerScopeStarts = <int>[];

  /// Table mapping each state variable to the index of its last appearance in
  /// [pendingEffects].
  ///
  /// And index of -1 means the local does not appear in [pendingEffects].
  final stateVarToPendingEffectsIndex = <int>[];

  /// Table mapping each index in [pendingEffects] to the previous occurrence
  /// of the same state variable in [pendingEffects].
  final previousPendingEffectsIndices = <int>[];

  /// Table mapping each local to the state variable that tracks it.
  final localToStateVar = <StateVar>[];

  /// See [Scopes.allocIndexToStateVar].
  final allocIndexToStateVar = <StateVar>[];

  /// See [Scopes.beginAddress].
  final beginAddresses = <int>[];

  /// See [Scopes.effectAt].
  final effects = <StateVar>[];

  /// See [Scopes.effectCount].
  final effectCounts = <int>[];

  /// See [Scopes.effectIndex].
  final effectIndices = <int>[];

  /// See [Scopes.endAddress].
  final endAddresses = <int>[];

  /// See [Scopes.lastDescendant].
  final lastDescendants = <int>[];

  /// See [Scopes.parent].
  final parents = <int>[];

  var nextAllocIndex = 0;

  /// Index into [pendingEffects] representing the start of the current scope.
  var scopeStart = 0;

  _ScopeAnalyzer(this.ir, this.eventListener);

  bool checkState() {
    if (enableDebugPrints) dumpState();
    var stateVarCount = stateVarToPendingEffectsIndex.length;
    var pendingEffectsCount = pendingEffects.length;
    // `_scopeIndices` always starts with the sentinel value `-1`.
    assert(scopeIndices[0] == -1, 'invalid sentinel value in _scopeIndices');
    // Scopes are numbered in pre-order, so `_scopeIndices` should be
    // monotonically increasing.
    for (var i = 0; i < scopeIndices.length - 1; i++) {
      assert(scopeIndices[i] < scopeIndices[i + 1],
          '_scopeIndices out of order: $scopeIndices');
    }
    // State variables mentioned in `_pendingEffects` must exist.
    for (var stateVar in pendingEffects) {
      assert(
          stateVar.index >= 0 && stateVar.index < stateVarCount,
          '_pendingEffects contains non-existent state var $stateVar: '
          '$pendingEffects');
    }
    // `_scopeStart` and `_outerScopeStarts` should refer to properly nested
    // indices within `_pendingEffects`.
    var previousScopeStart = 0;
    var scopeStarts = [...outerScopeStarts, scopeStart];
    for (var scopeStart in scopeStarts) {
      assert(scopeStart >= previousScopeStart,
          'improper nesting of scope starts: $scopeStarts');
      assert(scopeStart <= pendingEffectsCount,
          'scope start is too large: $scopeStart > $pendingEffectsCount');
    }
    // `_stateVarToPendingEffectsIndex` and `_previousPendingEffectsIndices`
    // should properly reflect the contents of `_pendingEffects`.
    assert(
        previousPendingEffectsIndices.length == pendingEffectsCount,
        '_previousPendingEffectsIndices should have length '
        '$pendingEffectsCount: $previousPendingEffectsIndices');
    var expectedStateVarToPendingEffectsIndex =
        List.filled(stateVarToPendingEffectsIndex.length, -1);
    for (var i = 0; i < pendingEffectsCount; i++) {
      assert(
          previousPendingEffectsIndices[i] ==
              expectedStateVarToPendingEffectsIndex[pendingEffects[i].index],
          '_previousPendingEffectsIndices[$i] is incorrect: expected '
          '${expectedStateVarToPendingEffectsIndex[pendingEffects[i].index]}, '
          'got ${previousPendingEffectsIndices[i]}');
      expectedStateVarToPendingEffectsIndex[pendingEffects[i].index] = i;
    }
    for (var i = 0; i < stateVarCount; i++) {
      assert(
          stateVarToPendingEffectsIndex[i] ==
              expectedStateVarToPendingEffectsIndex[i],
          '_stateVarToPendingEffectsIndex is incorrect: '
          'expected $expectedStateVarToPendingEffectsIndex, '
          'got $stateVarToPendingEffectsIndex');
    }
    // State variables mentioned in `_localToStateVar` must exist.
    for (var stateVar in localToStateVar) {
      assert(
          stateVar.index >= 0 && stateVar.index < stateVarCount,
          '_localToStateVar contains non-existent state var $stateVar: '
          '$localToStateVar');
    }
    return true;
  }

  StateVar createStateVar() {
    var stateVar = StateVar(stateVarToPendingEffectsIndex.length);
    stateVarToPendingEffectsIndex.add(-1);
    return stateVar;
  }

  void dumpState() {
    print('  scopeIndices: $scopeIndices');
    print('  pendingEffects: $pendingEffects');
    print('  scopeStart: $scopeStart');
    print('  outerScopeStarts: $outerScopeStarts');
    print('  stateVarToPendingEffectsIndex: $stateVarToPendingEffectsIndex');
    print('  previousPendingEffectsIndices: $previousPendingEffectsIndices');
    print('  localToStateVar: $localToStateVar');
    print('  allocIndexToStateVar: $allocIndexToStateVar');
    print('  beginAddresses: $beginAddresses');
    print('  effects: $effects');
    print('  effectCounts: $effectCounts');
    print('  effectIndices: $effectIndices');
    print('  endAddresses: $endAddresses');
    print('  lastDescendants: $lastDescendants');
    print('  parents: $parents');
  }

  void popScope(int address) {
    var innerScopeStart = scopeStart;
    scopeStart = outerScopeStarts.removeLast();
    var scopeIndex = scopeIndices.removeLast();
    endAddresses[scopeIndex] = address;
    lastDescendants[scopeIndex] = lastDescendants.length - 1;
    effectIndices[scopeIndex] = effects.length;
    effectCounts[scopeIndex] = pendingEffects.length - innerScopeStart;
    for (var i = innerScopeStart; i < pendingEffects.length;) {
      var stateVar = pendingEffects[i];
      effects.add(stateVar);
      var previousPendingEffectsIndex = previousPendingEffectsIndices[i];
      if (previousPendingEffectsIndex >= scopeStart) {
        // This state variable was also affected by the outer scope, so remove
        // the duplicate.
        removePendingEffect(stateVar, i, previousPendingEffectsIndex);
        // And then carry on with index `i`, which now represents a different
        // state variable.
      } else {
        // This state variable effect wasn't duplicated, so move onto the next
        // one.
        i++;
      }
    }
  }

  void pushScope(int address) {
    var scope = beginAddresses.length;
    eventListener.onPushScope(address: address, scope: scope);
    var pendingEffectsLength = pendingEffects.length;
    outerScopeStarts.add(scopeStart);
    scopeStart = pendingEffectsLength;
    var outerScope = scopeIndices.last;
    scopeIndices.add(scope);
    beginAddresses.add(address);
    endAddresses.add(-1);
    effectIndices.add(-1);
    effectCounts.add(-1);
    lastDescendants.add(-1);
    parents.add(outerScope);
  }

  void removePendingEffect(StateVar stateVar, int pendingEffectsIndex,
      int previousPendingEffectsIndex) {
    assert(
        stateVarToPendingEffectsIndex[stateVar.index] == pendingEffectsIndex);
    assert(previousPendingEffectsIndices[pendingEffectsIndex] ==
        previousPendingEffectsIndex);
    // There may be additional entries in `_pendingEffects` after the entry to
    // be removed, so move the last entry in `_pendingEffects` to
    // `pendingEffectsIndex`. This is a no-op if the entry to be removed is
    // already the last entry, but we do it anyway to avoid a
    // difficult-to-predict branch.
    var stateVarToMove = pendingEffects.last;
    pendingEffects[pendingEffectsIndex] = stateVarToMove;
    stateVarToPendingEffectsIndex[stateVarToMove.index] = pendingEffectsIndex;
    previousPendingEffectsIndices[pendingEffectsIndex] =
        previousPendingEffectsIndices.last;
    // Drop the last entry in `_pendingEffects`.
    stateVarToPendingEffectsIndex[stateVar.index] = previousPendingEffectsIndex;
    pendingEffects.removeLast();
    previousPendingEffectsIndices.removeLast();
  }

  void run() {
    for (var address = 0; address < ir.endAddress; address++) {
      assert(checkState());
      if (enableDebugPrints) {
        print('$address: ${ir.instructionToString(address)}');
      }
      eventListener.onInstruction(address);
      switch (ir.opcodeAt(address)) {
        case Opcode.alloc:
          var count = Opcode.alloc.decodeCount(ir, address);
          for (var i = 0; i < count; i++) {
            var stateVar = createStateVar();
            eventListener.onAlloc(
                index: allocIndexToStateVar.length, stateVar: stateVar);
            localToStateVar.add(stateVar);
            allocIndexToStateVar.add(stateVar);
          }
        case Opcode.block:
        case Opcode.function:
        case Opcode.loop:
          pushScope(address);
        case Opcode.end:
          popScope(address);
        case Opcode.release:
          var count = Opcode.release.decodeCount(ir, address);
          for (var i = 0; i < count; i++) {
            var stateVar = localToStateVar.last;
            var pendingEffectsIndex =
                stateVarToPendingEffectsIndex[stateVar.index];
            if (pendingEffectsIndex >= 0) {
              // Local should have been allocated in this scope, so it can't
              // affect any outer scope, and therefore we can stop tracking it.
              const previousPendingEffectsIndex = -1;
              removePendingEffect(
                  stateVar, pendingEffectsIndex, previousPendingEffectsIndex);
            }
            localToStateVar.removeLast();
          }
        case Opcode.writeLocal:
          var local = Opcode.writeLocal.decodeLocalIndex(ir, address);
          stateVarAffected(localToStateVar[local]);
      }
    }
    assert(checkState());
    assert(scopeIndices.length == 1);
    assert(scopeStart == 0);
    assert(outerScopeStarts.isEmpty);
    assert(localToStateVar.isEmpty);
    eventListener.onFinished();
  }

  void stateVarAffected(StateVar stateVar) {
    var pendingEffectsIndex = stateVarToPendingEffectsIndex[stateVar.index];
    // Only record an entry in `_pendingEffects` if there isn't already an
    // entry indicating that the state variable was affected in the
    // current scope.
    if (stateVarToPendingEffectsIndex[stateVar.index] < scopeStart) {
      previousPendingEffectsIndices.add(pendingEffectsIndex);
      stateVarToPendingEffectsIndex[stateVar.index] = pendingEffects.length;
      pendingEffects.add(stateVar);
    }
  }
}
