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(isolated_declarations): Emit computed properties when they are well known symbols #4099

Conversation

MichaelMitchell-at
Copy link
Contributor

@MichaelMitchell-at MichaelMitchell-at commented Jul 7, 2024

Fixes #4016.

To fully comply with the reference behavior of the TypeScript compiler, we need to be able to detect the case when Symbol or globalThis.Symbol do not refer to the global Symbol. I achieve this by creating a similar structure to ScopeTree called GlobalSymbolBindingTracker (will take suggestions for a better name) and have it visit the entire program before we begin transformation. We can keep track of which computed properties reference non-global Symbol by storing a set of their spans, which should each be uniquely identifying since we're in the context of a single file.
Later, if and when we transform the computed property into a type declaration, we can refer back to this set to see if it should be allowed.

Copy link

graphite-app bot commented Jul 7, 2024

Your org has enabled the Graphite merge queue for merging into main

Add the label “merge” to the PR and Graphite will automatically add it to the merge queue when it’s ready to merge. Or use the label “hotfix” to add to the merge queue as a hot fix.

You must have a Graphite account and log in to Graphite in order to use the merge queue. Sign up using this link.

@MichaelMitchell-at MichaelMitchell-at marked this pull request as draft July 7, 2024 22:44
@github-actions github-actions bot added the A-isolated-declarations Isolated Declarations label Jul 7, 2024
Copy link

codspeed-hq bot commented Jul 7, 2024

CodSpeed Performance Report

Merging #4099 will not alter performance

Comparing MichaelMitchell-at:well_known_symbols (2045470) with main (02d5637)

Summary

✅ 29 untouched benchmarks

@MichaelMitchell-at MichaelMitchell-at force-pushed the well_known_symbols branch 3 times, most recently from 79a5d02 to 224c490 Compare July 8, 2024 03:10
@Dunqing
Copy link
Member

Dunqing commented Jul 8, 2024

Have you looked at scope.rs?

@MichaelMitchell-at

This comment was marked as outdated.

@MichaelMitchell-at

This comment was marked as outdated.

@Dunqing
Copy link
Member

Dunqing commented Jul 8, 2024

Can you explain more what the difference between this approach and typescript's behavior?

@MichaelMitchell-at
Copy link
Contributor Author

Can you explain more what the difference between this approach and typescript's behavior?

With an alternative approach this code would have an isolated declarations error:

export function foo(Symbol: any): void {
  const x = {[Symbol.iterator]: 1};
}

because Symbol is bound in the current scope, and we are trying to use Symbol.* as a property key. In TypeScript this wouldn't be an isolated declarations error because x clearly has no effect on emit.

@Dunqing
Copy link
Member

Dunqing commented Jul 9, 2024

Let's try this alternative approach.

@MichaelMitchell-at
Copy link
Contributor Author

Ok, I might not get to this until next weekend

@MichaelMitchell-at MichaelMitchell-at force-pushed the well_known_symbols branch 4 times, most recently from cff5ab1 to 8e97f1d Compare July 15, 2024 03:52
@MichaelMitchell-at
Copy link
Contributor Author

@Dunqing actually I thought of an approach that matches the same behavior as TypeScript, but I'm not sure if the performance/memory usage would be considered acceptable by the project. Please take a look.

@MichaelMitchell-at MichaelMitchell-at marked this pull request as ready for review July 15, 2024 03:57
@Boshen Boshen requested a review from Dunqing July 15, 2024 04:07
@MichaelMitchell-at MichaelMitchell-at force-pushed the well_known_symbols branch 3 times, most recently from 4ccf44d to 14de332 Compare July 15, 2024 04:43
Comment on lines +84 to +135
// Collect information about global Symbol usage within computed
// properties before performing any transformations.
self.global_symbol_binding_tracker.visit_program(program);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could probably avoid allocations for this most of the time by first checking if there is both an occurrence of overriding the global Symbol and references to Symbol anywhere in the program, not necessarily in the same scope, but I'm not sure what the cost of that extra traversal would be.

Copy link
Member

@Dunqing Dunqing Jul 24, 2024

Choose a reason for hiding this comment

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

Is it possible to check on demand? For example, we initially only check for the root scope. When we transform the function we will check the function. This will only check for declarations that will be exported

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly, I will think about it some more. I think one of the main challenges comes just from organizing the code and performing state management. It feels like similar to with ScopeTree we would need to tell GlobalSymbolBindingTracker when it is entering or leaving a scope, but the logic will diverge from ScopeTree (otherwise we would have just used ScopeTree for this purpose), which may result in the code looking more confusing.

(Just voicing my thoughts out loud) I think what feels redundant with my current approach is that the short-circuit optimizations I take are making assumptions about how the tree will be traversed for emitting declarations, so that logic kind of feels like it's being reproduced in two places. The code could be potentially cleaner if that logic could be pulled out. What I imagine is something like alternative Visitor traits that only traverse the parts of the AST relevant to emitted declarations. Such traits would already have the "optimizations" I wrote naturally built-in. What's harder for me to imagine right now is how easy it would be to re-implement declaration transforms on top of such traits.

Please let me know your thoughts.

Copy link
Member

Choose a reason for hiding this comment

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

Possibly, I will think about it some more. I think one of the main challenges comes just from organizing the code and performing state management. It feels like similar to with ScopeTree we would need to tell GlobalSymbolBindingTracker when it is entering or leaving a scope, but the logic will diverge from ScopeTree (otherwise we would have just used ScopeTree for this purpose), which may result in the code looking more confusing.

Only Function and TSModuleDeclaration will have nested scopes, which I don't think should be too much trouble. We could try.

(Just voicing my thoughts out loud) I think what feels redundant with my current approach is that the short-circuit optimizations I take are making assumptions about how the tree will be traversed for emitting declarations, so that logic kind of feels like it's being reproduced in two places. The code could be potentially cleaner if that logic could be pulled out. What I imagine is something like alternative Visitor traits that only traverse the parts of the AST relevant to emitted declarations. Such traits would already have the "optimizations" I wrote naturally built-in. What's harder for me to imagine right now is how easy it would be to re-implement declaration transforms on top of such traits.

Adding another trait instead of Visitor may significantly increase maintenance costs. Currently Visitor is automatically generated with ast-codegen

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding another trait instead of Visitor may significantly increase maintenance costs

That's a good point. I suppose it would be necessary for such a trait to extend Visit and only override a small subset of the method implementations.

Only Function and TSModuleDeclaration will have nested scopes, which I don't think should be too much trouble. We could try.

I may not be able to get to this soon, but as you said, we have time!

@MichaelMitchell-at MichaelMitchell-at force-pushed the well_known_symbols branch 3 times, most recently from 21cb1e4 to 0d7ebf5 Compare July 15, 2024 07:10
@MichaelMitchell-at
Copy link
Contributor Author

Sorry for the late response. I will review this week.

No problem, no rush :)

Also, I wanted to ask, do you think we merge after TypeScript 5.6 is released or before?

I think it's ok to wait, though I pinged the TypeScript repo to see if they might include the change in a new patch release of 5.5.

Copy link
Member

@Dunqing Dunqing left a comment

Choose a reason for hiding this comment

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

Looks like we can skip visit_statement in GlobalSymbolBindingTracker. We just need to collect bindings.

@MichaelMitchell-at
Copy link
Contributor Author

Looks like we can skip visit_statement in GlobalSymbolBindingTracker. We just need to collect bindings.

I believe we need to traverse into the bodies of if/for/while loops and there are some cases where declaration is allowed, for example for (const Symbol of []) { ... }.

@Dunqing
Copy link
Member

Dunqing commented Jul 23, 2024

I believe we need to traverse into the bodies of if/for/while loops and there are some cases where declaration is allowed, for example for (const Symbol of []) { ... }.

Oh yes. You are right. Can we skip some statements that are not binding, traversing the whole AST has a performance impact.

@MichaelMitchell-at MichaelMitchell-at force-pushed the well_known_symbols branch 3 times, most recently from 51b5361 to a7e1cbc Compare July 23, 2024 10:25
@MichaelMitchell-at
Copy link
Contributor Author

@Dunqing I made some optimizations to bring the benchmark back to baseline performance, but each one needs careful attention to make sure there isn't a scenario that I didn't think of.

@Dunqing
Copy link
Member

Dunqing commented Jul 24, 2024

@Dunqing I made some optimizations to bring the benchmark back to baseline performance, but each one needs careful attention to make sure there isn't a scenario that I didn't think of.

Well done!

@Dunqing
Copy link
Member

Dunqing commented Jul 24, 2024

Can you add a test(like this) that uses non-global Symbol and globalThis in non-export declarations? I would like to see have any errors in this test.

@Dunqing
Copy link
Member

Dunqing commented Jul 24, 2024

I think it's ok to wait, though I pinged the TypeScript repo to see if they might include the change in a new patch release of 5.5.

I saw that v5.5.4 doesn't include this change. We have more time to improve this!

@MichaelMitchell-at
Copy link
Contributor Author

Can you add a test(like this) that uses non-global Symbol and globalThis in non-export declarations? I would like to see have any errors in this test.

Sure, just added them

Comment on lines +84 to +135
// Collect information about global Symbol usage within computed
// properties before performing any transformations.
self.global_symbol_binding_tracker.visit_program(program);
Copy link
Member

@Dunqing Dunqing Jul 24, 2024

Choose a reason for hiding this comment

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

Is it possible to check on demand? For example, we initially only check for the root scope. When we transform the function we will check the function. This will only check for declarations that will be exported

@DonIsaac
Copy link
Contributor

@Dunqing what do we need to do to get this over the finish line?

@Dunqing Dunqing marked this pull request as draft August 23, 2024 03:23
@Dunqing
Copy link
Member

Dunqing commented Aug 23, 2024

Feel free to convert it to open and ping me if it ready to review

@MichaelMitchell-at
Copy link
Contributor Author

I'm about to go on vacation so will probably not be able to touch this for at least another couple weeks. If anyone wants to iterate on this in the meantime, please feel free :)

@Dunqing
Copy link
Member

Dunqing commented Aug 23, 2024

I'm about to go on vacation so will probably not be able to touch this for at least another couple weeks. If anyone wants to iterate on this in the meantime, please feel free :)

Thank you for your hard work on Isolated Declaration! Have a nice holiday!!

@MichaelMitchell-at MichaelMitchell-at force-pushed the well_known_symbols branch 4 times, most recently from 25a0fb9 to ab309ee Compare September 23, 2024 20:01
@Boshen
Copy link
Member

Boshen commented Nov 25, 2024

Close as stale. Feel free to comeback when ts is updated.

@Boshen Boshen closed this Nov 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-isolated-declarations Isolated Declarations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

isolatedDeclarations does not support well-known symbols as properties
4 participants