// SPDX-License-Identifier: Apache-2.0

package chiselTests

import chisel3._
import chisel3.CompileOptions._
import chisel3.stage.ChiselStage

class CompileOptionsSpec extends ChiselFlatSpec with Utils {

  abstract class StrictModule extends Module()(chisel3.ExplicitCompileOptions.Strict)
  abstract class NotStrictModule extends Module()(chisel3.ExplicitCompileOptions.NotStrict)

  class SmallBundle extends Bundle {
    val f1 = UInt(4.W)
    val f2 = UInt(5.W)
  }
  class BigBundle extends SmallBundle {
    val f3 = UInt(6.W)
  }

  "A Module with missing bundle fields when compiled with implicit Strict.CompileOption " should "throw an exception" in {
    a[ChiselException] should be thrownBy extractCause[ChiselException] {
      import chisel3.ExplicitCompileOptions.Strict

      class ConnectFieldMismatchModule extends Module {
        val io = IO(new Bundle {
          val in = Input(new SmallBundle)
          val out = Output(new BigBundle)
        })
        io.out := io.in
      }
      ChiselStage.elaborate { new ConnectFieldMismatchModule() }
    }
  }

  "A Module with missing bundle fields when compiled with implicit NotStrict.CompileOption " should "not throw an exception" in {
    import chisel3.ExplicitCompileOptions.NotStrict

    class ConnectFieldMismatchModule extends Module {
      val io = IO(new Bundle {
        val in = Input(new SmallBundle)
        val out = Output(new BigBundle)
      })
      io.out := io.in
    }
    ChiselStage.elaborate { new ConnectFieldMismatchModule() }
  }

  "A Module in which a Reg is created with a bound type when compiled with implicit Strict.CompileOption " should "throw an exception" in {
    a[BindingException] should be thrownBy extractCause[BindingException] {
      import chisel3.ExplicitCompileOptions.Strict

      class CreateRegFromBoundTypeModule extends Module {
        val io = IO(new Bundle {
          val in = Input(new SmallBundle)
          val out = Output(new BigBundle)
        })
        val badReg = Reg(7.U(4.W))
      }
      ChiselStage.elaborate { new CreateRegFromBoundTypeModule() }
    }
  }

  "A Module in which a Reg is created with a bound type when compiled with implicit NotStrict.CompileOption " should "not throw an exception" in {
    import chisel3.ExplicitCompileOptions.NotStrict

    class CreateRegFromBoundTypeModule extends Module {
      val io = IO(new Bundle {
        val in = Input(new SmallBundle)
        val out = Output(new BigBundle)
      })
      val badReg = Reg(7.U(4.W))
    }
    ChiselStage.elaborate { new CreateRegFromBoundTypeModule() }
  }

  "A Module with wrapped IO when compiled with implicit Strict.CompileOption " should "not throw an exception" in {
    import chisel3.ExplicitCompileOptions.Strict

    class RequireIOWrapModule extends Module {
      val io = IO(new Bundle {
        val in = Input(UInt(32.W))
        val out = Output(Bool())
      })
      io.out := io.in(1)
    }
    ChiselStage.elaborate { new RequireIOWrapModule() }
  }

  "A Module with unwrapped IO when compiled with implicit Strict.CompileOption " should "throw an exception" in {
    a[BindingException] should be thrownBy extractCause[BindingException] {
      import chisel3.ExplicitCompileOptions.Strict

      class RequireIOWrapModule extends Module {
        val io = new Bundle {
          val in = Input(UInt(32.W))
          val out = Output(Bool())
        }
        io.out := io.in(1)
      }
      ChiselStage.elaborate {
        new RequireIOWrapModule()
      }
    }
  }

  "A Module connecting output as source to input as sink when compiled with implicit Strict.CompileOption " should "throw an exception" in {
    a[ChiselException] should be thrownBy extractCause[ChiselException] {
      import chisel3.ExplicitCompileOptions.Strict

      class SimpleModule extends Module {
        val io = IO(new Bundle {
          val in = Input(UInt(3.W))
          val out = Output(UInt(4.W))
        })
      }
      class SwappedConnectionModule extends SimpleModule {
        val child = Module(new SimpleModule)
        io.in := child.io.out
      }
      ChiselStage.elaborate { new SwappedConnectionModule() }
    }
  }

  "A Module connecting output as source to input as sink when compiled with implicit NotStrict.CompileOption " should "not throw an exception" in {
    import chisel3.ExplicitCompileOptions.NotStrict

    class SimpleModule extends Module {
      val io = IO(new Bundle {
        val in = Input(UInt(3.W))
        val out = Output(UInt(4.W))
      })
    }
    class SwappedConnectionModule extends SimpleModule {
      val child = Module(new SimpleModule)
      io.in := child.io.out
    }
    ChiselStage.elaborate { new SwappedConnectionModule() }
  }

  "A Module with directionless connections when compiled with implicit Strict.CompileOption " should "throw an exception" in {
    a[ChiselException] should be thrownBy extractCause[ChiselException] {
      // Verify we can suppress the inclusion of default compileOptions
      import Chisel.{defaultCompileOptions => _}
      import chisel3.ExplicitCompileOptions.Strict

      class SimpleModule extends Module {
        val io = IO(new Bundle {
          val in = Input(UInt(3.W))
          val out = Output(UInt(4.W))
        })
        val noDir = Wire(UInt(3.W))
      }

      class DirectionLessConnectionModule extends SimpleModule {
        val a = 0.U(3.W)
        val b = Wire(UInt(3.W))
        val child = Module(new SimpleModule)
        b := child.noDir
      }
      ChiselStage.elaborate { new DirectionLessConnectionModule() }
    }
  }

  "Strict.copy()" should "be equivalent in all CompileOptions traits" in {
    import chisel3.ExplicitCompileOptions.Strict
    val copiedCompileOptions = Strict.copy()
    assert(copiedCompileOptions.connectFieldsMustMatch == Strict.connectFieldsMustMatch)
    assert(copiedCompileOptions.declaredTypeMustBeUnbound == Strict.declaredTypeMustBeUnbound)
    assert(copiedCompileOptions.dontTryConnectionsSwapped == Strict.dontTryConnectionsSwapped)
    assert(copiedCompileOptions.dontAssumeDirectionality == Strict.dontAssumeDirectionality)
    assert(copiedCompileOptions.checkSynthesizable == Strict.checkSynthesizable)
    assert(copiedCompileOptions.explicitInvalidate == Strict.explicitInvalidate)
    assert(copiedCompileOptions.inferModuleReset == Strict.inferModuleReset)
    assert(copiedCompileOptions.migrateInferModuleReset == Strict.migrateInferModuleReset)
    assert(copiedCompileOptions.emitStrictConnects == Strict.emitStrictConnects)
  }

  "NotStrict.copy()" should "be equivalent in all CompileOptions traits" in {
    import chisel3.ExplicitCompileOptions.NotStrict
    val copiedCompileOptions = NotStrict.copy()
    assert(copiedCompileOptions.connectFieldsMustMatch == NotStrict.connectFieldsMustMatch)
    assert(copiedCompileOptions.declaredTypeMustBeUnbound == NotStrict.declaredTypeMustBeUnbound)
    assert(copiedCompileOptions.dontTryConnectionsSwapped == NotStrict.dontTryConnectionsSwapped)
    assert(copiedCompileOptions.dontAssumeDirectionality == NotStrict.dontAssumeDirectionality)
    assert(copiedCompileOptions.checkSynthesizable == NotStrict.checkSynthesizable)
    assert(copiedCompileOptions.explicitInvalidate == NotStrict.explicitInvalidate)
    assert(copiedCompileOptions.inferModuleReset == NotStrict.inferModuleReset)
    assert(copiedCompileOptions.migrateInferModuleReset == NotStrict.migrateInferModuleReset)
    assert(copiedCompileOptions.emitStrictConnects == NotStrict.emitStrictConnects)
  }

}
