fomkin/korolev

bug in DiffRenderContext

Zhen-hao opened this issue · 7 comments

It is triggered by user code, but I think the bug in how diff is rendered.
If I use access.transition the failure is silent. access.syncTransition makes the issue clear.

[info] scala.MatchError: 3 (of class java.lang.Byte)
[info]  at levsha.impl.DiffRenderContext.deleteLoop(DiffRenderContext.scala:408)
[info]  at levsha.impl.DiffRenderContext.diff(DiffRenderContext.scala:242)
[info]  at korolev.internal.ApplicationInstance.$anonfun$onState$8(ApplicationInstance.scala:119)
[info]  at korolev.internal.ApplicationInstance.$anonfun$onState$8$adapted(ApplicationInstance.scala:119)
[info]  at korolev.internal.Frontend.$anonfun$performDomChanges$1(Frontend.scala:189)
[info]  at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
[info]  at korolev.effect.Effect$FutureEffect.delay(Effect.scala:78)
[info]  at korolev.effect.Effect$FutureEffect.delay(Effect.scala:66)
[info]  at korolev.internal.Frontend.performDomChanges(Frontend.scala:187)
[info]  at korolev.internal.ApplicationInstance.$anonfun$onState$7(ApplicationInstance.scala:119)
[info]  at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
[info]  at korolev.effect.Effect$FutureEffect$$anon$1.execute(Effect.scala:69)
[info]  at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:393)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:302)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.dispatchOrAddCallbacks(Promise.scala:276)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.flatMap(Promise.scala:140)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:86)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:66)
[info]  at korolev.effect.syntax$EffectOps.flatMap(syntax.scala:36)
[info]  at korolev.internal.ApplicationInstance.$anonfun$onState$5(ApplicationInstance.scala:108)
[info]  at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
[info]  at korolev.effect.Effect$FutureEffect$$anon$1.execute(Effect.scala:69)
[info]  at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:393)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:302)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.dispatchOrAddCallbacks(Promise.scala:276)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.flatMap(Promise.scala:140)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:86)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:66)
[info]  at korolev.effect.syntax$EffectOps.flatMap(syntax.scala:36)
[info]  at korolev.internal.ApplicationInstance.$anonfun$onState$1(ApplicationInstance.scala:105)
[info]  at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
[info]  at korolev.effect.Effect$FutureEffect$$anon$1.execute(Effect.scala:69)
[info]  at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:393)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:302)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.dispatchOrAddCallbacks(Promise.scala:276)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.flatMap(Promise.scala:140)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:86)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:66)
[info]  at korolev.effect.syntax$EffectOps.flatMap(syntax.scala:36)
[info]  at korolev.internal.ApplicationInstance.onState(ApplicationInstance.scala:103)
[info]  at korolev.internal.ApplicationInstance.$anonfun$topLevelComponentInstance$2(ApplicationInstance.scala:84)
[info]  at korolev.internal.ComponentInstance.$anonfun$applyTransition$4(ComponentInstance.scala:280)
[info]  at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
[info]  at korolev.effect.Effect$FutureEffect$$anon$1.execute(Effect.scala:69)
[info]  at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:393)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:302)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.dispatchOrAddCallbacks(Promise.scala:276)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.flatMap(Promise.scala:140)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:86)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:66)
[info]  at korolev.effect.syntax$EffectOps.flatMap(syntax.scala:36)
[info]  at korolev.internal.ComponentInstance.$anonfun$applyTransition$2(ComponentInstance.scala:279)
[info]  at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
[info]  at korolev.effect.Effect$FutureEffect$$anon$1.execute(Effect.scala:69)
[info]  at scala.concurrent.impl.Promise$Transformation.submitWithValue(Promise.scala:393)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.submitWithValue(Promise.scala:302)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.dispatchOrAddCallbacks(Promise.scala:276)
[info]  at scala.concurrent.impl.Promise$DefaultPromise.flatMap(Promise.scala:140)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:86)
[info]  at korolev.effect.Effect$FutureEffect.flatMap(Effect.scala:66)
[info]  at korolev.effect.syntax$EffectOps.flatMap(syntax.scala:36)
[info]  at korolev.internal.ComponentInstance.$anonfun$applyTransition$1(ComponentInstance.scala:277)
[info]  at korolev.internal.ComponentInstance.applyTransition(ComponentInstance.scala:282)
[info]  at korolev.internal.ComponentInstance$browserAccess$.syncTransition(ComponentInstance.scala:140)
[info]  at com.nt.front.UiWebService.$anonfun$config$433(UiWebService.scala:2793)

I found the cause from user code:

select(id := "dropdown-list",
           option(value := "choice1", "Cat", if (true) selected else void),
           option(value := "choice2", "Dog", if (false) selected else void),
         selectorId)

though this type checks, but it breaks rendering.
The following works fine.

select(id := "dropdown-list",
          if (true) option(value := "choice1", "Cat", selected) else option(value := "choice1", "Cat"),
          if (false)  option(value := "choice2", "Dog", selected) else option(value := "choice2", "Dog"),
         selectorId)

maybe my understanding of void is wrong.

but it would be great to catch this at compile time.

It's known problem. If levsha.macros.unableToSortTagWarnings sys property is true, you will get warning about this. Unfortunately I never added it to documentation. Try:

select(id := "dropdown-list",
  option(value := "choice1", if (true) selected else void, "Cat"),
  option(value := "choice2", if (false) selected else void, "Dog"),
  selectorId
)

Also you can use when as shortcut for if (cond) node else void.

The problem explanation: DiffRenderContext requires strict order of Document kinds. First goes styles, then attributes, then nodes. Content of tag can be sorted in runtime, but optimeze macro should do it in compile time. Unfortunately, sometimes it's not possible to infer kind of a Document in compile time, so it leads to the problem.

It your case:

select(
  id := "dropdown-list",
  option(
    value := "choice1", // Attr
    "Cat", // TextNode
    if (true) selected else void // Attr
  ),
  selectorId
)

Thanks for the explanation!
It would be very helpful to add a comment about the importance of ordering in the documentation.

The problem was partially fixed in Levsha 1.0.0. Now if tag content couldn't be sorted during optimization it will be leaved unoptimized. However you can force optimization for these cases if you want. See optimizer options.

BTW, when I try to use the when constructor, I got errors like this

inferred type arguments [levsha.Document.Node[com.nt.front.UiState.globalContext.Event]] do not conform to method when's type parameter bounds [D <: levsha.Document[Nothing]]
Error occurred in an application involving default arguments.

I will try it later with version 1.0