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 29 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

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
@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

…yper

At typer, when transparent inline methods are expanded, unroll forwarders
are not yet generated.

So if we define forwarders for the first time, in the same compilation
unit that inlines a call to the forwarder, then it will fail.

Perhaps we can detect this and suspend the unit, otherwise it would seem
unreasonable complexity to generate forwarders in typer.
@bishabosha
Copy link
Member Author

bishabosha commented Oct 8, 2024

Just thought of another example that is problematic - in incremental compilation - a transparent inline method, which will resolve to an unrolled forwarder, is inlined in the same compilation run as the unrolled method. The forwarders will not yet be generated at typer when transparent inline is expanded.

In 932d797 I intercept this specific case in completing an inline body annotation - either reporting an error when the problematic method is in the same compilation unit, or suspending otherwise.

@bishabosha
Copy link
Member Author

bishabosha commented Oct 8, 2024

I think we should probably think carefully about the intention and semantics of SourceInvisible - it might have to play into macro checking as well - e.g. it allows definitions to go from visible to invisible and continue being resolved from TASTy - i.e. exactly what is needed for @unroll, but is it problematic for other cases?

SourceInvisible of course is not visible from source, but it is visible from inlined methods that previously compiled when it was visible.

e.g. maybe there should be a check to ensure that there is at least one visible overload in the same owner - in which case maybe it could be renamed InvisibleOverload

@bishabosha bishabosha force-pushed the sip-61-unroll branch 2 times, most recently from c45af2b to 2dcbaa4 Compare October 8, 2024 15:40
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
5 participants