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

Fix #19951: Align TASTy with the Java annotation model. #20539

Merged
merged 2 commits into from
Jul 3, 2024

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Jun 10, 2024

Scala annotations are classes, with a real constructor, which has a real signature where order is relevant but names are irrelevant.

On the contrary, Java annotations are interfaces, without any real constructors. The names of "fields" are relevant, whereas their order is irrelevant.

As illustrated by #19951, trying to shoehorn Java annotations into the Scala annotation model is not sustainable, and breaks in real ways. Therefore, in this commit we align how Java annotations are stored in TASTy with the Java annotation model.

During pickling:

  • Selection of the constructor is pickled without a signature.
  • Default arguments are dropped.
  • (Due to the parent commit, all arguments are NamedArgs at this point.)

During unpickling:

  • Selection of the constructor resolves to the unique constructor (instead of complaining because a signature-less SELECT should not resolve to a member with a signature).
  • Arguments to the constructor are reordered and extended with defaults to match the target constructor; we can do this because all the arguments are NamedArgs.

For backward compatibility, during unpickling:

  • If we read a SELECTin for a Java annotation constructor, we disregard its signature and pretend it was a SELECT.
  • We adapt arguments in best-effort way if not all of them are NamedArgs.

Still needs new tests. CI only for now.

@sjrd sjrd linked an issue Jun 10, 2024 that may be closed by this pull request
val sig = tree.tpe.signature
// #19951 The signature of a constructor of a Java annotation is irrelevant
val sig =
if name == nme.CONSTRUCTOR && tree.symbol.owner.is(JavaAnnotation) then Signature.NotAMethod
Copy link
Member

Choose a reason for hiding this comment

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

The change to signatures looks like the only part that couldn't be backported to 3.3 (because it would break the existing unpickler logic). If we agree that we'd like to backport the rest to 3.3, maybe this change should be done in a separate PR after we've validated that the other changes work together?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we leave this out, we also need to leave out the other change in this file. Otherwise we'll pickle constructor calls where the actual arguments do not match the signature, so an older compiler will also choke on it.

That said, as the latest push shows, even explicitly expanding all NamedArgs (the first commit) will break the current version of WartRemover. So I don't think we can backport this to 3.3.x at all.

Copy link
Member

Choose a reason for hiding this comment

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

I guess this will need to be discussed more, but I think breaking WartRemover could be OK if we allow enough time for upstream to release a fixed version before we release a 3.3.x with the backport in. It seems like this would lead to less issues in practice in the long term.

@sjrd sjrd force-pushed the java-annot-fields-by-name branch from 67b3930 to 9e2eb47 Compare June 10, 2024 12:25
@sjrd sjrd marked this pull request as ready for review June 10, 2024 12:26
@sjrd sjrd requested a review from smarter June 10, 2024 12:27
@sjrd
Copy link
Member Author

sjrd commented Jun 10, 2024

/cc @xuwei-k for the changes to the test at
https://github.com/scala/scala3/pull/20539/files#diff-3514058c43797515edabf8b5ca77c277e9d338f82d0ad7f9b022db83e27fd674
which shows that WartRemover's current implementation will be broken by this change.

@sjrd sjrd changed the title CI ONLY Fix #19951: Align TASTy with the Java annotation model. Fix #19951: Align TASTy with the Java annotation model. Jun 10, 2024
@sjrd sjrd added the needs-minor-release This PR cannot be merged until the next minor release label Jun 10, 2024
@sjrd
Copy link
Member Author

sjrd commented Jun 10, 2024

@Gedochao If we merge this now, we must backport it to 3.5.0-RC2. The alternative is to wait for 3.6.0.

@Gedochao
Copy link
Contributor

@Gedochao If we merge this now, we must backport it to 3.5.0-RC2. The alternative is to wait for 3.6.0.

Sounds like a job for Core 🦸
Let's discuss and decide later this week.

Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

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

Could you add a test in sbt-test/scala3-compat/ to check that we can read annotations produced by an older Scala 3 as best as we can?

(if !d.exists then lookupInSuper else d).asSeenFrom(prefix)
if owner.is(JavaAnnotation) && name == nme.CONSTRUCTOR then
// #19951 Fix up to read TASTy produced before 3.5.0 -- ignore the signature
ownerTpe.decl(name).asSeenFrom(prefix)
Copy link
Member

Choose a reason for hiding this comment

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

Java classes get a dummy extra constructor when parsing from sources:

// A dummy first constructor is needed for Java classes so that the real constructors see the
// import of the companion object. The constructor has parameter of type Unit so no Java code
// can call it.
// This also avoids clashes between the constructor parameter names and member names.
if (needsDummyConstr) {
if (constr1 == EmptyTree) constr1 = makeConstructor(List(), Nil, Parsers.unimplementedExpr)
stats1 = constr1 :: stats1
constr1 = makeConstructor(List(UnitTpt()), tparams, EmptyTree, fakeFlags)

So I wonder if this check is enough in the case where we're unpickling from tasty a reference to a java annotation that is also being passed to the compiler as source in the current run (this would be good to test anyway). The dummy constructor has the flag PrivateLocal set so we could filter that out explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hum. I'm happy to filter out PrivateLocal. That said I'm not sure how we can construct such a scenario in practice. Since the Scala class uses the annotation, it must be compiled together with or later than the annotation. But then how would we have the Scala class from TASTy but the Java class from source? Perhaps in some complicated incremental compilation scenario with mixed compilation? 🤔

@smarter smarter assigned sjrd and unassigned smarter Jun 10, 2024
@sjrd sjrd force-pushed the java-annot-fields-by-name branch from 9e2eb47 to 67ad9a2 Compare June 10, 2024 13:44
@sjrd sjrd requested a review from smarter June 10, 2024 13:44
@sjrd sjrd assigned smarter and unassigned sjrd Jun 10, 2024
@sjrd
Copy link
Member Author

sjrd commented Jun 10, 2024

I believe I addressed all the comments so far.

@smarter smarter removed their assignment Jun 10, 2024
@dwijnand dwijnand assigned sjrd and unassigned dwijnand Jun 10, 2024
@dwijnand dwijnand removed their request for review June 10, 2024 14:03
@sjrd sjrd force-pushed the java-annot-fields-by-name branch from 67ad9a2 to 60fa4c5 Compare June 10, 2024 15:21
@Gedochao Gedochao added this to the Future versions milestone Jun 14, 2024
@Gedochao
Copy link
Contributor

This has been decided to be included in the (TBA) 3.6.0 release.

sjrd added 2 commits July 3, 2024 14:22
The Java model of annotations is unordered and name-based. Even
though we typecheck things from source with a particular ordering,
semantically we must always use `NamedArg`s to match the Java
model.
Scala annotations are classes, with a real constructor, which has
a real signature where order is relevant but names are irrelevant.

On the contrary, Java annotations are interfaces, without any real
constructors. The names of "fields" are relevant, whereas their
order is irrelevant.

As illustrated by scala#19951, trying to shoehorn Java annotations into
the Scala annotation model is not sustainable, and breaks in real
ways. Therefore, in this commit we align how Java annotations are
stored in TASTy with the Java annotation model.

During pickling:

* Selection of the constructor is pickled without a signature.
* Default arguments are dropped.
* (Due to the parent commit, all arguments are `NamedArg`s at this point.)

During unpickling:

* Selection of the constructor resolves to the unique constructor
  (instead of complaining because a signature-less `SELECT` should
  not resolve to a member with a signature).
* Arguments to the constructor are reordered and extended with
  defaults to match the target constructor; we can do this because
  all the arguments are `NamedArg`s.

For backward compatibility, during unpickling:

* If we read a `SELECTin` for a Java annotation constructor, we
  disregard its signature and pretend it was a `SELECT`.
* We adapt arguments in best-effort way if not all of them are
  `NamedArg`s.
@sjrd sjrd force-pushed the java-annot-fields-by-name branch from 60fa4c5 to 6730f19 Compare July 3, 2024 12:22
@sjrd
Copy link
Member Author

sjrd commented Jul 3, 2024

Rebased to be in the 3.6.0 cycle.

@sjrd sjrd enabled auto-merge July 3, 2024 12:24
@sjrd sjrd merged commit 06c6a71 into scala:main Jul 3, 2024
23 checks passed
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.

Adding a method to a Java annotation breaks tasty-compatibility
5 participants