-
Notifications
You must be signed in to change notification settings - Fork 439
Typed throws #2219
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
Typed throws #2219
Conversation
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.
All of this should be behind an experimental language feature. Let me know if you need help with that as you get closer to actually wanting to merge it.
9c18353
to
7f39859
Compare
Great point, and it was super-easy to do. Thanks! |
kind: .thrownType, | ||
base: .syntax, | ||
nameForDiagnostics: "thrown 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.
Can you mark this node as experimental? Similar to https://github.com/apple/swift-syntax/blob/7c6d4f729f37f5e5a206b39ffa7bc1fac9479c24/CodeGeneration/Sources/SyntaxSupport/StmtNodes.swift#L590
Child( | ||
name: "thrownType", | ||
kind: .node(kind: .thrownType), | ||
isOptional: true | ||
), |
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 need to mark this child as experimental. There is no infrastructure for it yet, though. Could you write it?
Same for all the other thrownType
children.
Could you add documentation for these children?
@@ -56,7 +56,13 @@ public let TRAITS: [Trait] = [ | |||
Child(name: "asyncSpecifier", kind: .token(choices: [.keyword(.async), .keyword(.reasync)]), isOptional: true), | |||
Child(name: "unexpectedBetweenAsyncSpecifierAndThrowsSpecifier", kind: .node(kind: .unexpectedNodes), isOptional: true), | |||
Child(name: "throwsSpecifier", kind: .token(choices: [.keyword(.throws), .keyword(.rethrows)]), isOptional: true), | |||
Child(name: "unexpectedAfterThrowsSpecifier", kind: .node(kind: .unexpectedNodes), isOptional: true), | |||
Child(name: "unexpectedBetweenThrowsSpecifierAndThrownType", kind: .node(kind: .unexpectedNodes), isOptional: true), |
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 an API-breaking change that we should mention in the API-Incompatible Changes section of the release notes. I think it’s fine to break the API here since it’s unlikely that this property is in considerable use.
@@ -79,6 +79,27 @@ public let COMMON_NODES: [Node] = [ | |||
] | |||
), | |||
|
|||
Node( | |||
kind: .thrownType, | |||
base: .syntax, |
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.
Could you add a doc comments for this node and its children?
@@ -79,6 +79,27 @@ public let COMMON_NODES: [Node] = [ | |||
] | |||
), | |||
|
|||
Node( | |||
kind: .thrownType, |
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.
The more consistent naming of this node would be thrownTypeClause
in my opinion since it’s more than just the type (it also has parentheses).
Sources/SwiftParser/Specifiers.swift
Outdated
if throwsKeyword != nil && self.at(.leftParen) && experimentalFeatures.contains(.typedThrows) { | ||
thrownType = parseThrownType() | ||
} |
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.
IIUC this would also try to parse a thrown type for try async(MyError)
, which I think is confusing. IMO we should parse the thrown type whenever we set the throwsKeyword
if effect.spec.isThrowsSpecifier && self.peek().rawTokenKind == .leftParen { | ||
var backtrack = self.lookahead() | ||
backtrack.consumeAnyToken() | ||
backtrack.skipSingle() | ||
return backtrack.atFunctionTypeArrow() | ||
} |
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 think this doesn’t handle async throws(MyError)
because the current token is async
here, which means that we end up in the if
below, which just consumes throws
but not MyError
.
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.
The consumeAnyToken()
consumes the throws
, and the skipSingle
consumes a parenthesized expression.
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.
No, it doesn’t. The following test fails to parse
assertParse(
"[() async throws(MyError) -> Void]()"
)
@@ -279,9 +279,9 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { | |||
} | |||
} | |||
|
|||
if let throwsSpecifier = node.throwsSpecifier { | |||
if let throwsSpecifier = node.throwsSpecifier, node.thrownType == nil { |
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.
Why do we need to check that thrownType
is nil
here? We should be able to handle throws(MyError) async
as well.
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 don't need to do the check
"[() throws(PosixError) -> Void]()", | ||
experimentalFeatures: [.typedThrows] | ||
) | ||
} |
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 think the following test cases would be interesting as well. I have not filled in the expected diagnostics here but we should make sure that they make sense
Function parsing.
assertParse(
"func test() throws(MyError) async {}"
)
assertParse(
"func test() throws async(MyError) {}"
)
assertParse(
"func test() try(MyError) async {}"
)
assertParse(
"func test() try async(MyError) {}"
)
assertParse(
"func test() throws(MyError) await {}"
)
assertParse(
"func test() throws await(MyError) {}"
)
assertParse(
"func test() try(MyError) await {}"
)
assertParse(
"func test() try await(MyError) {}"
)
assertParse(
"func test() async(MyError) {}"
)
assertParse(
"func test() await(MyError) {}"
)
assertParse(
"func test() try(MyError) {}"
)
assertParse(
"func test() throws(MyError) {}"
)
assertParse(
"func test() throws(MyError)async {}" // no space between closing parenthesis and `async`
)
isAtFunctionTypeArrow
disambiguation
assertParse(
"[() throws(MyError) async -> Void]()"
)
assertParse(
"[() throws async(MyError) -> Void]()"
)
assertParse(
"[() try(MyError) async -> Void]()"
)
assertParse(
"[() try async(MyError) -> Void]()"
)
assertParse(
"[() throws(MyError) await -> Void]()"
)
assertParse(
"[() throws await(MyError) -> Void]()"
)
assertParse(
"[() try(MyError) await -> Void]()"
)
assertParse(
"[() try await(MyError) -> Void]()"
)
assertParse(
"[() async(MyError) -> Void]()"
)
assertParse(
"[() await(MyError) -> Void]()"
)
assertParse(
"[() try(MyError) -> Void]()"
)
assertParse(
"[() throws(MyError) -> Void]()"
)
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.
Great test ideas, thanks!
7f39859
to
f3168d0
Compare
@swift-ci please test |
DiagnosticSpec(locationMarker: "1️⃣", message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"]), | ||
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"), | ||
], | ||
fixedSource: "func test() async throws (MyError) {}", |
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.
@ahoppen are we missing removing (MyError)
here because they end up intersecting?
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 think it's grabbing both the async
and the unexpected nodes after the async
and moving them forward.
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.
That’s intentional and behaves correctly. (MyError)
gets parsed as unexpected tokens (which is fine, I think). And we don’t provide a Fix-It to remove unexpected tokens because most likely you wrote them for a reason and just removing something you wrote should not be a default fixing strategy.
"func test() 1️⃣try2️⃣(MyError) async {}", | ||
diagnostics: [ | ||
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]), | ||
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError) async' in function"), |
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 surprised this isn't handled as it is above when try
is the proper throws
or below where try
is before async
, ie. that async
is unexpected here.
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's because I am not parsing the thrown type clause along a recovery path, I think.
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 should parse the thrown type clause along the recovery path.
assertParse( | ||
"func test() throws(MyError) 1️⃣await {}", | ||
diagnostics: [ | ||
DiagnosticSpec(locationMarker: "1️⃣", message: "'await' must precede 'throws'", fixIts: ["move 'await' in front of 'throws'"]) |
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.
No expected async specifier; did you mean 'async'?
🤔? The fixed source is correct though so... that's good. This is the case for a few of the tests.
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.
There's some comments in the diagnostics generation about how the later errors subsume the earlier ones.
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 should be relatively easy to fix by changing the creation of missingToken
for misspelled tokens in parseEffectsSpecifier
like
asyncKeyword = missingToken(misspelledAsync.tokenKind, text: misspelledAsync.tokenText)
diagnostics: [ | ||
DiagnosticSpec(message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"]) | ||
], | ||
fixedSource: "func test() async throws(MyError){}", |
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.
Very nitpicky, but it would be good to keep the space before the {
.
@swift-ci please test |
@swift-ci please test Windows |
@swift-ci please test |
1 similar comment
@swift-ci please test |
@swift-ci please test |
@swift-ci please test Windows |
@swift-ci please test |
@swift-ci please test Windows |
@swift-ci please test Linux |
@@ -79,6 +79,34 @@ public let COMMON_NODES: [Node] = [ | |||
] | |||
), | |||
|
|||
Node( | |||
kind: .thrownTypeClause, |
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.
You should mark this node as experimental.
if effect.spec.isThrowsSpecifier && self.peek().rawTokenKind == .leftParen { | ||
var backtrack = self.lookahead() | ||
backtrack.consumeAnyToken() | ||
backtrack.skipSingle() | ||
return backtrack.atFunctionTypeArrow() | ||
} |
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.
No, it doesn’t. The following test fails to parse
assertParse(
"[() async throws(MyError) -> Void]()"
)
node.unexpectedBeforeAsyncSpecifier, node.unexpectedBetweenAsyncSpecifierAndThrowsSpecifier, node.unexpectedBetweenThrowsSpecifierAndThrownError, | ||
node.unexpectedAfterThrownError, |
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.
Could you wrap this array literal so that there’s one element per line. It just reads nicer that way.
|
||
extension EffectSpecifiersSyntax { | ||
// Default implementation for the experimental thrownError child, which must | ||
// be provided due to the use of @_spi. | ||
@_spi(ExperimentalLanguageFeatures) | ||
public var thrownError: ThrownTypeClauseSyntax? { | ||
get { return nil } | ||
set {} | ||
} | ||
} |
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.
SyntaxProtocol.swift really doesn’t seem like the right place to put this. Also, AFAICT everything compiles fine even if I remove this.
DiagnosticSpec(locationMarker: "1️⃣", message: "'async' must precede 'throws'", fixIts: ["move 'async' in front of 'throws'"]), | ||
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"), | ||
], | ||
fixedSource: "func test() async throws (MyError) {}", |
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.
That’s intentional and behaves correctly. (MyError)
gets parsed as unexpected tokens (which is fine, I think). And we don’t provide a Fix-It to remove unexpected tokens because most likely you wrote them for a reason and just removing something you wrote should not be a default fixing strategy.
assertParse( | ||
"func test() throws(MyError) 1️⃣await {}", | ||
diagnostics: [ | ||
DiagnosticSpec(locationMarker: "1️⃣", message: "'await' must precede 'throws'", fixIts: ["move 'await' in front of 'throws'"]) |
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 should be relatively easy to fix by changing the creation of missingToken
for misspelled tokens in parseEffectsSpecifier
like
asyncKeyword = missingToken(misspelledAsync.tokenKind, text: misspelledAsync.tokenText)
"func test() 1️⃣try2️⃣(MyError) await {}", | ||
diagnostics: [ | ||
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]), | ||
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError) await' in function"), |
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 should be parsing (MyError)
as a thrown type clause here or at least recover to parse await
as a misspelled async keyword. No matter how we implement it, the Fix-It that should be produced, is to move await
in front of try
.
DiagnosticSpec(locationMarker: "1️⃣", message: "expected throwing specifier; did you mean 'throws'?", fixIts: ["replace 'try' with 'throws'"]), | ||
DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code '(MyError)' in function"), | ||
], | ||
fixedSource: "func test() throws (MyError) {}", |
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 should change BasicFormat
to not insert a space here. BasicFormat.requiresWhitespace
is then entry point where this is decided.
fixedSource: "func test() async throws(MyError) {}", | ||
experimentalFeatures: [.typedThrows] | ||
) | ||
assertParse( |
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.
Could you add empty lines between the assertParse
statements to make it a little easier to read?
experimentalFeatures: .typedThrows | ||
) | ||
assertParse( | ||
"[() try(MyError) async -> Void]()", |
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 should not parse. This seems to be an existing issue in the new parser. I filed #2255
I’ve got a couple more comments in-line. On a higher level, the following still needs to be done.
@DougGregor Could you address my review comments in a follow-up PR? |
We just discussed the design of the syntax tree and I think the tree should be structured as follows:
|
Parsing of typed throw specifiers. See swiftlang/swift#68629 for more information.