// 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';

/// Checks that [ir] is well-formed.
///
/// Throws [ValidationError] if it's not.
///
/// During validation, progress information will be reported to [eventListener]
/// (if provided).
void validate(BaseIRContainer ir, {ValidationEventListener? eventListener}) {
  eventListener ??= ValidationEventListener();
  var validator = _Validator(ir, eventListener: eventListener);
  eventListener._validator = validator;
  validator.run();
  eventListener._validator = null;
}

class ValidationError extends Error {
  final int address;
  final String instructionString;
  final String message;

  ValidationError({
    required this.address,
    required this.instructionString,
    required this.message,
  });

  @override
  String toString() =>
      'Validation error at $address ($instructionString): $message';
}

/// Event listener used by [validate] to report progress information.
///
/// By itself this class does nothing; the caller of [validate] should make a
/// derived class that overrides one or more of the `on...` methods.
base class ValidationEventListener {
  late _Validator? _validator;

  int get localCount => _validator!.localCount;

  ValueCount get valueStackDepth => _validator!.valueStackDepth;

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

  /// Called prior to visiting each instruction.
  void onInstruction(int address) {}
}

/// A count of value entries.
///
/// The count may be [indeterminate], indicating that the current control flow
/// state is unreachable, and so exact matching of value counts is unnecessary.
///
// TODO(paulberry): when extension types are supported, make this an extension
// type.
class ValueCount {
  static const indeterminate = ValueCount._(-1);

  final int _depth;

  ValueCount(this._depth) : assert(_depth >= 0);

  const ValueCount._(this._depth);

  @override
  int get hashCode => _depth.hashCode;

  ValueCount operator +(int delta) {
    assert(delta >= 0);
    if (this == indeterminate) return indeterminate;
    return ValueCount(_depth + delta);
  }

  ValueCount operator -(int delta) {
    assert(delta >= 0);
    if (this == indeterminate) return indeterminate;
    return ValueCount(_depth - delta);
  }

  @override
  bool operator ==(other) => other is ValueCount && _depth == other._depth;

  bool indeterminateOrAtLeast(int other) =>
      this == indeterminate || _depth >= other;

  bool indeterminateOrEqualTo(int other) =>
      this == indeterminate || _depth == other;

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

/// Used by [_Validator] to track a control flow instruction (`block`, `loop`,
/// `tryCatch`, `tryFinally`, or `function` instruction) whose `matching `end`
/// instruction has not yet been encountered.
class _ControlFlowElement {
  /// The state of [_Validator.localCount] before the control flow instruction
  /// was encountered.
  final int localCountBefore;

  /// The state of [_Validator.functionFlags] before the control flow
  /// instruction was encountered.
  final FunctionFlags functionFlagsBefore;

  /// The number of entries that will be in the value stack after the matching
  /// `end` instruction.
  final ValueCount valueStackDepthAfter;

  /// The number of values that will be consumed from the value stack when the
  /// control flow construct is ended (or branched out of).
  final int branchValueCount;

  /// Whether the control flow instruction was a `function` instruction.
  final bool isFunction;

  _ControlFlowElement({
    required this.localCountBefore,
    required this.functionFlagsBefore,
    required this.valueStackDepthAfter,
    required this.branchValueCount,
    this.isFunction = false,
  });
}

class _Validator {
  final BaseIRContainer ir;
  final ValidationEventListener eventListener;
  final controlFlowStack = <_ControlFlowElement>[];
  var address = 0;

  /// Flags from the most recent `function` instruction whose corresponding
  /// `end` instruction has not yet been encountered.
  var functionFlags = FunctionFlags();

  var localCount = 0;
  var valueStackDepth = ValueCount(0);

  _Validator(this.ir, {required this.eventListener});

  /// Validates a `br` or `brIf` instruction.
  void branch(int nesting, {required bool conditional}) {
    if (conditional) popValues(1);
    check(nesting >= 0, 'Negative branch nesting');
    var target = controlFlowStack.length - 1 - nesting;
    check(target >= 0, 'Control flow stack underflow');
    for (var i = target + 1; i < controlFlowStack.length; i++) {
      check(
        !controlFlowStack[i].isFunction,
        'Cannot branch outside of enclosing function',
      );
    }
    var branchValueCount = controlFlowStack[target].branchValueCount;
    popValues(branchValueCount);
    if (conditional) {
      pushValues(branchValueCount);
    } else {
      valueStackDepth = ValueCount.indeterminate;
    }
  }

  /// Reports a validation error if [condition] is `false`.
  void check(bool condition, String message) {
    if (!condition) {
      fail(message);
    }
  }

  /// Unconditionally reports a validation error.
  Never fail(String message) {
    throw ValidationError(
      address: address,
      instructionString: address < ir.endAddress
          ? ir.instructionToString(address)
          : 'after last instruction',
      message: message,
    );
  }

  void popValues(int count) {
    check(
      valueStackDepth.indeterminateOrAtLeast(count),
      'Value stack underflow',
    );
    valueStackDepth -= count;
  }

  void pushValues(int count) {
    valueStackDepth += count;
  }

  void run() {
    check(ir.endAddress > 0, 'No instructions');
    for (address = 0; address < ir.endAddress; address++) {
      eventListener.onInstruction(address);
      var opcode = ir.opcodeAt(address);
      check(
        address != 0 || opcode == Opcode.function,
        'First instruction must be function',
      );
      switch (opcode) {
        case Opcode.alloc:
          var count = Opcode.alloc.decodeCount(ir, address);
          check(count >= 0, 'Negative alloc count');
          localCount += count;
        case Opcode.await_:
          check(functionFlags.isAsync, 'Await in synchronous function');
          popValues(1);
          pushValues(1);
        case Opcode.block:
          var inputCount = Opcode.block.decodeInputCount(ir, address);
          var outputCount = Opcode.block.decodeOutputCount(ir, address);
          check(inputCount >= 0, 'Negative input count');
          check(outputCount >= 0, 'Negative output count');
          popValues(inputCount);
          controlFlowStack.add(
            _ControlFlowElement(
              localCountBefore: localCount,
              functionFlagsBefore: functionFlags,
              valueStackDepthAfter: valueStackDepth + outputCount,
              branchValueCount: outputCount,
            ),
          );
          valueStackDepth = ValueCount(inputCount);
        case Opcode.br:
          var nesting = Opcode.br.decodeNesting(ir, address);
          branch(nesting, conditional: false);
        case Opcode.brIf:
          var nesting = Opcode.brIf.decodeNesting(ir, address);
          branch(nesting, conditional: true);
        case Opcode.call:
          var argumentNames = Opcode.call.decodeArgumentNames(ir, address);
          popValues(ir.decodeArgumentNames(argumentNames).length);
          pushValues(1);
        case Opcode.concat:
          var count = Opcode.concat.decodeCount(ir, address);
          check(count >= 0, 'Negative concat count');
          popValues(count);
          pushValues(1);
        case Opcode.drop:
          popValues(1);
        case Opcode.dup:
          popValues(1);
          pushValues(2);
        case Opcode.end:
          check(controlFlowStack.isNotEmpty, 'Unmatched end');
          var controlFlowElement = controlFlowStack.removeLast();
          check(
            localCount == controlFlowElement.localCountBefore,
            'Unreleased locals',
          );
          popValues(controlFlowElement.branchValueCount);
          check(
            valueStackDepth.indeterminateOrEqualTo(0),
            '${valueStackDepth._depth} superfluous value(s) remaining',
          );
          valueStackDepth = controlFlowElement.valueStackDepthAfter;
          functionFlags = controlFlowElement.functionFlagsBefore;
        case Opcode.eq:
          popValues(2);
          pushValues(1);
        case Opcode.function:
          var type = Opcode.function.decodeType(ir, address);
          var kind = Opcode.function.decodeFlags(ir, address);
          check(
            !kind.isInstance || address == 0,
            'Instance function may only be used at instruction address 0',
          );
          controlFlowStack.add(
            _ControlFlowElement(
              localCountBefore: localCount,
              functionFlagsBefore: functionFlags,
              valueStackDepthAfter: valueStackDepth + 1,
              branchValueCount: 1,
              isFunction: true,
            ),
          );
          functionFlags = kind;
          valueStackDepth = ValueCount(
            ir.countParameters(type) + (kind.isInstance ? 1 : 0),
          );
        case Opcode.identical:
          popValues(2);
          pushValues(1);
        case Opcode.is_:
          popValues(1);
          pushValues(1);
        case Opcode.literal:
          pushValues(1);
        case Opcode.loop:
          var inputCount = Opcode.loop.decodeInputCount(ir, address);
          check(inputCount >= 0, 'Negative input count');
          popValues(inputCount);
          controlFlowStack.add(
            _ControlFlowElement(
              localCountBefore: localCount,
              functionFlagsBefore: functionFlags,
              valueStackDepthAfter: ValueCount.indeterminate,
              branchValueCount: inputCount,
            ),
          );
          valueStackDepth = ValueCount(0);
          pushValues(inputCount);
        case Opcode.not:
          popValues(1);
          pushValues(1);
        case Opcode.readLocal:
          var localIndex = Opcode.readLocal.decodeLocalIndex(ir, address);
          check(localIndex >= 0, 'Negative local index');
          check(localIndex < localCount, 'No such local');
          pushValues(1);
        case Opcode.release:
          var count = Opcode.release.decodeCount(ir, address);
          check(count >= 0, 'Negative release count');
          var localCountFence =
              controlFlowStack.lastOrNull?.localCountBefore ?? 0;
          var newLocalCount = localCount - count;
          check(
            newLocalCount >= localCountFence,
            'Local variable stack underflow',
          );
          localCount = newLocalCount;
        case Opcode.shuffle:
          var popCount = Opcode.shuffle.decodePopCount(ir, address);
          var stackIndices = ir.decodeStackIndices(
            Opcode.shuffle.decodeStackIndices(ir, address),
          );
          check(popCount >= 0, 'Negative pop count');
          for (var stackIndex in stackIndices) {
            check(stackIndex >= 0, 'Negative stack index');
            check(stackIndex < popCount, 'Stack index too large');
          }
          popValues(popCount);
          pushValues(stackIndices.length);
        case Opcode.writeLocal:
          var localIndex = Opcode.writeLocal.decodeLocalIndex(ir, address);
          check(localIndex >= 0, 'Negative local index');
          check(localIndex < localCount, 'No such local');
          popValues(1);
        case Opcode.yield_:
          check(functionFlags.isGenerator, 'Yield in non-generator function');
          popValues(1);
        default:
          fail('Unexpected opcode ${opcode.describe()}');
      }
    }
    eventListener.onFinished();
    check(controlFlowStack.isEmpty, 'Missing end');
  }
}
