All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.arainko.ducktape.internal.Debug.scala Maven / Gradle / Ivy

There is a newer version: 0.2.5
Show newest version
package io.github.arainko.ducktape.internal

import scala.collection.immutable.VectorMap
import scala.compiletime.*
import scala.deriving.Mirror
import scala.quoted.*
import scala.reflect.ClassTag

private[ducktape] trait Debug[-A] {
  def astify(self: A)(using Quotes): Debug.AST

  extension (self: A) final def show(using Quotes): String = astify(self).dropEmpty.show
}

private[ducktape] object Debug extends LowPriorityDebug {
  import AST.*

  val nonShowable: Debug[Any] = new:
    def astify(self: Any)(using Quotes): AST = Empty

  def show[A](value: A)(using Debug[A], Quotes) = value.show

  given string: Debug[String] with {
    override def astify(self: String)(using Quotes): AST = Text(s""""${self}"""")
  }

  given int: Debug[Int] with {
    override def astify(self: Int)(using Quotes): AST = Text(self.toString)
  }

  given bool: Debug[Boolean] with {
    override def astify(self: Boolean)(using Quotes): AST = Text(self.toString)
  }

  given wildcardTpe: Debug[Type[?]] with {
    override def astify(self: Type[?])(using Quotes): AST = {
      import quotes.reflect.*
      Text(s"Type.of[${Printer.TypeReprShortCode.show(TypeRepr.of(using self))}]")
    }
  }

  given tpe[A]: Debug[Type[A]] with {
    def astify(self: Type[A])(using Quotes): AST = {
      import quotes.reflect.*
      Text(s"Type.of[${Printer.TypeReprShortCode.show(TypeRepr.of(using self))}]")
    }
  }

  given collection[A, Coll[a] <: Iterable[a]](using debug: Debug[A], tag: ClassTag[Coll[A]]): Debug[Coll[A]] with {
    def astify(self: Coll[A])(using Quotes): AST = {
      val name = tag.runtimeClass.getSimpleName()
      Collection(name, self.map(debug.astify).toVector)
    }
  }

  given map[K, V](using debugKey: Debug[K], debugValue: Debug[V]): Debug[Map[K, V]] with {

    def astify(self: Map[K, V])(using Quotes): AST = {
      Collection(
        "Map",
        self
          .map((key, value) => Product("Entry", VectorMap("key" -> debugKey.astify(key), "value" -> debugValue.astify(value))))
          .toVector
      )
    }
  }

  given option[A](using debug: Debug[A]): Debug[Option[A]] with {
    def astify(self: Option[A])(using Quotes): AST =
      self match
        case None        => Text("None")
        case Some(value) => Collection("Some", Vector(debug.astify(value)))
  }

  given term(using q: Quotes): Debug[q.reflect.Term] = new {

    def astify(self: q.reflect.Term)(using Quotes): AST = {
      import q.reflect.*
      Text {
        s"""
          |  Structure: ${Printer.TreeStructure.show(self)}
          |  Code: ${Printer.TreeShortCode.show(self)}""".stripMargin
      }
    }

  }

  given deferred: Debug[() => Any] = Debug.nonShowable

  given expr[A]: Debug[Expr[A]] with {

    def astify(self: Expr[A])(using Quotes): AST = {
      import quotes.reflect.*
      Text(s"Expr[${self.asTerm.tpe.show(using Printer.TypeReprShortCode)}]")
    }
  }

  inline def derived[A](using A: Mirror.Of[A]): Debug[A] =
    inline A match {
      case given Mirror.ProductOf[A] => product
      case given Mirror.SumOf[A]     => coproduct
    }

  private[ducktape] class ForProduct[A](tpeName: String, _instances: => IArray[Debug[Any]]) extends Debug[A] {
    private lazy val instances = _instances
    def astify(self: A)(using Quotes): AST = {
      val prod = self.asInstanceOf[scala.Product]
      val fields = prod.productElementNames
        .zip(instances)
        .zip(prod.productIterator)
        .map {
          case label -> debug -> value =>
            label -> debug.astify(value)
        }
        .to(VectorMap)

      Product(tpeName, fields)
    }
  }

  private inline def product[A](using A: Mirror.ProductOf[A]): Debug[A] = {
    val tpeName = constValue[A.MirroredLabel].toString
    def instances = summonAll[Tuple.Map[A.MirroredElemTypes, Debug]].toIArray.map(_.asInstanceOf[Debug[Any]])
    ForProduct(tpeName, instances)
  }

  private[ducktape] class ForCoproduct[A](instances: Vector[Debug[Any]])(using A: Mirror.SumOf[A]) extends Debug[A] {
    def astify(self: A)(using Quotes): AST = {
      val ordinal = A.ordinal(self)
      instances(ordinal).astify(self)
    }
  }

  private inline def coproduct[A](using A: Mirror.SumOf[A]): Debug[A] = ForCoproduct(deriveForAll[A.MirroredElemTypes].toVector)

  private inline def deriveForAll[Tup <: Tuple]: List[Debug[Any]] =
    inline erasedValue[Tup] match {
      case _: (head *: tail) =>
        // TODO: Doesn't take into account existing instances, getting stack overflows when trying to do that for some reason
        derived[head](using summonInline[Mirror.Of[head]]).asInstanceOf[Debug[Any]] :: deriveForAll[tail]
      case _: EmptyTuple => Nil
    }

  enum AST {
    case Empty
    case Text(value: String)
    case Product(name: String, fields: VectorMap[String, AST])
    case Collection(name: String, values: Vector[AST])

    final def dropEmpty: AST =
      this match
        case Empty                 => Empty
        case t @ Text(_)           => t
        case Product(name, fields) => Product(name, fields.collect { case (name, ast) if ast != Empty => name -> ast.dropEmpty })
        case Collection(name, values) => Collection(name, values.collect { case ast if ast != Empty => ast.dropEmpty })

    final def length: Int =
      this match
        case Empty                    => 0
        case Text(value)              => value.length
        case Product(name, fields)    => name.length + fields.map((name, ast) => name.length + ast.length).sum
        case Collection(name, values) => name.length + values.map(_.length).sum

    final def show: String = {
      def ident(n: Int) = "  " * n

      // if you think this is over then you're wrong
      val Separator = System.lineSeparator

      def recurse(ast: AST, depth: Int): String = {
        ast match
          case Empty       => ""
          case Text(value) => value
          case p @ Product(name, fields) =>
            if p.length >= 80 then {
              s"$name(".bold + Separator +
                fields.map { (name, ast) =>
                  ident(depth + 1) + name.yellow + " = ".yellow + recurse(ast, depth + 1)
                }.mkString("," + Separator) + Separator + ident(depth) + ")".bold
            } else {
              name.bold + "(".bold + fields
                .map((name, ast) => name.yellow + " = ".yellow + recurse(ast, 0))
                .mkString(", ") + ")".bold
            }

          case c @ Collection(name, values) =>
            if c.length >= 80 then {
              s"$name(".bold + Separator +
                values.map(value => ident(depth + 1) + recurse(value, depth + 1)).mkString("," + Separator) + Separator + ident(
                  depth
                ) + ")".bold
            } else {
              s"$name(".bold + values.map(recurse(_, 0)).mkString(", ") + ")".bold
            }
      }
      recurse(this, 0)
    }

    extension (self: String) {
      private def bold: String = s"${Console.BOLD}$self${Console.RESET}"
      private def yellow: String = s"${Console.YELLOW}$self${Console.RESET}"
    }
  }
}

private[ducktape] transparent trait LowPriorityDebug {
  given Debug[Nothing => Any] = Debug.nonShowable
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy