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 5 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)

fixes #21728

@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
@bishabosha
Copy link
Member Author

bishabosha commented Oct 7, 2024

I was talking to @sjrd today, and there is still a problem with Invisible - we have to consider the case of an inline method that calls a method, and then in V2 they add an unrolled parameter. The TASTy for the inline method when spliced will still try to resolve the old method by name+signature (i.e. try to resolve the new invisible forwarder) and fail.

@lihaoyi
Copy link
Contributor

lihaoyi commented Oct 7, 2024

How about if we leave the synthetic overloads visible. They're meant to be equivalent semantically - it should not matter which one ends up getting called. That's the current manual status quo and it works well enough.

@sjrd
Copy link
Member

sjrd commented Oct 7, 2024

That would definitely have user-visible consequences. For starters, type inference gets worse as soon as a method has overloads: you don't get expected types, so you don't get lambda param type inference, and implicit conversions may be inserted later or sooner than otherwise.

If we need to go that far, it's a language design change compared to the SIP, so we need to go back there. But I don't think we need to do that; if Invisible does not have the semantics we want, we can introduce a new TASTy flag with the semantics we want.

@lihaoyi
Copy link
Contributor

lihaoyi commented Oct 7, 2024

A new flag sounds reasonable if it's possible on the implementation side

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

bishabosha commented Oct 7, 2024

I added a test in ae4ceba that fails to resolve a forwarder when inlining a transparent inline method, then I fixed the test by introducing a new flag SourceInvisible (SOURCEINVISIBLE in tasty) that is identical to Invisible, except in the case of resolving SELECTin from TASTy

perhaps we can fold the behaviour of SourceInvisible into Invisible, depending on discussion

@bishabosha
Copy link
Member Author

Green ci again

@lihaoyi
Copy link
Contributor

lihaoyi commented Jan 9, 2025

Bumping reviewers @sjrd @lrytz, since it's been a few weeks

Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I didn't look at transform/UnrollDefinitions.scala in detail.

Is there anything that should be done in MiMa, or in tasty-mima?

@bishabosha
Copy link
Member Author

I'll work on the feedback tonight

@bishabosha
Copy link
Member Author

I've added in b894262 support for //> using target.platform jvm|scala-js in vulpix tests - which filter out files not matching the "test platform" - which also allows to have a separate check file for each platform.

If this is acceptable, should it be lifted to a separate PR?

@bishabosha
Copy link
Member Author

bishabosha commented Jan 18, 2025

@lrytz All comments addressed - CI green
@Gedochao @WojciechMazur who else should look at this?

@Gedochao
Copy link
Contributor

@bishabosha we'll discuss this on this week's Scala Core

@Gedochao
Copy link
Contributor

This is planned to be included in 3.7.0.
Waiting for one last round of review (cc @sjrd)

@lrytz
Copy link
Member

lrytz commented Jan 22, 2025

Is there anything that should be done in MiMa, or in tasty-mima?

@bishabosha ping on that question

@bishabosha
Copy link
Member Author

bishabosha commented Jan 23, 2025

Is there anything that should be done in MiMa, or in tasty-mima?

for MiMa, the forwarders should look like ordinary scala methods (except with invisible flag), and will be present in TASTy/bytecode as usual.

For tasty-mima - there is the new semantic of the Invisible flag which is that it should be visible when resolved from the TASTy of an inline method.

@sjrd
Copy link
Member

sjrd commented Jan 24, 2025

I don't think we need anything in tasty-mima. tasty-query ignores the Invisible flag, which corresponds to the new semantics, as far as it is concerned.

@sjrd
Copy link
Member

sjrd commented Jan 24, 2025

@bishabosha Is the git history relevant? Or should it all be squashed to help future git blames?

@bishabosha
Copy link
Member Author

Is the git history relevant? Or should it all be squashed to help future git blames?

there is perhaps 3-commit split - the initial commit which is copy-paste the com-lihaoyi/unroll code, commit 2 which is "the rest of the work" and perhaps a third commit which adds the vulpix directive parsing stuff

@sjrd
Copy link
Member

sjrd commented Jan 24, 2025

I'll let you decompose as you see fit. Then we can merge.

bishabosha and others added 5 commits January 26, 2025 15:22
also copy tests as sbt-scripted tests.

Co-authored-by: Jamie Thompson <[email protected]>
Co-authored-by: Li Haoyi <[email protected]>
- add documentation page
- move before pickling, fix errors due to unpickling select of Invisible definitions and incorrect spans
- forwarders now only call the original method.
- detect in posttyper which compilation units have unrolled definitions
- detect clashes with forwarders and existing definitions
- check for illegal uses of @unroll
- implementation restriction: no unroll for trait parameters
- unlink replaced definitions
- check for default parameter
- fix invalid pattern in generateFromProduct
- sbt-test/scripted: fork when running with unmanaged classpaths
- update stdlibExperimentalDefinitions.scala
- skip reflection test on scala.js
- require final methods, remove special treatment of abstract methods
- fix order of printing in test
- better error when multiple parameter lists with unroll
- Move sbt-scripted tests to vulpix suites
- now invisible members are visible in typer when resolving SELECTin from TASTy.
- add sbt-test/tasty-compat test to demonstrate when inline method calls forwarder
- add check files for run/unroll tests
- refactorings
- test clause interleaving and value class
- add basic support for `//> using target.platform jvm|scala-js` in Vulpix.
- if the directive is present, filter out files in compilation that dont match
  the test platform, also add a suffix to the expected check-file name.
- duplicate UnrollTestPlatformSpecific files for jvm and scala-js platforms,
  deleting the reflection code in scala-js version.
- remove directives from compilation tests not listened to by vulpix.
@bishabosha
Copy link
Member Author

@sjrd I squashed the commits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-minor-release This PR cannot be merged until the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement SIP-61 @unroll annotation
7 participants