@@ -466,22 +466,80 @@ use them when the given `p: Product` has a smaller `productArity` than the curre
466466 does not matter. Trying to standardize this across all possible macros and all possible
467467 typeclasses is out of scope
468468
469- 6 . ` @unroll ` only supports ` final ` methods. ` object ` methods and constructors are naturally
469+ 6 . ` @unroll ` generates a quadratic amount of generated bytecode as more default parameters
470+ are added: each forwarder has ` O(num-params) ` size, and there are ` O(num-default-params) `
471+ forwarders. We do not expect this to be a problem in practice, as the small size of the
472+ generated forwarder methods means the constant factor is small, but one could imagine
473+ the ` O(n^2) ` asymptotic complexity becoming a problem if a method accumulates hundreds of
474+ default parameters over time. In such extreme scenarios, some kind of builder pattern
475+ (such as those listed in [ Major Alternatives] ( #major-alternatives ) ) may be preferable.
476+
477+ 7 . ` @unroll ` only supports ` final ` methods. ` object ` methods and constructors are naturally
470478 final, but ` class ` or ` trait ` methods that are ` @unroll ` ed need to be explicitly marked ` final ` .
471479 It has proved difficult to implement the semantics of ` @unroll ` in the presence of downstream
472480 overrides, ` super ` , etc. where the downstream overrides can be compiled against by different
473481 versions of the upstream code. If we can come up with some implementation that works, we can
474482 lift this restriction later, but for now I have not managed to do so and so this restriction
475483 stays.
476484
477- 7 . ` @unroll ` generates a quadratic amount of generated bytecode as more default parameters
478- are added: each forwarder has ` O(num-params) ` size, and there are ` O(num-default-params) `
479- forwarders. We do not expect this to be a problem in practice, as the small size of the
480- generated forwarder methods means the constant factor is small, but one could imagine
481- the ` O(n^2) ` asymptotic complexity becoming a problem if a method accumulates hundreds of
482- default parameters over time. In such extreme scenarios, some kind of builder pattern
483- (such as those listed in [ Major Alternatives] ( #major-alternatives ) ) may be preferable.
484-
485+ ### Challenges of Non-Final Methods and Overriding
486+
487+ To elaborate a bit on the issues with non-final methods and overriding, consider the following
488+ case with three classes, ` Upstream ` , ` Middle ` , and ` Downstream ` , each of which is compiled
489+ against different versions of each other (hence the varying number of parameters for ` foo ` ):
490+
491+ ``` scala
492+ class Upstream { // V2
493+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , l : Long = 0 ) = s + n + b + l
494+ }
495+ ```
496+
497+ ``` scala
498+ class Downstream extends Upstream { // compiled against Upstream V1
499+ override def foo (s : String , n : Int = 1 ) = super .foo(s, n) + s + n
500+ }
501+ ```
502+
503+ ``` scala
504+ object Main1 { // compiled against Upstream V2
505+ def main (args : Array [String ]): Unit = {
506+ new Downstream ().foo(" hello" , 123 , false , 456L )
507+ }
508+ }
509+ ```
510+
511+ ``` scala
512+ object Main2 { // compiled against Upstream V1
513+ def main (args : Array [String ]): Unit = {
514+ new Downstream ().foo(" hello" , 123 )
515+ }
516+ }
517+ ```
518+
519+
520+ The challenge here is: how do we make sure that ` Main1 ` and ` Main2 ` , who call
521+ ` new Downstream().foo ` , correctly pick up the version of ` def foo ` that is
522+ provided by ` Downstream ` ?
523+
524+ With the current implementation, the ` override def foo ` inside ` Downstream ` would only
525+ override one of ` Upstream ` 's synthetic forwarders, but would not override the actual
526+ primary implementation. As a result, we would see ` Main1 ` calling the implementation
527+ of ` foo ` from ` Upstream ` , while ` Main2 ` calls the implementation of ` foo ` from
528+ ` Downstream ` . So even though both ` Main1 ` and ` Main2 ` have the same
529+ ` Upstream ` and ` Downstream ` code on the classpath, they end up calling different
530+ implementations based on what they were compiled against.
531+
532+ We cannot perform the method search and dispatch _ within_ the ` def foo ` methods,
533+ because it depends on exactly _ how_ ` foo ` is called: the ` InvokeVirtual ` call from
534+ ` Main1 ` is meant to resolve to ` Downstream#foo ` , while the ` InvokeSpecial ` call
535+ from ` Downstream#foo ` 's ` super.foo ` is meant to resolve to ` Upstream#foo ` . But a
536+ method implementation cannot know how it was called, and thus it is impossible
537+ for ` def foo ` to forward the call to the right place.
538+
539+ This issue only arises in the presence of version skew between ` Upstream ` and
540+ ` Downstream ` code, but that scenario is precisely where ` @unroll ` is meant to
541+ provide benefits. Thus, unless we can find some solution, we cannot properly support
542+ virtual methods and overrides in ` @unroll ` .
485543
486544## Major Alternatives
487545
0 commit comments