Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add a desugaring pass between the base linker and the optimizer. #5101

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

Merged
merged 2 commits into from
Feb 6, 2025

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Jan 4, 2025

Previously, the emitters and the optimizer all had to perform the same desugaring for LinkTimeProperty nodes. Instead, we now perform the desugaring in a dedicated phase, after the base linker.

The reachability analysis records whether each method needs desugaring or not. We mark those that do so that the desugaring pass knows what to process. Methods that do not require desugaring are not processed, and so incur no additional cost.

No caching is perfored in Desugarer. It processes so few methods that caching makes it (slightly) slower.

The machinery is heavy. It definitely outweighs the benefits in terms of duplication for LinkTimeProperty alone. However, the same machinery will be used to desugar NewLambda nodes. This commit serves as a stepping stone in that direction.


Alternative to #5096.

@sjrd sjrd force-pushed the desugaring-separate-phase branch from ae9fdc5 to a738094 Compare January 9, 2025 10:47
@sjrd sjrd marked this pull request as ready for review January 9, 2025 10:48
@sjrd sjrd requested a review from gzm0 January 9, 2025 10:48
@sjrd
Copy link
Member Author

sjrd commented Jan 9, 2025

This is now in a reviewable shape, I think.

The second commit might be over-engineered, though. 😅

@gzm0
Copy link
Contributor

gzm0 commented Jan 17, 2025

I've had a look at this and the PRs on top of this. I think it fits the overall linker design reasonably nicely.

Some higher level comments:

  • IMO the way we flag for desugaring is not very nice at the moment. I'm wondering if just member methodsToDesugar: Map[MemberNamespace, Set[MethodName]] in LinkedClass would do the trick.

  • I'm wondering if we could do More DCE wrt case object equality test #2396 in this phase (fold _ === LoadModule(_) to false in case the module is not instantiated, the analyzer could flag for "desugaring" if it sees this shape, which it will need to recognize anyways).

  • How do Knowledge accessor utilities #5099 / Refactor: Introduce common caching utilities. #5098 factor into this picture? (I have not looked at them in very much detail).

  • The second commit might be over-engineered, though. 😅

    I think there is value in having a better structured configuration value. However, I would not put any validation logic into it (and just "hard-code" that in the checkers, just like it is now).

I'll likely proceed with a more detailed review when I have time, unless you feel comparing this to #5096 needs more higher level discussion.

@sjrd
Copy link
Member Author

sjrd commented Jan 17, 2025

  • IMO the way we flag for desugaring is not very nice at the moment. I'm wondering if just member methodsToDesugar: Map[MemberNamespace, Set[MethodName]] in LinkedClass would do the trick.

I can try something like that.

  • I'm wondering if we could do More DCE wrt case object equality test #2396 in this phase (fold _ === LoadModule(_) to false in case the module is not instantiated, the analyzer could flag for "desugaring" if it sees this shape, which it will need to recognize anyways).

I don't think we can. The problem is that we don't see the shape _ === Load module(_) at this stage. We see something like LoadModule.equals(y). We have to be able to inline the equals before we see the shape. That's not something we can do at the desugaring phase. We need the optimizer to do it.

They don't. Those two PRs are unrelated. (I was prompted to look into that because I expected to have to write one more cache with clear after run for the desugaring phase. But it turned out it was so fast that a cache makes it worse, and so there is no relationship with the cache PRs.)

  • The second commit might be over-engineered, though. 😅

    I think there is value in having a better structured configuration value. However, I would not put any validation logic into it (and just "hard-code" that in the checkers, just like it is now).

So basically making it a bare enum-like thing with an ordering, and have the checkers know the acceptable ranges? That's what I started with, but found it unwieldy. I can revert to that state to give a comparison.

I'll likely proceed with a more detailed review when I have time, unless you feel comparing this to #5096 needs more higher level discussion.

No I think it's fine. It seems that this variant wins over #5096 overall.

@gzm0
Copy link
Contributor

gzm0 commented Jan 18, 2025

So basically making it a bare enum-like thing with an ordering, and have the checkers know the acceptable ranges? That's what I started with, but found it unwieldy. I can revert to that state to give a comparison.

Maybe consider giving the enum ordering to make the range checks easier to grok. Or even expand the features on top of the checker (val newLambdaAllowed = ...).

IMHO the important part is that the logic of what is allowed where is in the checkers and not the phase enum.

@sjrd sjrd force-pushed the desugaring-separate-phase branch from a738094 to 808638e Compare January 18, 2025 12:24
@sjrd
Copy link
Member Author

sjrd commented Jan 18, 2025

I pushed a different version of the CheckingPhase refactoring. I removed logic from CheckingPhase; it is now a pure enum. However, since ClassDefChecker and IRChecker must agree on "which feature is available when", I still kept that logic in a common place, which is now FeatureSet. FeatureSet is private[checked], so it is only an implementation detail of the two checkers. From the checkers' perspective, we use "logical" features now.

This should make things clearer while leaving the responsibility to the checkers, as you mentioned (although factored out in FeatureSet.supportedBy).

@sjrd
Copy link
Member Author

sjrd commented Jan 18, 2025

  • IMO the way we flag for desugaring is not very nice at the moment. I'm wondering if just member methodsToDesugar: Map[MemberNamespace, Set[MethodName]] in LinkedClass would do the trick.

I can try something like that.

I pushed a commit thast switches to that scheme. I'm not sure I like it better, though. It bothers me that we have fields in LinkedClass and LinkedTopLevelExport that are only useful between BaseLinker and Desugarer.

@gzm0
Copy link
Contributor

gzm0 commented Jan 19, 2025

It bothers me that we have fields in LinkedClass and LinkedTopLevelExport that are only useful between BaseLinker and Desugarer.

Yes, I agree, this is not nice in our design. However, I don't think this is anything new: LinkedClass contains a lot of fields only useful for the frontend pipeline (e.g. fieldsRead, all the module dependency information).

Arguably, this is something we should fix. Ironically, one of the easiest way I know is using structural subtyping like in go (producer exposes concrete type, consumer defines an interface of what it needs). Unfortunately, that's not really available to us in Scala.

Other than that, I find the alternative much cleaner :-/ Do you think it makes sense to figure out how to better decouple the fields we have for individual phases?

@sjrd
Copy link
Member Author

sjrd commented Jan 19, 2025

However, I don't think this is anything new: LinkedClass contains a lot of fields only useful for the frontend pipeline (e.g. fieldsRead, all the module dependency information).

Good point. The new desugaring info does not make the situation qualitatively worse.

This is probably fine as is, then.

We can try to improve that afterwards.

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

More in-depth review of check refactoring.

How big is the effort to split this into a separate PR to ease review?

hasRuntimeTypeInfo,
fieldsRead,
staticFieldsRead,
staticDependencies,
Copy link
Contributor

Choose a reason for hiding this comment

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

Note to self: This will not be correct anymore when we add the NewLambda desguaring: there will be a dependency on the class.

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

Review of the new phase (as far as possible given the current commit situation).

@@ -105,6 +107,13 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) {

private[frontend] object BaseLinker {

/** Takes a ClassDef and DCE infos to construct a stripped down LinkedClass.
*/
private[frontend] def refineClassDef(classDef: ClassDef, version: Version,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need this extra method?

newJSPropertyDef.copy()(jsPropertyDef.version)(newJSPropertyDef.pos)
}

override def transformTopLevelExportDef(exportDef: TopLevelExportDef): TopLevelExportDef = {
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC this override is (now) unnecessary?

@sjrd
Copy link
Member Author

sjrd commented Jan 19, 2025

How big is the effort to split this into a separate PR to ease review?

I can do that, but probably not before Wednesday.

@gzm0
Copy link
Contributor

gzm0 commented Jan 19, 2025

I can do that, but probably not before Wednesday.

No worries. I will not have time before that for sure.

@sjrd
Copy link
Member Author

sjrd commented Jan 25, 2025

Rebased on top of #5120.

@sjrd sjrd force-pushed the desugaring-separate-phase branch from b21e091 to a977798 Compare January 25, 2025 13:13
@sjrd sjrd force-pushed the desugaring-separate-phase branch from a977798 to 2820d22 Compare February 3, 2025 05:04
@sjrd sjrd requested a review from gzm0 February 3, 2025 05:04
@sjrd
Copy link
Member Author

sjrd commented Feb 3, 2025

Rebased. This is now back to being standalone and re-reviewable.

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

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

Just one comment regarding the name of desugaringInfo.

@@ -89,3 +92,19 @@ final class LinkedClass(

def fullName: String = className.nameString
}

object LinkedClass {
final class DesugaringInfo(
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs somewhere a comment at the very least what the info is. (the fact that desugaring is required).

Otherwise this IMO reads more like "information needed/useful for desugaring".

Ideally this of course goes into the names, but I realize that methodsNeedingDesugaring is probably a bit too long :-/

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 only we had Latin's gérondif 😁 We could call it AbSaccharoTemperandum 😅

Perhaps DesugaringRequirements?

@sjrd sjrd force-pushed the desugaring-separate-phase branch from 2820d22 to 4f75c5a Compare February 5, 2025 09:06
@sjrd
Copy link
Member Author

sjrd commented Feb 5, 2025

I went for DesugaringRequirements.

Also I changed how we construct instances of DesugaringRequirements, in a way that provides a fast isEmpty test. This allows Desugarer to entirely bypass classes that need no desugaring. It's now running in 3.5 ms on my machine.

@sjrd sjrd requested a review from gzm0 February 5, 2025 09:09
Previously, the emitters and the optimizer all had to perform the
same desugaring for `LinkTimeProperty` nodes. Instead, we now
perform the desugaring in a dedicated phase, after the base
linker.

The reachability analysis records whether each method needs
desugaring or not. We mark those that do so that the desugaring
pass knows what to process. Methods that do not require
desugaring are not processed, and so incur no additional cost.

No caching is performed in `Desugarer`. It processes so few
methods that caching makes it (slightly) *slower*.

The machinery is heavy. It definitely outweighs the benefits in
terms of duplication for `LinkTimeProperty` alone. However, the
same machinery will be used to desugar `NewLambda` nodes. This
commit serves as a stepping stone in that direction.
@sjrd sjrd force-pushed the desugaring-separate-phase branch from 4f75c5a to 1987d87 Compare February 6, 2025 13:09
private def desugarClass(linkedClass: LinkedClass): LinkedClass = {
import linkedClass._

if (desugaringRequirements.isEmpty) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

@sjrd sjrd merged commit 76f7be7 into scala-js:main Feb 6, 2025
3 checks passed
@sjrd sjrd deleted the desugaring-separate-phase branch February 6, 2025 15:46
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.

2 participants