@@ -166,6 +166,36 @@ spot it!
166
166
Sebastien Doraene's talk [ Designing Libraries for Source and Binary Compatibility] ( https://www.youtube.com/watch?v=2wkEX6MCxJs )
167
167
explores some of the challenges, and discusses the workarounds.
168
168
169
+
170
+ ## Requirements
171
+
172
+ ### Backwards Compatibility
173
+
174
+ Given:
175
+
176
+ * Two libraries, ** Upstream** and ** Downstream** , where ** Downstream** depends on ** Upstream**
177
+
178
+ * If we use a _ newer_ version of ** Upstream** which contains an added
179
+ default parameter together with an _ older_ version of ** Downstream** compiled
180
+ against an _ older_ version of ** Upstream** before that default parameter was added
181
+
182
+ * The behavior should be binary compatible and semantically indistinguishable from using
183
+ a verion of ** Downstream** compiled against the _ newer_ version of ** Upstream**
184
+
185
+ ** Note:** we do not aim for _ Forwards_ compatibility. Using an _ older_
186
+ version of ** Upstream** with a _ newer_ version of ** Downstream** compiled against a
187
+ _ newer_ version of ** Upstream** is not a use case we want to support. The vast majority
188
+ of OSS software does not promise forwards compatibility, including software such as
189
+ the JVM, so we should just follow suite
190
+
191
+ ### All Overrides Are Equivalent
192
+
193
+ All versions of an ` @unroll ` ed method ` def foo ` should have the same semantics when called
194
+ with the same parameters.
195
+
196
+ ** Note:** this includes forwarder methods that may never get called in the scenarios
197
+ required by our [ Backwards Compatibility] ( #backwards-compatibility ) requirement.
198
+
169
199
## Proposed solution
170
200
171
201
@@ -427,59 +457,187 @@ take into account field default values, and this change is necessary to make it
427
457
use them when the given ` p: Product ` has a smaller ` productArity ` than the current
428
458
` CaseClass ` implementation
429
459
460
+ ### Abstract Methods
461
+
462
+ Apart from ` final ` methods, ` @unroll ` also supports purely abstract methods. Consider
463
+ the following example with a trait ` Unrolled ` and an implementation ` UnrolledObj ` :
464
+
465
+ ``` scala
466
+ trait Unrolled { // version 3
467
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , @ unroll l : Long = 0 ): String
468
+ }
469
+ ```
470
+ ``` scala
471
+ object UnrolledObj extends Unrolled { // version 3
472
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , @ unroll l : Long = 0 ) = s + n + b
473
+ }
474
+ ```
475
+
476
+ This unrolls to:
477
+ ``` scala
478
+ trait Unrolled { // version 3
479
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , @ unroll l : Long = 0 ): String = foo(s, n, b)
480
+ def foo (s : String , n : Int , b : Boolean ): String = foo(s, n)
481
+ def foo (s : String , n : Int ): String
482
+ }
483
+ ```
484
+ ``` scala
485
+ object UnrolledObj extends Unrolled { // version 3
486
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , @ unroll l : Long = 0 ) = s + n + b + l
487
+ def foo (s : String , n : Int , b : Boolean ) = foo(s, n, b, 0 )
488
+ def foo (s : String , n : Int ) = foo(s, n, true )
489
+ }
490
+ ```
491
+
492
+ Note that both the abstract methods from ` trait Unrolled ` and the concrete methods
493
+ from ` object UnrolledObj ` generate forwarders when ` @unroll ` ed, but the forwarders
494
+ are generated _ in opposite directions_ ! Unrolled concrete methods forward from longer
495
+ parameter lists to shorter parameter lists, while unrolled abstract methods forward
496
+ from shorter parameter lists to longer parameter lists. For example, we may have a
497
+ version of ` object UnrolledObj ` that was compiled against an earlier version of ` trait Unrolled ` :
498
+
499
+
500
+ ``` scala
501
+ object UnrolledObj extends Unrolled { // version 2
502
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true ) = s + n + b
503
+ def foo (s : String , n : Int ) = foo(s, n, true )
504
+ }
505
+ ```
506
+
507
+ But further downstream code calling ` .foo ` on ` UnrolledObj ` may expect any of the following signatures,
508
+ depending on what version of ` Unrolled ` and ` UnrolledObj ` it was compiled against:
509
+
510
+ ``` scala
511
+ UnrolledObj .foo(String , Int )
512
+ UnrolledObj .foo(String , Int , Boolean )
513
+ UnrolledObj .foo(String , Int , Boolean , Long )
514
+ ```
515
+
516
+ Because such downstream code cannot know which version of ` Unrolled ` that ` UnrolledObj `
517
+ was compiled against, we need to ensure all such calls find their way to the correct
518
+ implementation of ` def foo ` , which may be at any of the above signatures. This "double
519
+ forwarding" strategy ensures that regardless of _ which_ version of ` .foo ` gets called,
520
+ it ends up eventually forwarding to the actual implementation of ` foo ` , with
521
+ the correct combination of passed arguments and default arguments
522
+
523
+ ``` scala
524
+ UnrolledObj .foo(String , Int ) // forwards to UnrolledObj.foo(String, Int, Boolean)
525
+ UnrolledObj .foo(String , Int , Boolean ) // actual implementation
526
+ UnrolledObj .foo(String , Int , Boolean , Long ) // forwards to UnrolledObj.foo(String, Int, Boolean)
527
+ ```
528
+
529
+ As is the case for ` @unroll ` ed methods on ` trait ` s and ` class ` es, ` @unroll ` ed
530
+ implementations of an abtract method must be final.
531
+
532
+ #### Are Reverse Forwarders Really Necessary?
533
+
534
+ This "double forwarding" strategy is not strictly necessary to support
535
+ [ Backwards Compatibility] ( #backwards-compatibility ) : the "reverse" forwarders
536
+ generated for abstract methods are only necessary when a downstream callsite
537
+ of ` UnrolledObj.foo ` is compiled against a newer version of the original
538
+ ` trait Unrolled ` than the ` object UnrolledObj ` was, as shown below:
539
+
540
+ ``` scala
541
+ trait Unrolled { // version 3
542
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , @ unroll l : Long = 0 ): String = foo(s, n, b)
543
+ // generated
544
+ def foo (s : String , n : Int , b : Boolean ): String = foo(s, n)
545
+ def foo (s : String , n : Int ): String
546
+ }
547
+ ```
548
+ ``` scala
549
+ object UnrolledObj extends Unrolled { // version 2
550
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true ) = s + n + b
551
+ // generated
552
+ def foo (s : String , n : Int ) = foo(s, n, true )
553
+ }
554
+ ```
555
+ ``` scala
556
+ // version 3
557
+ UnrolledObj .foo(" hello" , 123 , true , 456L )
558
+ ```
559
+
560
+ If we did not have the reverse forwarder from ` foo(String, Int, Boolean, Long) ` to
561
+ ` foo(String, Int, Boolean) ` , this call would fail at runtime with an ` AbstractMethodError ` .
562
+ It also will get caught by MiMa as a ` ReversedMissingMethodProblem ` .
563
+
564
+ This configuration of version is not allowed given our definition of backwards compatibility:
565
+ that definition assumes that ` Unrolled ` must be of a greater version than ` UnrolledObj ` ,
566
+ which itself must be of a greater version than the final call to ` UnrolledObj.foo ` . However,
567
+ the reverse forwarders are to fulfill our requirement
568
+ [ All Overrides Are Equivalent] ( #all-overrides-are-equivalent ) :
569
+ looking at ` trait Unrolled // version 3 ` and ` object UnrolledObj // version 2 ` in isolation,
570
+ we find that without the reverse forwarders the signature ` foo(String, Int, Boolean, Long) `
571
+ is defined but not implemented. Such an un-implemented abstract method is something
572
+ we want to avoid, even if our artifact version constraints mean it should technically
573
+ never get called.
430
574
431
575
## Limitations
432
576
433
- 1 . Only the one parameter list of multi-parameter list methods (i.e. curried or taking
434
- implicits) can be ` @unroll ` ed. Unrolling multiple parameter lists would generate a number
435
- of forwarder methods exponential with regard to the number of parameter lists unrolled,
436
- and the generated forwarders may begin to conflict with each other. We can choose to spec
437
- this out and implement it later if necessary, but for 99% of use cases ` @unroll ` ing one
438
- parameter list should be enough. Typically, only one parameter list in a method has default
439
- arguments, with other parameter lists being ` implicit ` s or a single callback/blocks, neither
440
- of which usually has default values.
441
-
442
- 2 . As unrolling generates synthetic forwarder methods for binary compatibility, it is
443
- possible for them to collide if your unrolled method has manually-defined overloads
444
-
445
- 3 . As mentioned earlier, ` @unroll ` ed case classes are only fully binary compatible in Scala 3,
446
- though they are _ almost_ binary compatible in Scala 2. Direct calls to ` unapply ` are binary
447
- incompatible, but most common pattern matching of ` case class ` es goes through a different
448
- code path that _ is_ binary compatible. In practice this should be sufficient for 99% of use
449
- cases, but it does mean that it is possible for code written as below to fail in Scala 2
450
- if a new unrolled parameter is added to the case class and ` .unapply ` is called directly.
451
-
452
- 4 . While ` @unroll ` ed ` case class ` es are fully binary compatible, they are * not* fully
453
- _ source_ compatible, due to the fact that pattern matching requires all arguments to
454
- be specified. This proposal does not change that. Future improvements related to
455
- [ Pattern Matching on Named Fields] ( https://github.com/scala/improvement-proposals/pull/44 )
456
- may bring improvements here. But as we discussed earlier, binary compatibility is generally
457
- more important than source compatibility, and so we do not need to wait for any source
458
- compatibility improvements to land before proceeding with these binary compatibility
459
- improvements.
460
-
461
- 5 . This proposal does not address how macros will derive typeclasses for ` case class ` es, and
462
- whether or not those will be binary/source/semantically compatible. That is up to the
463
- individual macro implementations to decide. e.g., [ uPickle] ( https://github.com/com-lihaoyi/upickle )
464
- has a very similar rule about adding ` case class ` fields, except that field ordering
465
- does not matter. Trying to standardize this across all possible macros and all possible
466
- typeclasses is out of scope
467
-
468
- 6 . ` @unroll ` generates a quadratic amount of generated bytecode as more default parameters
469
- are added: each forwarder has ` O(num-params) ` size, and there are ` O(num-default-params) `
470
- forwarders. We do not expect this to be a problem in practice, as the small size of the
471
- generated forwarder methods means the constant factor is small, but one could imagine
472
- the ` O(n^2) ` asymptotic complexity becoming a problem if a method accumulates hundreds of
473
- default parameters over time. In such extreme scenarios, some kind of builder pattern
474
- (such as those listed in [ Major Alternatives] ( #major-alternatives ) ) may be preferable.
475
-
476
- 7 . ` @unroll ` only supports ` final ` methods. ` object ` methods and constructors are naturally
477
- final, but ` class ` or ` trait ` methods that are ` @unroll ` ed need to be explicitly marked ` final ` .
478
- It has proved difficult to implement the semantics of ` @unroll ` in the presence of downstream
479
- overrides, ` super ` , etc. where the downstream overrides can be compiled against by different
480
- versions of the upstream code. If we can come up with some implementation that works, we can
481
- lift this restriction later, but for now I have not managed to do so and so this restriction
482
- stays.
577
+ ### Only the one parameter list of multi-parameter list methods can be ` @unroll ` ed.
578
+
579
+ Unrolling multiple parameter lists would generate a number
580
+ of forwarder methods exponential with regard to the number of parameter lists unrolled,
581
+ and the generated forwarders may begin to conflict with each other. We can choose to spec
582
+ this out and implement it later if necessary, but for 99% of use cases ` @unroll ` ing one
583
+ parameter list should be enough. Typically, only one parameter list in a method has default
584
+ arguments, with other parameter lists being ` implicit ` s or a single callback/blocks, neither
585
+ of which usually has default values.
586
+
587
+ ### Unrolled forwarder methods can collide with manually-defined overrides
588
+
589
+ This is similar to any other generated methods. We can raise an error to help users
590
+ debug such scenarios, but such name collisions are inevitably possible given how binary
591
+ compatibility on the JVM works.
592
+
593
+ ### ` @unroll ` ed case classes are only fully binary compatible in Scala 3
594
+
595
+
596
+ They are _ almost_ binary compatible in Scala 2. Direct calls to ` unapply ` are binary
597
+ incompatible, but most common pattern matching of ` case class ` es goes through a different
598
+ code path that _ is_ binary compatible. There are also the ` AbstractFunctionN ` traits, from
599
+ which the companion object inherits ` .curried ` and ` .tupled ` members. Luckily, ` unapply `
600
+ was made binary compatible in Scala 3, and ` AbstractFunctionN ` , ` .curried ` , and ` .tupled `
601
+ were removed
602
+
603
+ ### While ` @unroll ` ed ` case class ` es are * not* fully _ source_ compatible
604
+
605
+ This is due to the fact that pattern matching requires all arguments to
606
+ be specified. This proposal does not change that. Future improvements related to
607
+ [ Pattern Matching on Named Fields] ( https://github.com/scala/improvement-proposals/pull/44 )
608
+ may bring improvements here. But as we discussed earlier, binary compatibility is generally
609
+ more important than source compatibility, and so we do not need to wait for any source
610
+ compatibility improvements to land before proceeding with these binary compatibility
611
+ improvements.
612
+
613
+ ### Binary and semantic compatibility for macro-derived derive typeclasses is out of scope
614
+
615
+
616
+ This propsosal does not have any opinion on whether or not macro-derivation is be binary/source/semantically
617
+ compatible. That is up to the
618
+ individual macro implementations to decide. e.g., [ uPickle] ( https://github.com/com-lihaoyi/upickle )
619
+ has a very similar rule about adding ` case class ` fields, except that field ordering
620
+ does not matter. Trying to standardize this across all possible macros and all possible
621
+ typeclasses is out of scope
622
+
623
+ ### ` @unroll ` generates a quadratic amount of generated bytecode as more default parameters are added
624
+
625
+ Each forwarder has ` O(num-params) ` size, and there are ` O(num-default-params) `
626
+ forwarders. We do not expect this to be a problem in practice, as the small size of the
627
+ generated forwarder methods means the constant factor is small, but one could imagine
628
+ the ` O(n^2) ` asymptotic complexity becoming a problem if a method accumulates hundreds of
629
+ default parameters over time. In such extreme scenarios, some kind of builder pattern
630
+ (such as those listed in [ Major Alternatives] ( #major-alternatives ) ) may be preferable.
631
+
632
+ ###` @unroll ` only supports ` final ` methods.
633
+
634
+ ` object ` methods and constructors are naturally
635
+ final, but ` class ` or ` trait ` methods that are ` @unroll ` ed need to be explicitly marked ` final ` .
636
+ It has proved difficult to implement the semantics of ` @unroll ` in the presence of downstream
637
+ overrides, ` super ` , etc. where the downstream overrides can be compiled against by different
638
+ versions of the upstream code. If we can come up with some implementation that works, we can
639
+ lift this restriction later, but for now I have not managed to do so and so this restriction
640
+ stays.
483
641
484
642
### Challenges of Non-Final Methods and Overriding
485
643
@@ -647,52 +805,6 @@ object Unrolled{
647
805
```
648
806
649
807
650
- ### Abstract Methods
651
-
652
- In [ Limitations] ( #limitations ) , I mentioned that ` @unroll ` only supports ` final ` methods.
653
- It is likely possible for abstract methods which are ` @unrolled ` to have concrete forwarder
654
- methods generated on their behalf.
655
-
656
- ``` scala
657
- import scala .annotation .unroll
658
-
659
- trait Unrolled {
660
- def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true ): String
661
- }
662
-
663
- object Unrolled extends Unrolled {
664
- def foo (s : String , n : Int = 1 , b : Boolean = true ) = s + n + b
665
- }
666
- ```
667
-
668
- Unrolls to:
669
-
670
- ``` scala
671
- trait Unrolled {
672
- def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true ): String = foo(s, n)
673
- def foo (s : String , n : Int = 1 ): String = foo(s, n, true )
674
- }
675
-
676
- object Unrolled extends Unrolled {
677
- def foo (s : String , n : Int = 1 , b : Boolean = true ) = s + n + b
678
- }
679
- ```
680
-
681
- As the forwarders are concrete, the implementor of the abstract method does
682
- not need to ` @unroll ` the implementation: they only need to provide the implementation
683
- for the primary method ` def ` and not the forwarders.
684
-
685
- One thing to note is that the ` @unroll ` ed abstract method needs to _ itself_ become a
686
- forwarder method, despite originally being abstract! That is because downstream code
687
- compiled against an old version may define classes which ` extends Unrolled ` and
688
- define a concrete ` def foo(s: String, n: Int = 1): String ` , while other downstream code compiled
689
- against a newer version may define a concrete
690
- ` def foo(s: String, n: Int = 1, b: Boolean = true): String ` . Thus, we need both overloads
691
- of ` foo ` to be forwarders, so that downstream code can override either version and still work.
692
-
693
- This handling for abstract methods is not fully fleshed out or implemented, so I'm
694
- not sure if it can truly be made to work.
695
-
696
808
### Should the Generated Methods be Deprecated or Invisible?
697
809
698
810
It is not clear to me if we should discourage usage of the generated forwarders
@@ -743,7 +855,7 @@ This is not currently implemented in `@unroll`, but would be a straightforward a
743
855
Given this:
744
856
745
857
``` scala
746
- def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , l : Long = 0 ) = s + n + b + l
858
+ def foo (s : String , n : Int = 1 , @ unroll b : Boolean = true , @ unroll l : Long = 0 ) = s + n + b + l
747
859
```
748
860
749
861
There are two ways to do the forwarders. First option, which I used in above, is
@@ -765,7 +877,12 @@ def foo(s: String, n: Int) = foo(s, n, true)
765
877
The first option results in shorter stack traces, while the second option results in
766
878
roughly half as much generated bytecode in the method bodies (though it's still ` O(n^2) ` ).
767
879
768
- For now I chose the first option.
880
+ In order to allow ` @unroll ` ing of [ Abstract Methods] ( #abstract-methods ) , we had to go with
881
+ the second option. This is because when an abstract method is overriden, it is not necessarily
882
+ true that the longest override that contains the implementation. Thus we need to forward
883
+ between the different ` def foo ` overrides one at a time until the override containing the
884
+ implementation is found.
885
+
769
886
770
887
771
888
## Implementation & Testing
0 commit comments