Skip to content
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