Skip to content

[SwiftParser] Implement nonisolated(nonsending) specifier #3047

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 7 commits into from
Apr 12, 2025
Merged
3 changes: 3 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ public enum Keyword: CaseIterable {
case none
case nonisolated
case nonmutating
case nonsending
case objc
case obsoleted
case of
Expand Down Expand Up @@ -551,6 +552,8 @@ public enum Keyword: CaseIterable {
return KeywordSpec("nonisolated")
case .nonmutating:
return KeywordSpec("nonmutating")
case .nonsending:
return KeywordSpec("nonsending")
case .objc:
return KeywordSpec("objc")
case .obsoleted:
Expand Down
3 changes: 3 additions & 0 deletions CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon
case multipleTrailingClosureElementList
case namedOpaqueReturnType
case nilLiteralExpr
case nonisolatedSpecifierArgument
case nonisolatedSpecifierArgumentList
case nonisolatedTypeSpecifier
case objCSelectorPiece
case objCSelectorPieceList
case operatorDecl
Expand Down
49 changes: 47 additions & 2 deletions CodeGeneration/Sources/SyntaxSupport/TypeNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,52 @@ public let TYPE_NODES: [Node] = [
]
),

Node(
kind: .nonisolatedSpecifierArgument,
base: .syntax,
nameForDiagnostics: nil,
documentation: """
A single argument that can be added to a nonisolated specifier: 'nonsending'.

### Example
`data` in `func foo(data: nonisolated(nonsending) () async -> Void) -> X`
""",
traits: [
"Parenthesized"
],
children: [
Child(
name: "leftParen",
kind: .token(choices: [.token(.leftParen)])
),
Child(
name: "nonsendingKeyword",
kind: .token(choices: [.keyword(.nonsending)])
),
Child(
name: "rightParen",
kind: .token(choices: [.token(.rightParen)])
),
]
),

Node(
kind: .nonisolatedTypeSpecifier,
base: .syntax,
nameForDiagnostics: "'nonisolated' specifier",
children: [
Child(
name: "nonisolatedKeyword",
kind: .token(choices: [.keyword(.nonisolated)])
),
Child(
name: "argument",
kind: .node(kind: .nonisolatedSpecifierArgument),
isOptional: true
),
]
),

Node(
kind: .simpleTypeSpecifier,
base: .syntax,
Expand All @@ -689,7 +735,6 @@ public let TYPE_NODES: [Node] = [
.keyword(.__shared),
.keyword(.__owned),
.keyword(.isolated),
.keyword(.nonisolated),
.keyword(._const),
.keyword(.borrowing),
.keyword(.consuming),
Expand All @@ -704,6 +749,6 @@ public let TYPE_NODES: [Node] = [
kind: .typeSpecifierList,
base: .syntaxCollection,
nameForDiagnostics: nil,
elementChoices: [.simpleTypeSpecifier, .lifetimeTypeSpecifier]
elementChoices: [.simpleTypeSpecifier, .lifetimeTypeSpecifier, .nonisolatedTypeSpecifier]
),
]
6 changes: 3 additions & 3 deletions Sources/SwiftParser/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ extension Parser {
shouldParseArgument = true
case .customAttribute:
shouldParseArgument =
self.withLookahead { $0.atCustomAttributeArgument() }
self.withLookahead { $0.atAttributeOrSpecifierArgument() }
&& self.at(TokenSpec(.leftParen, allowAtStartOfLine: false))
case .optional:
shouldParseArgument = self.at(.leftParen)
Expand Down Expand Up @@ -1002,7 +1002,7 @@ extension Parser {
// MARK: Lookahead

extension Parser.Lookahead {
mutating func atCustomAttributeArgument() -> Bool {
mutating func atAttributeOrSpecifierArgument() -> Bool {
var lookahead = self.lookahead()
lookahead.skipSingle()

Expand Down Expand Up @@ -1036,7 +1036,7 @@ extension Parser.Lookahead {
}

if self.at(TokenSpec(.leftParen, allowAtStartOfLine: false))
&& self.withLookahead({ $0.atCustomAttributeArgument() })
&& self.withLookahead({ $0.atAttributeOrSpecifierArgument() })
{
self.skipSingle()
}
Expand Down
30 changes: 28 additions & 2 deletions Sources/SwiftParser/Expressions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,39 @@ extension Parser {
}
}

/// Make sure that we only accept `nonisolated(nonsending)` as a valid type specifier
/// in expression context to minimize source compatibility impact.
func canParseNonisolatedAsSpecifierInExpressionContext() -> Bool {
return withLookahead {
guard $0.consume(if: .keyword(.nonisolated)) != nil else {
return false
}

if $0.currentToken.isAtStartOfLine {
return false
}

guard $0.consume(if: .leftParen) != nil else {
return false
}

guard $0.consume(if: TokenSpec(.nonsending, allowAtStartOfLine: false)) != nil else {
return false
}

return $0.at(TokenSpec(.rightParen, allowAtStartOfLine: false))
}
}

/// Parse an expression sequence element.
mutating func parseSequenceExpressionElement(
flavor: ExprFlavor,
pattern: PatternContext = .none
) -> RawExprSyntax {
// Try to parse '@' sign or 'inout' as an attributed typerepr.
if self.at(.atSign, .keyword(.inout)) {
// Try to parse '@' sign, 'inout', or 'nonisolated' as an attributed typerepr.
if self.at(.atSign, .keyword(.inout))
|| self.canParseNonisolatedAsSpecifierInExpressionContext()
{
var lookahead = self.lookahead()
if lookahead.canParseType() {
let type = self.parseType()
Expand Down
6 changes: 5 additions & 1 deletion Sources/SwiftParser/Modifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,11 @@ extension Parser {
let detail: RawDeclModifierDetailSyntax?
if self.at(.leftParen) {
let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen)
let (unexpectedBeforeDetailToken, detailToken) = self.expect(TokenSpec(.unsafe, remapping: .identifier))
let (unexpectedBeforeDetailToken, detailToken) = self.expect(
TokenSpec(.unsafe, remapping: .identifier),
TokenSpec(.nonsending, remapping: .identifier),
default: TokenSpec(.identifier)
)
let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
detail = RawDeclModifierDetailSyntax(
unexpectedBeforeLeftParen,
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftParser/TokenPrecedence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ enum TokenPrecedence: Comparable {
.module,
.noasync,
.none,
.nonsending,
.obsoleted,
.of,
.Protocol,
Expand Down
167 changes: 161 additions & 6 deletions Sources/SwiftParser/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -687,26 +687,92 @@ extension Parser.Lookahead {
return true
}

mutating func skipTypeAttributeList() {
mutating func canParseTypeAttributeList() -> Bool {
var specifierProgress = LoopProgressCondition()
// TODO: Can we model isolated/_const so that they're specified in both canParse* and parse*?
while canHaveParameterSpecifier,
self.at(anyIn: SimpleTypeSpecifierSyntax.SpecifierOptions.self) != nil || self.at(.keyword(.isolated))
|| self.at(.keyword(._const)),
self.at(anyIn: SimpleTypeSpecifierSyntax.SpecifierOptions.self) != nil
|| self.at(.keyword(.nonisolated), .keyword(.dependsOn)),
self.hasProgressed(&specifierProgress)
{
self.consumeAnyToken()
switch self.currentToken {
case .keyword(.nonisolated):
let canParseNonisolated = self.withLookahead({
// Consume 'nonisolated'
$0.consumeAnyToken()

// The argument is missing but it still could be a valid modifier,
// i.e. `nonisolated` in an inheritance clause.
guard $0.at(TokenSpec(.leftParen, allowAtStartOfLine: false)) else {
return true
}

// Consume '('
$0.consumeAnyToken()

// nonisolated accepts a single modifier at the moment: 'nonsending'
// we need to check for that explicitly to avoid misinterpreting this
// keyword to be a modifier when it isn't i.e. `[nonisolated(42)]`
guard $0.consume(if: TokenSpec(.nonsending, allowAtStartOfLine: false)) != nil else {
return false
}

return $0.consume(if: TokenSpec(.rightParen, allowAtStartOfLine: false)) != nil
})

guard canParseNonisolated else {
return false
}

self.consumeAnyToken()

guard self.at(TokenSpec(.leftParen, allowAtStartOfLine: false)) else {
continue
}

self.skipSingle()

case .keyword(.dependsOn):
let canParseDependsOn = self.withLookahead({
// Consume 'dependsOn'
$0.consumeAnyToken()

if $0.currentToken.isAtStartOfLine {
return false
}

// `dependsOn` requires an argument list.
guard $0.atAttributeOrSpecifierArgument() else {
return false
}

return true
})

guard canParseDependsOn else {
return false
}

self.consumeAnyToken()
self.skipSingle()

default:
self.consumeAnyToken()
}
}

var attributeProgress = LoopProgressCondition()
while self.at(.atSign), self.hasProgressed(&attributeProgress) {
self.consumeAnyToken()
self.skipTypeAttribute()
}

return true
}

mutating func canParseTypeScalar() -> Bool {
self.skipTypeAttributeList()
guard self.canParseTypeAttributeList() else {
return false
}

guard self.canParseSimpleOrCompositionType() else {
return false
Expand Down Expand Up @@ -1056,6 +1122,88 @@ extension Parser {
return .lifetimeTypeSpecifier(lifetimeSpecifier)
}

private mutating func parseNonisolatedTypeSpecifier() -> RawTypeSpecifierListSyntax.Element {
let (unexpectedBeforeNonisolatedKeyword, nonisolatedKeyword) = self.expect(.keyword(.nonisolated))

// If the next token is not '(' this could mean two things:
// - What follows is a type and we should allow it because
// using `nonsisolated` without an argument is allowed in
// an inheritance clause.
// - The '(nonsending)' was omitted.
if !self.at(.leftParen) {
// `nonisolated P<...>` is allowed in an inheritance clause.
if withLookahead({ $0.canParseTypeIdentifier() }) {
let nonisolatedSpecifier = RawNonisolatedTypeSpecifierSyntax(
unexpectedBeforeNonisolatedKeyword,
nonisolatedKeyword: nonisolatedKeyword,
argument: nil,
arena: self.arena
)
return .nonisolatedTypeSpecifier(nonisolatedSpecifier)
}

// Otherwise require '(nonsending)'
let argument = RawNonisolatedSpecifierArgumentSyntax(
leftParen: missingToken(.leftParen),
nonsendingKeyword: missingToken(.keyword(.nonsending)),
rightParen: missingToken(.rightParen),
arena: self.arena
)

let nonisolatedSpecifier = RawNonisolatedTypeSpecifierSyntax(
unexpectedBeforeNonisolatedKeyword,
nonisolatedKeyword: nonisolatedKeyword,
argument: argument,
arena: self.arena
)

return .nonisolatedTypeSpecifier(nonisolatedSpecifier)
}

// Avoid being to greedy about `(` since this modifier should be associated with
// function types, it's possible that the argument is omitted and what follows
// is a function type i.e. `nonisolated () async -> Void`.
if self.at(.leftParen) && !withLookahead({ $0.atAttributeOrSpecifierArgument() }) {
let argument = RawNonisolatedSpecifierArgumentSyntax(
leftParen: missingToken(.leftParen),
nonsendingKeyword: missingToken(.keyword(.nonsending)),
rightParen: missingToken(.rightParen),
arena: self.arena
)

let nonisolatedSpecifier = RawNonisolatedTypeSpecifierSyntax(
unexpectedBeforeNonisolatedKeyword,
nonisolatedKeyword: nonisolatedKeyword,
argument: argument,
arena: self.arena
)

return .nonisolatedTypeSpecifier(nonisolatedSpecifier)
}

let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen)
let (unexpectedBeforeModifier, modifier) = self.expect(.keyword(.nonsending))
let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)

let argument = RawNonisolatedSpecifierArgumentSyntax(
unexpectedBeforeLeftParen,
leftParen: leftParen,
unexpectedBeforeModifier,
nonsendingKeyword: modifier,
unexpectedBeforeRightParen,
rightParen: rightParen,
arena: self.arena
)

let nonisolatedSpecifier = RawNonisolatedTypeSpecifierSyntax(
unexpectedBeforeNonisolatedKeyword,
nonisolatedKeyword: nonisolatedKeyword,
argument: argument,
arena: self.arena
)
return .nonisolatedTypeSpecifier(nonisolatedSpecifier)
}

private mutating func parseSimpleTypeSpecifier(
specifierHandle: TokenConsumptionHandle
) -> RawTypeSpecifierListSyntax.Element {
Expand All @@ -1079,6 +1227,13 @@ extension Parser {
} else {
break SPECIFIER_PARSING
}
} else if self.at(.keyword(.nonisolated)) {
// If '(' is located on the new line 'nonisolated' cannot be parsed
// as a specifier.
if self.peek(isAt: .leftParen) && self.peek().isAtStartOfLine {
break SPECIFIER_PARSING
}
specifiers.append(parseNonisolatedTypeSpecifier())
} else {
break SPECIFIER_PARSING
}
Expand Down
Loading