-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Add a jump-table for visitEachChild #50266
Conversation
419d989
to
56c8d67
Compare
@typescript-bot perf test |
Heya @rbuckton, I've started to run the perf test suite on this PR at 56c8d67. You can monitor the build here. Update: The results are in! |
@rbuckton Here they are:
CompilerComparison Report - main..50266
System
Hosts
Scenarios
TSServerComparison Report - main..50266
System
Hosts
Scenarios
Developer Information: |
56c8d67
to
b43e228
Compare
b43e228
to
83b60d7
Compare
@@ -885,6 +885,144 @@ namespace ts { | |||
/* @internal */ jsDocCache?: readonly JSDocTag[]; // Cache for getJSDocTags | |||
} | |||
|
|||
/* @internal */ | |||
export type HasChildren = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that if we need to update this, we'll know because some other code won't be able to compile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To add a method to visitEachChild
(and potentially forEachChild
), we'd have to add the node to this union.
// Names | ||
|
||
case SyntaxKind.Identifier: | ||
Debug.type<Identifier>(node); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is maybe a silly question, but are we sure the perf boost here isn't only because the new code never calls Debug.type
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug.type()
had almost no impact on perf when I added it. It's a noop, so inlining pretty much results in elimination. It definitely wouldn't account for a 3% difference in emit time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, just checking!
@@ -885,6 +885,144 @@ namespace ts { | |||
/* @internal */ jsDocCache?: readonly JSDocTag[]; // Cache for getJSDocTags | |||
} | |||
|
|||
/* @internal */ | |||
export type HasChildren = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if someone adds a new syntax kind and forgets to update this? Seems like their new node might not emit its children?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK that's what would have happened in the old code too, because it'd hit the default case to just do nothing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both this and #50225 use HasChildren
, then you wouldn't be able to define a forEachChild
branch or visitEachChild
branch without adding an entry, and if you add an entry to HasChildren
you would have compile time errors if you forget to add branches to forEachChild
and visitEachChild
.
83b60d7
to
35b5e31
Compare
One more perf test to see if it's worth dropping the early check for tokens in |
Heya @rbuckton, I've started to run the perf test suite on this PR at 35b5e31. You can monitor the build here. Update: The results are in! |
@@ -378,929 +378,799 @@ namespace ts { | |||
export function visitEachChild<T extends Node>(node: T | undefined, visitor: Visitor, context: TransformationContext, nodesVisitor?: typeof visitNodes, tokenVisitor?: Visitor): T | undefined; | |||
/* @internal */ | |||
export function visitEachChild<T extends Node>(node: T | undefined, visitor: Visitor, context: TransformationContext, nodesVisitor?: NodesVisitor, tokenVisitor?: Visitor, nodeVisitor?: NodeVisitor): T | undefined; // eslint-disable-line @typescript-eslint/unified-signatures | |||
export function visitEachChild(node: Node | undefined, visitor: Visitor, context: TransformationContext, nodesVisitor = visitNodes, tokenVisitor?: Visitor, nodeVisitor: NodeVisitor = visitNode): Node | undefined { | |||
export function visitEachChild<T extends Node>(node: T | undefined, visitor: Visitor, context: TransformationContext, nodesVisitor = visitNodes, tokenVisitor?: Visitor, nodeVisitor: NodeVisitor = visitNode): T | undefined { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the input possibly unrelated to the output? @jakebailey noticed something like this while converting to SFT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It totally is. In this case, the implementation fails under SFT because it doesn't overlap with its overloads, except that we don't properly check that without SFT which is why this weird narrower implementation works.
Changing the signature to match the overload is totally fine; no worse than what we have now and users won't see the difference. Actual SFT support and a corrected signature will have to come later when we're ready for a break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm asserting visitEachChildTable[node.kind] as VisitEachChildFunction<T> | undefined
. It's difficult to prove this in the type system, but VisitEachChildTable[T["kind"]]
should be VisitEachChildFunction<T> | undefined
. This isn't perfect, but its better than any
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also don't have a great way of expressing the existing overloads in the type system. visitEachChild<BinaryExpressionButWithExtraTypeInfo>(...)
would say it produces a BinaryExpressionButWithExtraTypeInfo
, but would actually produce a BinaryExpressionButWithExtraTypeInfo | BinaryExpression
depending on whether we updated the node. Unless we introduce a big ADT union of all possible Node
subtypes, I don't think this can be improved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, unless I'm mistaken, the transform API is used heavily to completely and entirely replace nodes when visiting with... whatever, even nodes of differing types. So, I would expect that a fixed signature would instead have an overload with a more specific return first, but also a more generic "it's just a node" when the result can't be inferred.
But, that's offtopic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
visitEachChild
always returns the same type of node passed as the first argument. You can only update child nodes using the supplied visitor
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, oops, I think Daniel and I are thinking of visitNode
and visitNodes
, which do have a bogus signature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is and it isn't. visitNode
is really supposed to be used with a test
parameter, such that you validate the result is a T
when visiting a class of nodes such as Statement
, Expression
, etc. The test
is optional, but really shouldn't be. Unfortunately, making test
a required argument would probably be a breaking change, and we'd have to expose some of the higher-level node tests (i.e., isStatement
, isExpression
, isClassElement
, etc.) in the public API that are currently internal.
@rbuckton Here they are:
CompilerComparison Report - main..50266
System
Hosts
Scenarios
TSServerComparison Report - main..50266
System
Hosts
Scenarios
Developer Information: |
35b5e31
to
dadca7f
Compare
Have we decided on whether we prefer the Array or Object Literal variant for jump tables? |
Does the same type safety work for the array? My guess was not (especially if we are using push to keep the array compact) |
No, it doesn't. To do the same thing with an array would require using an array literal and packing it with leading |
For me, if the perf is the same, I'd prefer the safer one. |
Yeah, ultimately I didn't see enough of a win doing one over the other. |
Similar to #50225, this creates a table to handle each potential syntax kind in
visitEachChild
.