Skip to content

Model Syntax nodes as structs #155

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 4 commits into from
Oct 22, 2019
Merged

Conversation

ahoppen
Copy link
Member

@ahoppen ahoppen commented Oct 11, 2019

While investigating performance problems with SwiftSyntax, in particular the SyntaxRewriter, we have noticed that a considerable time was spent dealing with existentials that result from the way we model the syntax hierarchy using protocols. In an effort to reduce this problem, I have restructured the entire syntax layout to eliminate protocols and be entirely struct-based. As this has some major implications to SwiftSyntax’s API, I’d like to get some feedback on whether we should use it or not.

API Change

The main idea of this PR is to model each syntax node as a struct and allow casting between those structs either through initializers or designated as(TargetType.self) casting methods.

Furthermore there exist protocols SyntaxProtocol, ExprSyntaxProtocol etc. to which all all corresponding syntax nodes conform. That way, additional methods can be added to all syntax nodes by conforming to the corresponding protocol.

There is an enum SyntaxEnum that can be retrieved from as Syntax node by calling asSyntaxEnum and allows exhaustive switching over all enum kinds.

Performance improvement

Performance measurements were done by running an empty SyntaxRewriter / SyntaxVisitor on a Swift file with ~5000 lines of code. The baseline is the current performance of SwiftSyntax before #157 was merged.

Highlights are a 10x performance increase in SytnaxRewriter performance. Performance of SyntaxVisitor is roughly the same right now but can be improved by 10% in #158 that models it as a class instead of a protocol.

Empty SyntaxRewriter

Before After #157 After this PR
Debug 391.8ms 180.7ms (-54%) 129.7ms (-28%)
Release 114.5ms 60.8ms (-47%) 12.7ms(-79%)

Empty SyntaxVisitor

Before After this PR After #158
Debug 92.9ms 112.1ms (+21%) 107.9ms (+16%)
Release 12.3ms 12.6ms (+3%) 10.9ms (-12%)

Empty SyntaxAnyVisitor

Before After this PR After #158
Debug 106.2ms 136.2ms (+28%) 115.3ms (+9%)
Release 24.8ms 15.5ms (-38%) 11.4ms (-54%)

API example

// ===============
// MARK: Protocols
// ===============

protocol SyntaxProtocol {
    /// Property that is used to access the underlying storage of the syntax node.
    /// Should not be accessed directly, `Syntax(self)` is the preferred spelling.
    var _syntaxNode: Syntax { get }    
}
extension SyntaxProtocol {
    /// Converts the given `Syntax` node to this type. Returns `nil` if the 
    /// conversion is not possible.
    init?(_ syntax: Syntax)

    var children: SyntaxChildren {
        return SyntaxChildren(Syntax(self))
    }
    var isPresent: Bool {
        return Syntax(self).isPresent
    }
    // etc.
}

protocol ExprSyntaxProtocol {
    var asExprSyntax: ExprSyntax { get }
}
extension ExprSyntaxProtocol {}

// =============
// MARK: Structs
// =============

struct Syntax: SyntaxProtocol {
    /// Create a `Syntax` node from a specialized syntax node.
    init<S: SyntaxProtocol>(_ syntax: S)

    func is<SyntaxType: SyntaxProtocol(_ syntaxType: SyntaxType.Type) -> Bool
    func as<SyntaxType: SyntaxProtocol(_ syntaxType: SyntaxType.Type) -> SyntaxType
}

struct ExprSyntax: ExprSyntaxProtocol, SyntaxProtocol {
    func is<SyntaxType: ExprSyntaxProtocol(_ syntaxType: SyntaxType.Type) -> Bool
    func as<SyntaxType: ExprSyntaxProtocol(_ syntaxType: SyntaxType.Type) -> SyntaxType
}

struct FunctionCallExprSyntax: ExprSyntaxProtocol, SyntaxProtocol {
}

// ==========
// MARK: Enum
// ==========

enum SyntaxEnum {
    case functionCallExpr(FunctionCallExprSyntax)
    // And all other leaf types (i.e. no cases for base types like ExprSyntax)
}

extension Syntax {
    var asSyntaxEnum: SyntaxEnum
}

@akyrtzi
Copy link
Contributor

akyrtzi commented Oct 11, 2019

\cc @allevato

@ahoppen ahoppen force-pushed the remove-existentials branch from 97a0cdf to 672d4ef Compare October 16, 2019 22:33
@ahoppen ahoppen closed this Oct 16, 2019
@ahoppen ahoppen reopened this Oct 16, 2019
@ahoppen ahoppen force-pushed the remove-existentials branch from 672d4ef to 6184faf Compare October 16, 2019 22:58
@ahoppen ahoppen changed the title WIP: Performance improvements by removing protocols Model Syntax nodes as structs Oct 16, 2019
@ahoppen ahoppen requested a review from akyrtzi October 16, 2019 23:13
@ahoppen
Copy link
Member Author

ahoppen commented Oct 16, 2019

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 97a0cdf

@ahoppen ahoppen force-pushed the remove-existentials branch 2 times, most recently from a6390cb to 6f03210 Compare October 17, 2019 18:17
@ahoppen
Copy link
Member Author

ahoppen commented Oct 17, 2019

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 6184faf

Avoid the overhead of existentials by modelling the entire syntax node
hierarchy using structs with custom casting functions.

This results in a major performance gain for SyntaxRewriter.
Improve the performance of SyntaxRewriter by avoiding unnecessary
casts, reusing the SyntaxBox and eliminating .enumerated()
Refactor the implementation to match SyntaxRewriter, thereby gaining
the same performance benefits.
@ahoppen ahoppen force-pushed the remove-existentials branch from 6f03210 to 5a654ca Compare October 17, 2019 20:19
Otherwise it requires a lot of boilerplate to return the Syntax node's
type in the lit-test-helper.
@ahoppen ahoppen force-pushed the remove-existentials branch from 5a654ca to 4ea7f87 Compare October 17, 2019 20:24
@ahoppen
Copy link
Member Author

ahoppen commented Oct 17, 2019

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 6f03210

Copy link
Member

@allevato allevato left a comment

Choose a reason for hiding this comment

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

I'm happy with these changes—the is/as methods and initializer-based conversions are an improvement over the original property-based forms, and should make client code cleaner. The performance improvements will be great for swift-format too.

Can you update the API Example in your PR description to use the new version of the APIs? It looks like it's still based on the original version of this PR; having it up-to-date will be helpful for future readers who stumble upon this change.

@ahoppen
Copy link
Member Author

ahoppen commented Oct 18, 2019

@allevato Thanks for the review. Sorry, I totally missed to update the code example. It’s updated now.

@ahoppen ahoppen merged commit 6aa112e into swiftlang:master Oct 22, 2019
@ahoppen ahoppen deleted the remove-existentials branch October 28, 2019 22:24
adevress pushed a commit to adevress/swift-syntax that referenced this pull request Jan 14, 2024
Don't propagate the diagnostic engine when formatting.
adevress pushed a commit to adevress/swift-syntax that referenced this pull request Jan 14, 2024
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.

6 participants