Repository: 7mind/izumi
Pull request #712 (2019-11-04 00:06:07)

Enable caching for runtime LightTypeTag creation by default
Add class name to macro logs


Patch:
diff --git a/build.sbt b/build.sbt
index 7208ec1d3..7d12b9e9c 100644
--- a/build.sbt
+++ b/build.sbt
@@ -4029,7 +4029,7 @@ lazy val `microsite` = project.in(file("doc/microsite"))
     siteSubdirName in ScalaUnidoc := s"${DocKeys.prefix.value}/api",
     siteSubdirName in Paradox := s"${DocKeys.prefix.value}/doc",
     paradoxProperties ++= Map(
-                "scaladoc.izumi.base_url" -> s"/${DocKeys.prefix.value}/api/izumi/",
+                "scaladoc.izumi.base_url" -> s"/${DocKeys.prefix.value}/api/",
                 "scaladoc.base_url" -> s"/${DocKeys.prefix.value}/api/",
                 "izumi.version" -> version.value,
               ),
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/definition/Binding.scala b/distage/distage-model/src/main/scala/izumi/distage/model/definition/Binding.scala
index 7dedfd0b7..afb050b0e 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/definition/Binding.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/definition/Binding.scala
@@ -8,46 +8,42 @@ import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
 import izumi.fundamentals.platform.language.SourceFilePosition
 
 sealed trait Binding {
-
   def key: DIKey
-
   def origin: SourceFilePosition
+  def tags: Set[BindingTag]
 
   def group: GroupingKey
-  def tags: Set[BindingTag]
 
   def addTags(tags: Set[BindingTag]): Binding
+  protected[this] def withTags(tags: Set[BindingTag]): Binding
 
-
-  override def hashCode(): Int = group.hashCode()
-
-  override def equals(obj: Any): Boolean = obj match {
+  override final def hashCode(): Int = group.hashCode()
+  override final def equals(obj: Any): Boolean = obj match {
     case b: Binding =>
       b.group == this.group && b.tags == this.tags
     case _ =>
       false
   }
-
-  protected[distage] def withTags(tags: Set[BindingTag]): Binding
 }
 
 object Binding {
+
   sealed trait GroupingKey {
     def key: DIKey
   }
   object GroupingKey {
-    case class KeyImpl(key: DIKey, impl: ImplDef) extends GroupingKey
-    case class Key(key: DIKey) extends GroupingKey
+    final case class KeyImpl(key: DIKey, impl: ImplDef) extends GroupingKey
+    final case class Key(key: DIKey) extends GroupingKey
   }
 
-  sealed trait WithTarget {
+  private[Binding] sealed trait WithTarget {
     def withTarget[K <: DIKey](key: K): Binding
   }
   sealed trait ImplBinding extends Binding {
     def implementation: ImplDef
 
     def withImplDef(implDef: ImplDef): ImplBinding
-    protected[distage] def withTags(tags: Set[BindingTag]): ImplBinding
+    protected[this] def withTags(tags: Set[BindingTag]): ImplBinding
     override def addTags(tags: Set[BindingTag]): ImplBinding
   }
 
@@ -58,7 +54,7 @@ object Binding {
 
     override def withImplDef(implDef: ImplDef): SingletonBinding[K] = copy(implementation = implDef)
     override def withTarget[T <: RuntimeDIUniverse.DIKey](key: T): SingletonBinding[T] = copy(key = key)
-    protected[distage] def withTags(newTags: Set[BindingTag]): SingletonBinding[K] = copy(tags = newTags)
+    protected[this] def withTags(newTags: Set[BindingTag]): SingletonBinding[K] = copy(tags = newTags)
     override def addTags(moreTags: Set[BindingTag]): SingletonBinding[K] = withTags(this.tags ++ moreTags)
   }
 
@@ -90,14 +86,14 @@ object Binding {
       GroupingKey.KeyImpl(gk, implementation)
     }
     override def withImplDef(implDef: ImplDef): SetElementBinding = copy(implementation = implDef)
-    protected[distage] def withTags(newTags: Set[BindingTag]): SetElementBinding = copy(tags = newTags)
+    protected[this] def withTags(newTags: Set[BindingTag]): SetElementBinding = copy(tags = newTags)
     override def addTags(moreTags: Set[BindingTag]): SetElementBinding = withTags(this.tags ++ moreTags)
   }
 
   final case class EmptySetBinding[+K <: DIKey](key: K, tags: Set[BindingTag], origin: SourceFilePosition) extends SetBinding with WithTarget {
     override def group: GroupingKey = GroupingKey.Key(key)
     override def withTarget[T <: RuntimeDIUniverse.DIKey](key: T): EmptySetBinding[T] = copy(key = key)
-    protected[distage] def withTags(newTags: Set[BindingTag]): EmptySetBinding[K] = copy(tags = newTags)
+    protected[this] def withTags(newTags: Set[BindingTag]): EmptySetBinding[K] = copy(tags = newTags)
     override def addTags(moreTags: Set[BindingTag]): EmptySetBinding[K] = withTags(this.tags ++ moreTags)
   }
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala b/distage/distage-model/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala
index a933e2737..13a03a3a8 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/definition/dsl/AbstractBindingDefDSL.scala
@@ -134,10 +134,10 @@ object AbstractBindingDefDSL {
 
   final class SetRef
   (
-    initial: EmptySetBinding[DIKey.TypeKey]
-    , setOps: mutable.Queue[SetInstruction] = mutable.Queue.empty
-    , elems: mutable.Queue[SetElementRef] = mutable.Queue.empty
-    , multiElems: mutable.Queue[MultiSetElementRef] = mutable.Queue.empty
+    initial: EmptySetBinding[DIKey.TypeKey],
+    setOps: mutable.Queue[SetInstruction] = mutable.Queue.empty,
+    elems: mutable.Queue[SetElementRef] = mutable.Queue.empty,
+    multiElems: mutable.Queue[MultiSetElementRef] = mutable.Queue.empty,
   ) extends BindingRef {
     override def interpret: collection.Seq[Binding] = {
       val emptySetBinding = setOps.foldLeft(initial: EmptySetBinding[DIKey.BasicKey]) {
@@ -186,8 +186,6 @@ object AbstractBindingDefDSL {
       }
     }
 
-
-
     def append(op: SetElementInstruction): SetElementRef = {
       ops += op
       this
@@ -210,7 +208,6 @@ object AbstractBindingDefDSL {
           }
       }
 
-
       Seq(bind, refBind)
     }
 
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala b/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala
index 03ab622c6..57c43b329 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/references/WithDIKey.scala
@@ -12,11 +12,7 @@ trait WithDIKey {
     def tpe: SafeType
   }
 
-
-
   object DIKey {
-    // in order to make idea links working we need to put a dot before Position occurence and avoid using #
-
     def get[K: Tag]: TypeKey = TypeKey(SafeType.get[K])
 
     sealed trait BasicKey extends DIKey {
diff --git a/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/console/TrivialLogger.scala b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/console/TrivialLogger.scala
index 1b604353e..c8a95dba1 100644
--- a/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/console/TrivialLogger.scala
+++ b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/console/TrivialLogger.scala
@@ -1,44 +1,36 @@
 package izumi.fundamentals.platform.console
 
 import izumi.fundamentals.platform.console.TrivialLogger.{Config, Level}
-import izumi.fundamentals.platform.strings.IzString._
-
-import scala.reflect.ClassTag
 import izumi.fundamentals.platform.exceptions.IzThrowable._
+import izumi.fundamentals.platform.strings.IzString._
 
 import scala.collection.mutable
+import scala.reflect.{ClassTag, classTag}
 
 trait TrivialLogger {
   def log(s: => String): Unit
-
   def log(s: => String, e: => Throwable): Unit
 
   def err(s: => String): Unit
-
   def err(s: => String, e: => Throwable): Unit
 
   def sub(): TrivialLogger = sub(1)
-
   def sub(delta: Int): TrivialLogger
 }
 
 trait AbstractStringTrivialSink {
   def flush(value: => String): Unit
-
   def flushError(value: => String): Unit
 }
 
 object AbstractStringTrivialSink {
-
   object Console extends AbstractStringTrivialSink {
     override def flush(value: => String): Unit = System.out.println(value)
-
     override def flushError(value: => String): Unit = System.err.println(value)
   }
-
 }
 
-class TrivialLoggerImpl(config: Config, id: String, logMessages: Boolean, logErrors: Boolean, loggerLevel: Int) extends TrivialLogger {
+final class TrivialLoggerImpl(config: Config, id: String, logMessages: Boolean, logErrors: Boolean, loggerLevel: Int) extends TrivialLogger {
   override def log(s: => String): Unit = {
     flush(Level.Info, format(s))
   }
@@ -59,13 +51,12 @@ class TrivialLoggerImpl(config: Config, id: String, logMessages: Boolean, logErr
     new TrivialLoggerImpl(config, id, logMessages, logErrors, loggerLevel + delta)
   }
 
-
   @inline private[this] def format(s: => String): String = {
-    s"$s"
+    s"$id: $s"
   }
 
-  @inline private[this] def formatError(s: => String, e: => Throwable) = {
-    s"$s\n${e.stackTrace}"
+  @inline private[this] def formatError(s: => String, e: => Throwable): String = {
+    s"$id: $s\n${e.stackTrace}"
   }
 
   @inline private[this] def flush(level: Level, s: => String): Unit = {
@@ -83,38 +74,33 @@ class TrivialLoggerImpl(config: Config, id: String, logMessages: Boolean, logErr
 }
 
 object TrivialLogger {
-
   sealed trait Level
-
   object Level {
-
     case object Info extends Level
-
     case object Error extends Level
-
   }
 
-  case class Config(
-                     sink: AbstractStringTrivialSink = AbstractStringTrivialSink.Console,
-                     forceLog: Boolean = false
-                   )
+  final case class Config(
+                           sink: AbstractStringTrivialSink = AbstractStringTrivialSink.Console,
+                           forceLog: Boolean = false,
+                         )
 
-  def make[T: ClassTag](id: String, config: Config = Config()): TrivialLogger = {
-    val logMessages: Boolean = checkLog(id, config, default = false)
-    val logErrors: Boolean = checkLog(id, config, default = true)
-    new TrivialLoggerImpl(config, id, logMessages, logErrors, 0)
+  def make[T: ClassTag](sysProperty: String, config: Config = Config()): TrivialLogger = {
+    val logMessages: Boolean = checkLog(sysProperty, config, default = false)
+    val logErrors: Boolean = checkLog(sysProperty, config, default = true)
+    new TrivialLoggerImpl(config, classTag[T].runtimeClass.getSimpleName, logMessages, logErrors, loggerLevel = 0)
   }
 
-  private val enabled = new mutable.HashMap[String, Boolean]()
+  private[this] val enabled = new mutable.HashMap[String, Boolean]()
 
-  private def checkLog(id: String, config: Config, default: Boolean): Boolean = enabled.synchronized {
-    config.forceLog || enabled.getOrElseUpdate(id, {
-      val parts = id.split('.')
+  private[this] def checkLog(sysProperty: String, config: Config, default: Boolean): Boolean = enabled.synchronized {
+    config.forceLog || enabled.getOrElseUpdate(sysProperty, {
+      val parts = sysProperty.split('.')
       val candidates = parts.tail.scanLeft(parts.head) {
         case (acc, chunk) =>
           acc + "." + chunk
       }
-      candidates.exists(c => System.getProperty(c).asBoolean().getOrElse(default))
+      candidates.exists(System.getProperty(_).asBoolean().getOrElse(default))
     })
   }
 }
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/DebugProperties.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/DebugProperties.scala
index f866bf79f..70cc4bcc1 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/DebugProperties.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/DebugProperties.scala
@@ -3,11 +3,25 @@ package izumi.fundamentals.reflection
 import izumi.fundamentals.platform
 
 /**
-  * To see macro debug output during compilation, set `-Dizumi.debug.macro.rtti=true` java property! e.g.
+  * To see macro debug output during compilation, set `-Dizumi.debug.macro.rtti=true` system property! e.g.
+  *
   * {{{
   * sbt -Dizumi.debug.macro.rtti=true compile
   * }}}
+  *
+  * Add compiler option `-Xmacro-settings:izumi.rtti.cache.compile` to enable compile-time caching of computed
+  * LightTypeTags. The option is disabled by default since there appears to be little noticeable compile-time
+  * performance gain from caching, but results may vary.
+  *
+  * {{{
+  * scalacOptions += "-Xmacro-settings:izumi.rtti.cache.compile"
+  * }}}
+  *
+  * Set system property `-Dizumi.rtti.cache.runtime=false` to disable caching for runtime creation of LightTypeTags.
+  * Caching is enabled by default for runtime light type tag creation.
   */
 object DebugProperties extends platform.logging.DebugProperties {
   final val `izumi.debug.macro.rtti` = "izumi.debug.macro.rtti"
+  final val `izumi.rtti.cache.compile` = "izumi.rtti.cache.compile"
+  final val `izumi.rtti.cache.runtime` = "izumi.rtti.cache.runtime"
 }
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/ScalacSink.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/ScalacSink.scala
index f55f775f5..c6d1cfb8d 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/ScalacSink.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/ScalacSink.scala
@@ -4,7 +4,7 @@ import izumi.fundamentals.platform.console.AbstractStringTrivialSink
 
 import scala.reflect.macros.blackbox
 
-class ScalacSink(c: blackbox.Context) extends AbstractStringTrivialSink {
+final class ScalacSink(c: blackbox.Context) extends AbstractStringTrivialSink {
   override def flush(value: => String): Unit = {
     c.info(c.enclosingPosition, value, force = true)
   }
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TagMacro.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TagMacro.scala
index e8494bc07..e4a18a0a6 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TagMacro.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TagMacro.scala
@@ -3,7 +3,7 @@ package izumi.fundamentals.reflection
 import izumi.fundamentals.platform.console.TrivialLogger
 import izumi.fundamentals.reflection.ReflectionUtil.{Kind, kindOf}
 import izumi.fundamentals.reflection.Tags.{defaultTagImplicitError, hktagFormat, hktagFormatMap}
-import izumi.fundamentals.reflection.macrortti.{LTag, LightTypeTagMacro, LightTypeTagMacro0}
+import izumi.fundamentals.reflection.macrortti.LightTypeTagMacro0
 
 import scala.annotation.{implicitNotFound, tailrec}
 import scala.collection.immutable.ListMap
@@ -16,8 +16,6 @@ class TagMacro(val c: blackbox.Context) {
 
   import c.universe._
 
-  protected[this] val defaultError: String = defaultTagImplicitError
-
   protected[this] val logger: TrivialLogger = TrivialMacroLogger.make[this.type](c, DebugProperties.`izumi.debug.macro.rtti`)
 
   /**
@@ -37,12 +35,12 @@ class TagMacro(val c: blackbox.Context) {
 
     val newHkTag = mkHKTagArg(typeConstructor, kindOf(typeConstructor))
 
-    val res = summonTypeTag[DIU](newHkTag).asInstanceOf[c.Expr[DIU#ScalaReflectTypeTag[T]]]
+    val res = summonTypeTag[DIU, T](newHkTag)
 
-    logger.log(s"resulting implicit summon $res")
+    logger.log(s"resulting ArgStruct TypeTag $res")
 
-    val tagMacro = new LightTypeTagMacro(c)
-    val lhktag = tagMacro.makeHKTagRaw[T](newHkTag.asInstanceOf[tagMacro.c.Type]).asInstanceOf[c.Expr[LTag.WeakHK[T]]]
+    val ltagMacro = new LightTypeTagMacro0[c.type](c)
+    val lhktag = ltagMacro.makeHKTagRaw[T](newHkTag)
 
     logger.log(s"resulting LHKTag $lhktag")
 
@@ -51,19 +49,12 @@ class TagMacro(val c: blackbox.Context) {
     }
   }
 
-  // FIXME: report scalac bug - `Nothing` type is lost when two implicits for it are summoned as in
-  //    implicit final def tagFromTypeTag[T](implicit t: TypeTag[T], l: LTag[T]): Tag[T] = Tag(t, l.fullLightTypeTag)
-  //  [info] /Users/kai/src/izumi-r2/distage/distage-core/src/test/scala/izumi/distage/impl/TagTest.scala:55:17: universe.this.RuntimeDIUniverse.Tag.tagFromTypeTag is not a valid implicit value for izumi.distage.model.reflection.universe.RuntimeDIUniverse.Tag[Nothing] because:
-  //  [info] hasMatchingSymbol reported error: type mismatch;
-  //  [info]  found   : izumi.fundamentals.reflection.macrortti.LTag.Weak[Nothing]
-  //  [info]  required: izumi.fundamentals.reflection.macrortti.LWeakTag[T]
-  //  [info]     (which expands to)  izumi.fundamentals.reflection.macrortti.LTag.Weak[T]
-  //  [info] Note: Nothing <: T, but class Weak is invariant in type T.
-  //  [info] You may wish to define T as +T instead. (SLS 4.5)
-  //  [info]       assert(Tag[Nothing].tpe == safe[Nothing])
+  // workaround for a scalac bug - `Nothing` type is lost when two implicits for it are summoned from one implicit as in:
+  //  implicit final def tagFromTypeTag[T](implicit t: TypeTag[T], l: LTag[T]): Tag[T] = Tag(t, l.fullLightTypeTag)
+  // https://github.com/scala/bug/issues/11715
   def FIXMEgetLTagAlso[DIU <: Tags with Singleton, T](t: c.Expr[DIU#ScalaReflectTypeTag[T]])(implicit w: c.WeakTypeTag[T]): c.Expr[DIU#Tag[T]] = {
-    val tagMacro = new LightTypeTagMacro0[c.type](c)
-    val ltag = tagMacro.makeParsedLightTypeTag[T](w)
+    val ltagMacro = new LightTypeTagMacro0[c.type](c)
+    val ltag = ltagMacro.makeParsedLightTypeTag[T](w)
     c.Expr[DIU#Tag[T]] {
       q"${c.prefix.asInstanceOf[Expr[DIU#TagObject]]}.apply[$w]($t, $ltag)"
     }
@@ -111,7 +102,7 @@ class TagMacro(val c: blackbox.Context) {
       AnnotationTools.findArgument(_) {
         case Literal(Constant(s: String)) => s
       }
-    ).getOrElse(defaultError)
+    ).getOrElse(defaultTagImplicitError)
   }
 
   @inline
@@ -120,7 +111,7 @@ class TagMacro(val c: blackbox.Context) {
       intersection.map {
         t0 =>
           val t = norm(t0.dealias)
-          summonTag[DIU](t)
+          summonTypeTagFromTag[DIU](t)
       }
     }")
     val structTag = mkStruct[DIU](struct)
@@ -151,23 +142,23 @@ class TagMacro(val c: blackbox.Context) {
   protected[this] def mkTag[DIU <: Tags with Singleton : c.WeakTypeTag, T: c.WeakTypeTag](universe: c.Expr[DIU], tpe: c.Type): c.Expr[DIU#Tag[T]] = {
 
     val ctor = tpe.typeConstructor
-    val constructorTag: c.Expr[DIU#ScalaReflectTypeTag[_]] = paramKind(ctor) match {
-      case None =>
-        val tpeN = applyToNothings(tpe)
-        logger.log(s"Type after replacing with Nothing $tpeN, replaced args ${tpe.typeArgs}")
-        summonTypeTag[DIU](tpeN)
-      case Some(Kind(Nil)) =>
+    val constructorTag: c.Expr[DIU#ScalaReflectTypeTag[_]] = typeParameterKind(ctor) match {
+      case None => // concrete type
+        val tpeToNothings = applyToNothings(tpe)
+        logger.log(s"Type after replacing with Nothing $tpeToNothings, replaced args ${tpe.typeArgs}")
+        summonTypeTag[DIU, Any](tpeToNothings)
+      case Some(Kind(Nil)) => // error: lower-kinded type parameter in constructor position
         val msg = s"  could not find implicit value for ${hktagFormat(tpe)}: $tpe is a type parameter without an implicit Tag or TypeTag!"
         addImplicitError(msg)
         c.abort(c.enclosingPosition, msg)
-      case Some(hole) =>
-        summonTag[DIU](ctor, hole)
+      case Some(kind) =>
+        summonTypeTagFromTagWithKind[DIU](ctor, kind)
     }
 
     val args = tpe.typeArgs.map {
       t => norm(t.dealias)
     }
-    val argTags = c.Expr[List[DIU#ScalaReflectTypeTag[_]]](q"${args.map(summonTag[DIU])}")
+    val argTags = c.Expr[List[DIU#ScalaReflectTypeTag[_]]](q"${args.map(summonTypeTagFromTag[DIU])}")
 
     reify {
       {universe.splice.Tag.appliedTag[T](constructorTag.splice, argTags.splice)}
@@ -175,7 +166,7 @@ class TagMacro(val c: blackbox.Context) {
   }
 
   @inline
-  protected[this] def paramKind(tpe: c.Type): Option[Kind] = {
+  protected[this] def typeParameterKind(tpe: c.Type): Option[Kind] = {
     // c.internal.isFreeType ?
     if (tpe.typeSymbol.isParameter)
       Some(kindOf(tpe))
@@ -238,51 +229,54 @@ class TagMacro(val c: blackbox.Context) {
   }
 
   @inline
-  protected[this] def summonTypeTag[DIU <: Tags with Singleton : c.WeakTypeTag](tpeN: c.Type): c.Expr[DIU#ScalaReflectTypeTag[_]] = {
-    c.Expr[DIU#ScalaReflectTypeTag[_]](q"_root_.scala.Predef.implicitly[${appliedType(weakTypeOf[DIU#ScalaReflectTypeTag[Nothing]], tpeN)}]")
+  protected[this] def summonTypeTag[DIU <: Tags with Singleton : c.WeakTypeTag, T](tpe: c.Type): c.Expr[DIU#ScalaReflectTypeTag[T]] = {
+    c.Expr[DIU#ScalaReflectTypeTag[T]](q"_root_.scala.Predef.implicitly[${appliedType(weakTypeOf[DIU#ScalaReflectTypeTag[Nothing]], tpe)}]")
   }
 
   @inline
-  protected[this] def summonWeakTypeTag[DIU <: Tags with Singleton : c.WeakTypeTag](tpeN: c.Type): c.Expr[DIU#ScalaReflectWeakTypeTag[_]] = {
-    c.Expr[DIU#ScalaReflectWeakTypeTag[_]](q"_root_.scala.Predef.implicitly[${appliedType(weakTypeOf[DIU#ScalaReflectWeakTypeTag[Nothing]], tpeN)}]")
+  protected[this] def summonWeakTypeTag[DIU <: Tags with Singleton : c.WeakTypeTag](tpe: c.Type): c.Expr[DIU#ScalaReflectWeakTypeTag[_]] = {
+    c.Expr[DIU#ScalaReflectWeakTypeTag[_]](q"_root_.scala.Predef.implicitly[${appliedType(weakTypeOf[DIU#ScalaReflectWeakTypeTag[Nothing]], tpe)}]")
   }
 
   @inline
-  protected[this] def summonTag[DIU <: Tags with Singleton : c.WeakTypeTag](tpe: c.Type): c.Expr[DIU#ScalaReflectTypeTag[_]] = {
-    summonTag[DIU](tpe, kindOf(tpe))
+  protected[this] def summonTypeTagFromTag[DIU <: Tags with Singleton : c.WeakTypeTag](tpe: c.Type): c.Expr[DIU#ScalaReflectTypeTag[_]] = {
+    summonTypeTagFromTagWithKind[DIU](tpe, kindOf(tpe))
   }
 
   @inline
-  protected[this] def summonTag[DIU <: Tags with Singleton : c.WeakTypeTag](tpe: c.Type, kind: Kind): c.Expr[DIU#ScalaReflectTypeTag[_]] = {
-    val summoned = try {
-      if (kind == Kind(Nil)) {
-        c.inferImplicitValue(appliedType(weakTypeOf[DIU#Tag[Nothing]].typeConstructor, tpe), silent = false)
-      } else {
-        val Arg = mkHKTagArg(tpe, kind)
-        logger.log(s"Created impicit Arg: $Arg")
-        c.inferImplicitValue(appliedType(weakTypeOf[DIU#HKTag[Nothing]].typeConstructor, Arg), silent = false)
-      }
-    } catch {
-      case _: TypecheckException =>
-        val msg =
-          s"""  could not find implicit value for ${hktagFormat(tpe)}
-             |${
-            hktagFormatMap.get(kind) match {
-              case Some(_) => ""
-              case None =>
-                val (args, params) = kind.args.zipWithIndex.map {
-                  case (k, i) =>
-                    val name = s"T${i + 1}"
-                    k.format(name) -> name
-                }.unzip
-                s"""\n$tpe is of a kind $kind, which doesn't have a tag name. Please create a tag synonym as follows:\n\n
-                   |  type TagXXX[${kind.format("K")}] = HKTag[ { type Arg[${args.mkString(", ")}] = K[${params.mkString(", ")}] } ]\n\n
-                   |And use it in your context bound, as in def x[$tpe: TagXXX] = ...
+  protected[this] def summonTypeTagFromTagWithKind[DIU <: Tags with Singleton : c.WeakTypeTag](tpe: c.Type, kind: Kind): c.Expr[DIU#ScalaReflectTypeTag[_]] = {
+    // dynamically typed
+    val summoned = {
+      try {
+        if (kind == Kind(Nil)) {
+          c.inferImplicitValue(appliedType(weakTypeOf[DIU#Tag[Nothing]].typeConstructor, tpe), silent = false)
+        } else {
+          val Arg = mkHKTagArg(tpe, kind)
+          logger.log(s"Created impicit Arg: $Arg")
+          c.inferImplicitValue(appliedType(weakTypeOf[DIU#HKTag[Nothing]].typeConstructor, Arg), silent = false)
+        }
+      } catch {
+        case _: TypecheckException =>
+          val msg =
+            s"""  could not find implicit value for ${hktagFormat(tpe)}
+               |${
+              hktagFormatMap.get(kind) match {
+                case Some(_) => ""
+                case None =>
+                  val (args, params) = kind.args.zipWithIndex.map {
+                    case (k, i) =>
+                      val name = s"T${i + 1}"
+                      k.format(name) -> name
+                  }.unzip
+                  s"""\n$tpe is of a kind $kind, which doesn't have a tag name. Please create a tag synonym as follows:\n\n
+                     |  type TagXXX[${kind.format("K")}] = HKTag[ { type Arg[${args.mkString(", ")}] = K[${params.mkString(", ")}] } ]\n\n
+                     |And use it in your context bound, as in def x[$tpe: TagXXX] = ...
                """.stripMargin
-            }
-          }""".stripMargin
-        addImplicitError(msg)
-        c.abort(c.enclosingPosition, msg)
+              }
+            }""".stripMargin
+          addImplicitError(msg)
+          c.abort(c.enclosingPosition, msg)
+      }
     }
 
     c.Expr[DIU#ScalaReflectTypeTag[_]](q"{$summoned.tpe}")
@@ -320,7 +314,7 @@ class TagMacro(val c: blackbox.Context) {
 
   @inline
   protected[this] def resetImplicitError[DIU <: Tags with Singleton : c.WeakTypeTag](): Unit = {
-    setImplicitError[DIU](defaultError)
+    setImplicitError[DIU](defaultTagImplicitError)
   }
 
   @inline
@@ -330,10 +324,10 @@ class TagMacro(val c: blackbox.Context) {
     val _ = symbolOf[DIU#Tag[Any]].setAnnotations(Annotation(typeOf[implicitNotFound], List[Tree](Literal(Constant(err))), ListMap.empty))
   }
 
-  //        // TODO: in 2.13 we can use these little functions to enrich error messages further (possibly remove .setAnnotation hack completely) by attaching implicitNotFound to parameter
-  //        c.Expr[DIU#ScalaReflectTypeTag[_]](q"""
-  //           { def $name(implicit @_root_.scala.annotation.implicitNotFound($implicitMsg)
-  //          $param: ${appliedType(weakTypeOf[DIU#Tag[Nothing]], t)}) = $param; $name.tag }""")
+//  // TODO: in 2.13 we can use these little functions to enrich error messages further (possibly remove .setAnnotation hack completely) by attaching implicitNotFound to parameter
+//  c.Expr[DIU#ScalaReflectTypeTag[_]](q"""
+//     { def $name(implicit @_root_.scala.annotation.implicitNotFound($implicitMsg)
+//    $param: ${appliedType(weakTypeOf[DIU#Tag[Nothing]], t)}) = $param; $name.tag }""")
 
 }
 
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/Tags.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/Tags.scala
index 1a281cbb8..431e16a3a 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/Tags.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/Tags.scala
@@ -46,10 +46,9 @@ trait Tags extends UniverseGeneric { self =>
   @implicitNotFound("could not find implicit value for Tag[${T}]. Did you forget to put on a Tag, TagK or TagKK context bound on one of the parameters in ${T}? e.g. def x[T: Tag, F[_]: TagK] = ...")
   trait Tag[T] extends TagInterface[T, TypeTag] {
     def tag: LightTypeTag
-
-    override def toString: String = s"Tag[${tpe.tpe}]@@[$tag]"
-
     def tpe: TypeTag[T]
+
+    override final def toString: String = s"Tag[${tpe.tpe}]@@[$tag]"
   }
 
   object Tag extends LowPriorityTagInstances {
@@ -86,6 +85,7 @@ trait Tags extends UniverseGeneric { self =>
         override def tag: LightTypeTag = FLTT
       }
     }
+
     /**
       * Resulting [Tag] will not have the ability to migrate into a different universe
       * (which is not usually a problem, but still worth naming it 'unsafe')
@@ -138,16 +138,9 @@ trait Tags extends UniverseGeneric { self =>
     /** For construction from [[TagLambdaMacro]] */
     type HKTagRef[T] = HKTag[T]
 
-    // FIXME: report scalac bug - `Nothing` type is lost when two implicits for it are summoned as in
-    //    implicit final def tagFromTypeTag[T](implicit t: TypeTag[T], l: LTag[T]): Tag[T] = Tag(t, l.fullLightTypeTag)
-    //  [info] /Users/kai/src/izumi-r2/distage/distage-core/src/test/scala/izumi/distage/impl/TagTest.scala:55:17: universe.this.RuntimeDIUniverse.Tag.tagFromTypeTag is not a valid implicit value for izumi.distage.model.reflection.universe.RuntimeDIUniverse.Tag[Nothing] because:
-    //  [info] hasMatchingSymbol reported error: type mismatch;
-    //  [info]  found   : izumi.fundamentals.reflection.macrortti.LTag.Weak[Nothing]
-    //  [info]  required: izumi.fundamentals.reflection.macrortti.LWeakTag[T]
-    //  [info]     (which expands to)  izumi.fundamentals.reflection.macrortti.LTag.Weak[T]
-    //  [info] Note: Nothing <: T, but class Weak is invariant in type T.
-    //  [info] You may wish to define T as +T instead. (SLS 4.5)
-    //  [info]       assert(Tag[Nothing].tpe == safe[Nothing])
+    // workaround for a scalac bug - `Nothing` type is lost when two implicits for it are summoned from one implicit as in:
+    //  implicit final def tagFromTypeTag[T](implicit t: TypeTag[T], l: LTag[T]): Tag[T] = Tag(t, l.fullLightTypeTag)
+    // https://github.com/scala/bug/issues/11715
     implicit final def tagFromTypeTag[T](implicit t: TypeTag[T]): Tag[T] = macro TagMacro.FIXMEgetLTagAlso[self.type, T]
   }
 
@@ -178,12 +171,11 @@ trait Tags extends UniverseGeneric { self =>
     *
     */
   trait HKTag[T] extends TagInterface[T, TypeTag] {
-    def tag: LightTypeTag
-
-    override def toString: String = s"${hktagFormat(tpe.tpe)}@@[$tag]"
-
     /** Internal `TypeTag` holding the `typeConstructor` of type `T` */
     def tpe: TypeTag[_]
+    def tag: LightTypeTag
+
+    override final def toString: String = s"${hktagFormat(tpe.tpe)}@@[$tag]"
   }
 
   object HKTag extends LowPriorityHKTagInstances {
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TrivialMacroLogger.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TrivialMacroLogger.scala
index 0a4c0959c..0e17886a0 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TrivialMacroLogger.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/TrivialMacroLogger.scala
@@ -15,7 +15,7 @@ import scala.reflect.macros.blackbox
   * @see [[DebugProperties]]
   */
 object TrivialMacroLogger {
-  def make[T: ClassTag](c: blackbox.Context, id: String): TrivialLogger = {
-    TrivialLogger.make[T](id, config = Config(sink = new ScalacSink(c)))
+  def make[T: ClassTag](c: blackbox.Context, sysProperty: String): TrivialLogger = {
+    TrivialLogger.make[T](sysProperty, config = Config(sink = new ScalacSink(c)))
   }
 }
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTag.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTag.scala
index 67b64dbc5..58fcbf79d 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTag.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTag.scala
@@ -93,6 +93,14 @@ object LightTypeTag {
     }
   }
 
+  def parse[T](refString: String, basesString: String): LightTypeTag = {
+    lazy val shared = {
+      subtypeDBsSerializer.unpickle(ByteBuffer.wrap(basesString.getBytes("ISO-8859-1")))
+    }
+
+    new ParsedLightTypeTag(refString, () => shared.bases, () => shared.idb)
+  }
+
   final class ParsedLightTypeTag(
                                   private val refString: String,
                                   bases: () => Map[AbstractReference, Set[AbstractReference]],
@@ -116,16 +124,7 @@ object LightTypeTag {
     final case class SubtypeDBs(bases: Map[AbstractReference, Set[AbstractReference]], idb: Map[NameReference, Set[NameReference]])
   }
 
-  // parse lazy ParsedLightTypeTag
-  def parse[T](refString: String, basesString: String): LightTypeTag = {
-    lazy val shared = {
-      subtypeDBsSerializer.unpickle(ByteBuffer.wrap(basesString.getBytes("ISO-8859-1")))
-    }
-
-    new ParsedLightTypeTag(refString, () => shared.bases, () => shared.idb)
-  }
-
-  val (lttRefSerializer: Pickler[LightTypeTagRef], subtypeDBsSerializer: Pickler[SubtypeDBs]) = {
+  private[macrortti] val (lttRefSerializer: Pickler[LightTypeTagRef], subtypeDBsSerializer: Pickler[SubtypeDBs]) = {
     import boopickle.Default._
 
     implicit lazy val appliedRefSerializer: Pickler[AppliedReference] = generatePickler[AppliedReference]
@@ -136,15 +135,13 @@ object LightTypeTag {
     implicit lazy val dbsSerializer: Pickler[SubtypeDBs] = generatePickler[SubtypeDBs]
 
     // false positive unused warnings
-    appliedRefSerializer.discard();
-    nameRefSerializer.discard();
+    appliedRefSerializer.discard()
+    nameRefSerializer.discard()
     abstractRefSerializer.discard()
 
     (refSerializer, dbsSerializer)
   }
 
-  private[izumi] object ReflectionLock
-
   private[macrortti] def mergeIDBs[T](self: Map[T, Set[T]], other: Map[T, Set[T]]): Map[T, Set[T]] = {
     import izumi.fundamentals.collections.IzCollections._
 
@@ -152,4 +149,6 @@ object LightTypeTag {
     both.toMultimap.mapValues(_.flatten).toMap
   }
 
+  private[izumi] object ReflectionLock
+
 }
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagImpl.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagImpl.scala
index 10d461860..e5cdba1a6 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagImpl.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagImpl.scala
@@ -4,6 +4,7 @@ import java.util.concurrent.ConcurrentHashMap
 
 import izumi.fundamentals.collections.IzCollections._
 import izumi.fundamentals.platform.console.TrivialLogger
+import izumi.fundamentals.platform.strings.IzString._
 import izumi.fundamentals.reflection.ReflectionUtil._
 import izumi.fundamentals.reflection.macrortti.LightTypeTag.ReflectionLock
 import izumi.fundamentals.reflection.macrortti.LightTypeTagImpl.Broken
@@ -16,31 +17,33 @@ import scala.collection.mutable
 import scala.reflect.api.Universe
 
 object LightTypeTagImpl {
-  lazy val cache = new ConcurrentHashMap[Any, Any]()
+  private lazy val globalCache = new ConcurrentHashMap[Any, AbstractReference]
 
+  /** caching is enabled by default for runtime light type tag creation */
+  private[this] def runtimeCacheEnabled(): Boolean = {
+    System.getProperty(DebugProperties.`izumi.rtti.cache.runtime`).asBoolean()
+      .getOrElse(true)
+  }
+
+  /** Create a LightTypeTag at runtime for a reflected type */
   def makeLightTypeTag(u: Universe)(typeTag: u.Type): LightTypeTag = {
     ReflectionLock.synchronized {
       val logger = TrivialLogger.make[this.type](DebugProperties.`izumi.debug.macro.rtti`)
-      new LightTypeTagImpl[u.type](u, withCache = false, logger).makeFullTagImpl(typeTag)
+      new LightTypeTagImpl[u.type](u, withCache = runtimeCacheEnabled(), logger).makeFullTagImpl(typeTag)
     }
   }
 
   sealed trait Broken[T, S] {
     def toSet: Set[T]
   }
-
   object Broken {
-
     final case class Single[T, S](t: T) extends Broken[T, S] {
       override def toSet: Set[T] = Set(t)
     }
-
     final case class Compound[T, S](tpes: Set[T], decls: Set[S]) extends Broken[T, S] {
       override def toSet: Set[T] = tpes
     }
-
   }
-
 }
 
 final class LightTypeTagImpl[U <: SingletonUniverse](val u: U, withCache: Boolean, logger: TrivialLogger) {
@@ -304,12 +307,12 @@ final class LightTypeTagImpl[U <: SingletonUniverse](val u: U, withCache: Boolea
     if (withCache) {
       val st = new StableType(tpe)
       // we may accidentally recompute twice in concurrent environment but that's fine
-      Option(LightTypeTagImpl.cache.get(st)) match {
+      Option(LightTypeTagImpl.globalCache.get(st)) match {
         case Some(value) =>
-          value.asInstanceOf[AbstractReference]
+          value
         case None =>
           val ref = makeRef(tpe, Map.empty)
-          LightTypeTagImpl.cache.put(st, ref)
+          LightTypeTagImpl.globalCache.put(st, ref)
           ref
       }
     } else {
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagMacro.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagMacro.scala
index 115cabe8e..7a011bb95 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagMacro.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/LightTypeTagMacro.scala
@@ -15,22 +15,21 @@ private[reflection] class LightTypeTagMacro0[C <: blackbox.Context](val c: C) {
 
   final val lightTypeTag: Tree = q"${symbolOf[LightTypeTag.type].asClass.module}"
 
-  private val logger: TrivialLogger = TrivialMacroLogger.make[this.type](c, DebugProperties.`izumi.debug.macro.rtti`)
+  final private val logger: TrivialLogger = TrivialMacroLogger.make[this.type](c, DebugProperties.`izumi.debug.macro.rtti`)
 
-  def cacheEnabled: Boolean = c.settings.contains("ltt-cache")
+  protected final def cacheEnabled(): Boolean = c.settings.contains(DebugProperties.`izumi.rtti.cache.compile`)
+  protected final val impl = new LightTypeTagImpl[c.universe.type](c.universe, withCache = cacheEnabled(), logger)
 
-  protected val impl = new LightTypeTagImpl[c.universe.type](c.universe, withCache = cacheEnabled, logger)
-
-  @inline def makeWeakHKTag[ArgStruct: c.WeakTypeTag]: c.Expr[LTag.WeakHK[ArgStruct]] = {
+  @inline final def makeWeakHKTag[ArgStruct: c.WeakTypeTag]: c.Expr[LTag.WeakHK[ArgStruct]] = {
     makeHKTagRaw[ArgStruct](weakTypeOf[ArgStruct])
   }
 
-  @inline def makeWeakTag[T: c.WeakTypeTag]: c.Expr[LTag.Weak[T]] = {
+  @inline final def makeWeakTag[T: c.WeakTypeTag]: c.Expr[LTag.Weak[T]] = {
     val res = makeParsedLightTypeTagImpl(weakTypeOf[T])
     c.Expr[LTag.Weak[T]](q"new ${weakTypeOf[LTag.Weak[T]]}($res)")
   }
 
-  @inline def makeStrongTag[T: c.WeakTypeTag]: c.Expr[LTag[T]] = {
+  @inline final def makeStrongTag[T: c.WeakTypeTag]: c.Expr[LTag[T]] = {
     val tpe = weakTypeOf[T]
     if (allPartsStrong(tpe)) {
       val res = makeParsedLightTypeTagImpl(tpe)
@@ -40,11 +39,11 @@ private[reflection] class LightTypeTagMacro0[C <: blackbox.Context](val c: C) {
     }
   }
 
-  @inline def makeParsedLightTypeTag[T: c.WeakTypeTag]: c.Expr[LightTypeTag] = {
+  @inline final def makeParsedLightTypeTag[T: c.WeakTypeTag]: c.Expr[LightTypeTag] = {
     makeParsedLightTypeTagImpl(weakTypeOf[T])
   }
 
-  def makeHKTagRaw[ArgStruct](argStruct: Type): c.Expr[LTag.WeakHK[ArgStruct]] = {
+  final def makeHKTagRaw[ArgStruct](argStruct: Type): c.Expr[LTag.WeakHK[ArgStruct]] = {
     def badShapeError(t: TypeApi) = {
       c.abort(c.enclosingPosition, s"Expected type shape RefinedType `{ type Arg[A] = X[A] }` for summoning `LightTagK[X]`, but got $t (raw: ${showRaw(t)} ${t.getClass})")
     }
@@ -62,7 +61,7 @@ private[reflection] class LightTypeTagMacro0[C <: blackbox.Context](val c: C) {
     }
   }
 
-  def makeParsedLightTypeTagImpl(tpe: Type): c.Expr[LightTypeTag] = {
+  final def makeParsedLightTypeTagImpl(tpe: Type): c.Expr[LightTypeTag] = {
     val res = impl.makeFullTagImpl(tpe)
 
     @inline def serialize[A: Pickler](a: A): String = {
@@ -76,7 +75,7 @@ private[reflection] class LightTypeTagMacro0[C <: blackbox.Context](val c: C) {
     c.Expr[LightTypeTag](q"$lightTypeTag.parse($strRef : _root_.java.lang.String, $strDBs : _root_.java.lang.String)")
   }
 
-  protected def allPartsStrong(tpe: Type): Boolean = {
+  protected final def allPartsStrong(tpe: Type): Boolean = {
     def selfStrong = !tpe.typeSymbol.isParameter
     def prefixStrong = {
       tpe match {
diff --git a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/RuntimeAPI.scala b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/RuntimeAPI.scala
index ea8b3b790..3b6bee181 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/RuntimeAPI.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala/izumi/fundamentals/reflection/macrortti/RuntimeAPI.scala
@@ -1,8 +1,8 @@
 package izumi.fundamentals.reflection.macrortti
 
-import izumi.fundamentals.reflection.macrortti.LightTypeTagRef.{AbstractReference, AppliedNamedReference, AppliedReference, Boundaries, FullReference, IntersectionReference, Lambda, NameReference, Refinement, RefinementDecl, TypeParam}
+import izumi.fundamentals.reflection.macrortti.LightTypeTagRef._
 
-protected[izumi] object RuntimeAPI {
+private[izumi] object RuntimeAPI {
 
   def unpack(ref: AbstractReference): Set[NameReference] = {
     def unpackBoundaries(b: Boundaries): Set[NameReference] = {
@@ -67,7 +67,7 @@ protected[izumi] object RuntimeAPI {
     }
   }
 
-  class Rewriter[T](rules: Map[String, T])(complete: (Rewriter[T], NameReference, T) => AbstractReference) {
+  final class Rewriter[T](rules: Map[String, T])(complete: (Rewriter[T], NameReference, T) => AbstractReference) {
     def replaceRefs(reference: AbstractReference): AbstractReference = {
       reference match {
         case l: Lambda =>
diff --git a/project/Deps.sc b/project/Deps.sc
index 4cec69131..3e6c51157 100644
--- a/project/Deps.sc
+++ b/project/Deps.sc
@@ -639,7 +639,7 @@ object Izumi {
           "siteSubdirName" in SettingScope.Raw("Paradox") := """s"${DocKeys.prefix.value}/doc"""".raw,
           SettingDef.RawSettingDef(
             """paradoxProperties ++= Map(
-            "scaladoc.izumi.base_url" -> s"/${DocKeys.prefix.value}/api/izumi/",
+            "scaladoc.izumi.base_url" -> s"/${DocKeys.prefix.value}/api/",
             "scaladoc.base_url" -> s"/${DocKeys.prefix.value}/api/",
             "izumi.version" -> version.value,
           )"""),


Test patch:
diff --git a/distage/distage-core/src/test/scala/izumi/distage/dsl/DSLTest.scala b/distage/distage-core/src/test/scala/izumi/distage/dsl/DSLTest.scala
index b2629c301..959bb3613 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/dsl/DSLTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/dsl/DSLTest.scala
@@ -168,8 +168,8 @@ class DSLTest extends WordSpec {
       }
 
       assert(definition.bindings == Set(
-        Bindings.binding[TestClass].withTags(Set("tag1", "tag2"))
-        , Bindings.binding[TestDependency0].withTags(Set("tag1", "tag2", "sniv")))
+        Bindings.binding[TestClass].addTags(Set("tag1", "tag2"))
+        , Bindings.binding[TestDependency0].addTags(Set("tag1", "tag2", "sniv")))
       )
     }