Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SIP-61 @unroll annotation #21693

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

bishabosha
Copy link
Member

@bishabosha bishabosha commented Oct 2, 2024

Still need to write documentation,
Documentation is written also

I am doing this work on behalf of @lihaoyi

The main implementation follows com-lihaoyi/unroll but with some changes:

  • @unroll annotation is @experimental
  • run before pickling
  • increase validation checks for incorrect usage of @unroll
  • Underscore not EmptyTree in pattern match default case
  • ensure correct spans in TASTy
  • ensure symbols are correctly substituted in types

There is one main library addition: scala.annotation.unroll, i.e. the @unroll annotation that appears on parameters,

commits are a bit of a mess - a lot of churn

Edit: removed changes:

  • reuse the symbol when generating the "forwarder" of an abstract method.
  • infer override when overriding trait methods that "appear" abstract in source code, but were actually implemented by the unroll annotation
  • do not add the Invisible flag to abstract unrolled methods - this means they can actually still be visible in separately compiled compilation units (because unrolling runs before pickler now)
  • Internal annotation scala.annotation.internal.AbstractUnroll - marker used to indicate that an unrolled abstract method was abstract in source code (unrolling phase actually provides a default implementation) - this enables downstream implementers to avoid the override flag. (Maybe we shouldn't allow this convenience?)
  • Internal annotation scala.annotation.internal.UnrollForwarder - marker on any generated forwarder - which is used for special casing TASTy pickling of method calls to the forwarders
  • because forwarders get the Invisible flag, this makes them impossible to select from TASTy normally (via SELECTin), (invisible def are filtered out "before typer") so I intercept the Select trees to be TERMREFdirect, and then restore them to Select after TASTY. perhaps someone has a better idea, or we could change the resolution rules for Invisible? or invent a new TASTy node? (I also tried generating a Ident tree rather than a Select, but this had a type error)

@lihaoyi
Copy link
Contributor

lihaoyi commented Oct 3, 2024

@bishabosha some high level comments:

  1. We ended up removing the abstract def support from the SIP (SIP-61 - Unroll Default Arguments for Binary Compatibility improvement-proposals#78) since we weren't confident in the semantics, so let's disable it in the implementation as well and raise an error

  2. It's not obvious to me from the tests, but is it possible to raise errors for the following cases:

    • @unroll on an invalid statement (e.g. on a val, or on a type foo: @unroll
    • @unroll on a non-final class/trait method: @unroll and its delegation model generally only works with final methods, and cannot support overrides with things getting wonky. object methods are final by default so we don't need to worry, but we should ask people to mark trait and class methods as final if they want to use @unroll on them
  3. The original compiler plugin didn't have support for trait parameter lists. Would that be easy to add? It's probably not critical, but would be nice to have for consistency

@bishabosha
Copy link
Member Author

Right, I should have noted that the abstract method support should have been dropped - now it is, I will push further commits with more invalidation checks, and see if trait constructor unroll can work

@bishabosha
Copy link
Member Author

bishabosha commented Oct 3, 2024

@lihaoyi there isn't a way to support trait constructor parameters that isn't a rewrite that a user could do manually, so I think this is unexplored territory - e.g. providing default implementations in bytecode for param accessors of traits

All the other concerns were addressed in above commits

@lihaoyi
Copy link
Contributor

lihaoyi commented Oct 4, 2024

@lihaoyi there isn't a way to support trait constructor parameters that isn't a rewrite that a user could do manually, so I think this is unexplored territory - e.g. providing default implementations in bytecode for param accessors of traits

All the other concerns were addressed in above commits

Sounds good, let's skip trait params for now.

Anyone from the Scala 3 side able to review the code itself and the integration into the scala 3 codebase?

@bishabosha
Copy link
Member Author

bishabosha commented Oct 4, 2024

one implication I guess is that someone might not plan to use @unroll when they first made the API, so not make the method final, and then are potentially restricted from introducing @unroll in the future, because some client may have added an override. I guess we should include in our binary compatibilty documentation that you should make (all?) methods final

or we put in documentation to pre-introduce @deprecatedOverriding for this?

@lihaoyi
Copy link
Contributor

lihaoyi commented Oct 4, 2024

I think forcing final is probably necessary, either explicitly by asking the user to put the keyword, or implicitly by having the compiler add it.

At least as currently designed, if code can override an @unrolled method, it can result in different callsites running different logic depending on what version of the @unrolled API they were compiled against, which violates all sorts of expectations.

So for case classes and object methods and constructors, things are already final so the requirement doesn't make a difference.

For class and trait methods, if they're not final but nobody is overriding them, then having the upstream API add final changes nothing. If they are not final but someone is overriding them, then adding final upstream is the difference between a loud JVM linkage error and a silent misbehavior, and I think the loud JVM linkage error is the preferable failure mode

@lihaoyi
Copy link
Contributor

lihaoyi commented Oct 4, 2024

CC @lrytz since you're the manager, not sure if you should be reviewing this?

@Gedochao Gedochao added the needs-minor-release This PR cannot be merged until the next minor release label Oct 4, 2024
@Gedochao Gedochao requested review from lrytz and sjrd October 4, 2024 12:25
@bishabosha
Copy link
Member Author

bishabosha commented Oct 4, 2024

@Gedochao I thought experimental stuff can go in a patch? - I guess the tasty peculiarity - but that will have experimental flag in the tasty if used

@Gedochao
Copy link
Contributor

Gedochao commented Oct 4, 2024

@Gedochao I thought experimental stuff can go in a patch? - I guess the tasty peculiarity - but that will have experimental flag in the tasty if used

Ah, if it's behind a flag then it's fine, my bad.

@Gedochao Gedochao removed the needs-minor-release This PR cannot be merged until the next minor release label Oct 4, 2024
@bishabosha bishabosha added the needs-minor-release This PR cannot be merged until the next minor release label Oct 4, 2024
@bishabosha
Copy link
Member Author

bishabosha commented Oct 4, 2024

@Gedochao I added the label back - this would not be correct to backport to 3.5.2 (no block at the tasty level), so it should be in 3.6.0 minimum

@bishabosha
Copy link
Member Author

Just added in the documentation

@bishabosha
Copy link
Member Author

bishabosha commented Oct 5, 2024

I added commit b1c2ec0 which removes the TASTy hack. The commit changes the way forwarders are generated - i.e. they all call the original unrolled method, (rather than "telescoping" and calling the next invisible forwarder).

Since we no longer need to resolve calls to Invisible methods, the TASTy hack is no longer needed

@bishabosha bishabosha removed the needs-minor-release This PR cannot be merged until the next minor release label Oct 5, 2024
Traits are forbidden in source code from having secondary constructors,
which is what the current transform would generate.

Trait parameters are encoded in concrete implementing classes as getter methods,
perhaps unroll could provide default implementations, but this is unexplored.
Now, all forwarders directly call the unrolled method.
This means that there is no need to resolve an invisible method,
so the "hack" using TERMREFdirect is no longer needed.

This increases bytecode size but reduces stack depth.

Also removed scala.annotation.internal.UnrolledForwarder,
as it is only needed for the TASTy hack.
Also standardise error messages as Declaration Errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants