@@ -466,22 +466,84 @@ use them when the given `p: Product` has a smaller `productArity` than the curre
466
466
does not matter. Trying to standardize this across all possible macros and all possible
467
467
typeclasses is out of scope
468
468
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
470
478
final, but ` class ` or ` trait ` methods that are ` @unroll ` ed need to be explicitly marked ` final ` .
471
479
It has proved difficult to implement the semantics of ` @unroll ` in the presence of downstream
472
480
overrides, ` super ` , etc. where the downstream overrides can be compiled against by different
473
481
versions of the upstream code. If we can come up with some implementation that works, we can
474
482
lift this restriction later, but for now I have not managed to do so and so this restriction
475
483
stays.
476
484
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 four classes, ` Upstream ` , ` Downstream ` , ` Main1 ` and ` Main2 ` , 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 ` .
543
+
544
+ It may be possible to loosen this restriction to also allow abstract methods that
545
+ are implemented only once by a final method. See the section about
546
+ [ Abstract Methods] ( #abstract-methods ) for details.
485
547
486
548
## Major Alternatives
487
549
@@ -605,7 +667,7 @@ are a large number of default parameters being added every version, as it would
605
667
amount of generated code.
606
668
607
669
608
- ### Abstract and Virtual Methods
670
+ ### Abstract Methods
609
671
610
672
In [ Limitations] ( #limitations ) , I mentioned that ` @unroll ` only supports ` final ` methods.
611
673
It is likely possible for abstract methods which are ` @unrolled ` to have concrete forwarder
0 commit comments