Skip to content

Commit 46b5bda

Browse files
committed
Parse InlineArray type sugar
Parse e.g `[3 x Int]` as type sugar for InlineArray. Gated behind an experimental feature flag for now.
1 parent c1ebe0e commit 46b5bda

File tree

10 files changed

+336
-24
lines changed

10 files changed

+336
-24
lines changed

CodeGeneration/Sources/SyntaxSupport/ExperimentalFeatures.swift

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public enum ExperimentalFeature: String, CaseIterable {
2323
case abiAttribute
2424
case keypathWithMethodMembers
2525
case oldOwnershipOperatorSpellings
26+
case inlineArrayTypeSugar
2627

2728
/// The name of the feature as it is written in the compiler's `Features.def` file.
2829
public var featureName: String {
@@ -47,6 +48,8 @@ public enum ExperimentalFeature: String, CaseIterable {
4748
return "KeypathWithMethodMembers"
4849
case .oldOwnershipOperatorSpellings:
4950
return "OldOwnershipOperatorSpellings"
51+
case .inlineArrayTypeSugar:
52+
return "InlineArrayTypeSugar"
5053
}
5154
}
5255

@@ -73,6 +76,8 @@ public enum ExperimentalFeature: String, CaseIterable {
7376
return "keypaths with method members"
7477
case .oldOwnershipOperatorSpellings:
7578
return "`_move` and `_borrow` as ownership operators"
79+
case .inlineArrayTypeSugar:
80+
return "sugar type for InlineArray"
7681
}
7782
}
7883

CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift

+3
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ public enum Keyword: CaseIterable {
293293
case willSet
294294
case witness_method
295295
case wrt
296+
case x
296297
case yield
297298

298299
public var spec: KeywordSpec {
@@ -735,6 +736,8 @@ public enum Keyword: CaseIterable {
735736
return KeywordSpec("witness_method")
736737
case .wrt:
737738
return KeywordSpec("wrt")
739+
case .x:
740+
return KeywordSpec("x", experimentalFeature: .inlineArrayTypeSugar)
738741
case .yield:
739742
return KeywordSpec("yield")
740743
}

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

+1
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public enum SyntaxNodeKind: String, CaseIterable, IdentifierConvertible, TypeCon
172172
case inheritedTypeList
173173
case initializerClause
174174
case initializerDecl
175+
case inlineArrayType
175176
case inOutExpr
176177
case integerLiteralExpr
177178
case isExpr

CodeGeneration/Sources/SyntaxSupport/TypeNodes.swift

+42
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,48 @@ public let TYPE_NODES: [Node] = [
300300
]
301301
),
302302

303+
Node(
304+
kind: .inlineArrayType,
305+
base: .type,
306+
experimentalFeature: .inlineArrayTypeSugar,
307+
nameForDiagnostics: "inline array type",
308+
documentation: "An inline array type `[3 x Int]`, sugar for `InlineArray<3, Int>`.",
309+
children: [
310+
Child(
311+
name: "leftSquare",
312+
kind: .token(choices: [.token(.leftSquare)])
313+
),
314+
Child(
315+
name: "count",
316+
kind: .node(kind: .genericArgument),
317+
nameForDiagnostics: "count",
318+
documentation: """
319+
The `count` argument for the inline array type.
320+
321+
- Note: In semantically valid Swift code, this is always an integer or a wildcard type, e.g `_` in `[_ x Int]`.
322+
"""
323+
),
324+
Child(
325+
name: "separator",
326+
kind: .token(choices: [.keyword(.x)])
327+
),
328+
Child(
329+
name: "element",
330+
kind: .node(kind: .genericArgument),
331+
nameForDiagnostics: "element type",
332+
documentation: """
333+
The `element` argument for the inline array type.
334+
335+
- Note: In semantically valid Swift code, this is always a type.
336+
"""
337+
),
338+
Child(
339+
name: "rightSquare",
340+
kind: .token(choices: [.token(.rightSquare)])
341+
),
342+
]
343+
),
344+
303345
Node(
304346
kind: .memberType,
305347
base: .type,

CodeGeneration/Tests/ValidateSyntaxNodes/ValidateSyntaxNodes.swift

+11
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,11 @@ class ValidateSyntaxNodes: XCTestCase {
380380
message:
381381
"child 'defaultKeyword' has a single keyword as its only token choice and is followed by a colon. It should thus be named 'defaultLabel'"
382382
),
383+
// 'separator' is more descriptive than 'xKeyword'
384+
ValidationFailure(
385+
node: .inlineArrayType,
386+
message: "child 'separator' has a single keyword as its only token choice and should thus be named 'xKeyword'"
387+
),
383388
]
384389
)
385390
}
@@ -523,6 +528,12 @@ class ValidateSyntaxNodes: XCTestCase {
523528
message:
524529
"child 'closure' is named inconsistently with 'FunctionCallExprSyntax.trailingClosure', which has the same type ('ClosureExprSyntax')"
525530
),
531+
// Giving these fields distinct names is more helpful.
532+
ValidationFailure(
533+
node: .inlineArrayType,
534+
message:
535+
"child 'element' is named inconsistently with 'InlineArrayTypeSyntax.count', which has the same type ('GenericArgumentSyntax')"
536+
),
526537
]
527538
)
528539
}

Sources/SwiftParser/Expressions.swift

+9
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,15 @@ extension Parser {
15911591

15921592
let (unexpectedBeforeLSquare, lsquare) = self.expect(.leftSquare)
15931593

1594+
// Check to see if we have an InlineArray type in expression position.
1595+
if self.isAtStartOfInlineArrayTypeBody() {
1596+
let type = self.parseInlineArrayType(
1597+
unexpectedBeforeLSquare: unexpectedBeforeLSquare,
1598+
leftSquare: lsquare
1599+
)
1600+
return RawExprSyntax(RawTypeExprSyntax(type: type, arena: self.arena))
1601+
}
1602+
15941603
if let rsquare = self.consume(if: .rightSquare) {
15951604
return RawExprSyntax(
15961605
RawArrayExprSyntax(

Sources/SwiftParser/TokenPrecedence.swift

+1
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ enum TokenPrecedence: Comparable {
362362
.weak,
363363
.witness_method,
364364
.wrt,
365+
.x,
365366
.unsafe:
366367
self = .exprKeyword
367368
#if RESILIENT_LIBRARIES

Sources/SwiftParser/Types.swift

+121-23
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,11 @@ extension Parser {
577577
}
578578

579579
extension Parser {
580+
/// Whether the parser is at the start of an InlineArray type sugar body.
581+
func isAtStartOfInlineArrayTypeBody() -> Bool {
582+
withLookahead { $0.canParseStartOfInlineArrayTypeBody() }
583+
}
584+
580585
/// Parse an array or dictionary type..
581586
mutating func parseCollectionType() -> RawTypeSyntax {
582587
if let remaingingTokens = remainingTokensIfMaximumNestingLevelReached() {
@@ -592,6 +597,15 @@ extension Parser {
592597
}
593598

594599
let (unexpectedBeforeLSquare, leftsquare) = self.expect(.leftSquare)
600+
601+
// Check to see if we're at the start of an InlineArray type.
602+
if self.isAtStartOfInlineArrayTypeBody() {
603+
return self.parseInlineArrayType(
604+
unexpectedBeforeLSquare: unexpectedBeforeLSquare,
605+
leftSquare: leftsquare
606+
)
607+
}
608+
595609
let firstType = self.parseType()
596610
if let colon = self.consume(if: .colon) {
597611
let secondType = self.parseType()
@@ -622,6 +636,39 @@ extension Parser {
622636
)
623637
}
624638
}
639+
640+
mutating func parseInlineArrayType(
641+
unexpectedBeforeLSquare: RawUnexpectedNodesSyntax?,
642+
leftSquare: RawTokenSyntax
643+
) -> RawTypeSyntax {
644+
precondition(self.experimentalFeatures.contains(.inlineArrayTypeSugar))
645+
646+
// We allow both values and types here and for the element type for
647+
// better recovery in cases where the user writes e.g '[Int x 3]'.
648+
let count = self.parseGenericArgumentType()
649+
650+
let (unexpectedBeforeSeparator, separator) = self.expect(
651+
TokenSpec(.x, allowAtStartOfLine: false)
652+
)
653+
654+
let element = self.parseGenericArgumentType()
655+
656+
let (unexpectedBeforeRightSquare, rightSquare) = self.expect(.rightSquare)
657+
658+
return RawTypeSyntax(
659+
RawInlineArrayTypeSyntax(
660+
unexpectedBeforeLSquare,
661+
leftSquare: leftSquare,
662+
count: .init(argument: count, trailingComma: nil, arena: self.arena),
663+
unexpectedBeforeSeparator,
664+
separator: separator,
665+
element: .init(argument: element, trailingComma: nil, arena: self.arena),
666+
unexpectedBeforeRightSquare,
667+
rightSquare: rightSquare,
668+
arena: self.arena
669+
)
670+
)
671+
}
625672
}
626673

627674
extension Parser.Lookahead {
@@ -714,15 +761,7 @@ extension Parser.Lookahead {
714761
}
715762
case TokenSpec(.leftSquare):
716763
self.consumeAnyToken()
717-
guard self.canParseType() else {
718-
return false
719-
}
720-
if self.consume(if: .colon) != nil {
721-
guard self.canParseType() else {
722-
return false
723-
}
724-
}
725-
guard self.consume(if: .rightSquare) != nil else {
764+
guard self.canParseCollectionTypeBody() else {
726765
return false
727766
}
728767
case TokenSpec(.wildcard):
@@ -762,6 +801,59 @@ extension Parser.Lookahead {
762801
return true
763802
}
764803

804+
/// Checks whether we can parse the start of an InlineArray type. This does
805+
/// not include the element type.
806+
mutating func canParseStartOfInlineArrayTypeBody() -> Bool {
807+
guard self.experimentalFeatures.contains(.inlineArrayTypeSugar) else {
808+
return false
809+
}
810+
811+
// We must have at least '[<type-or-integer> x', which cannot be any other
812+
// kind of expression or type. We specifically look for both types and
813+
// integers for better recovery in e.g cases where the user writes e.g
814+
// '[Int x 2]'. We only do type-scalar since variadics would be ambiguous
815+
// e.g 'Int...x'.
816+
guard self.canParseTypeScalar() || self.canParseIntegerLiteral() else {
817+
return false
818+
}
819+
820+
// We don't currently allow multi-line since that would require
821+
// disambiguation with array literals.
822+
return self.consume(if: TokenSpec(.x, allowAtStartOfLine: false)) != nil
823+
}
824+
825+
mutating func canParseInlineArrayTypeBody() -> Bool {
826+
guard self.canParseStartOfInlineArrayTypeBody() else {
827+
return false
828+
}
829+
// Note we look for both types and integers for better recovery in e.g cases
830+
// where the user writes e.g '[Int x 2]'.
831+
guard self.canParseGenericArgument() else {
832+
return false
833+
}
834+
return self.consume(if: .rightSquare) != nil
835+
}
836+
837+
mutating func canParseCollectionTypeBody() -> Bool {
838+
// Check to see if we have an InlineArray sugar type.
839+
if self.experimentalFeatures.contains(.inlineArrayTypeSugar) {
840+
var lookahead = self.lookahead()
841+
if lookahead.canParseInlineArrayTypeBody() {
842+
self = lookahead
843+
return true
844+
}
845+
}
846+
guard self.canParseType() else {
847+
return false
848+
}
849+
if self.consume(if: .colon) != nil {
850+
guard self.canParseType() else {
851+
return false
852+
}
853+
}
854+
return self.consume(if: .rightSquare) != nil
855+
}
856+
765857
mutating func canParseTupleBodyType() -> Bool {
766858
guard
767859
!self.at(.rightParen, .rightBrace) && !self.atContextualPunctuator("...")
@@ -863,6 +955,24 @@ extension Parser.Lookahead {
863955
return lookahead.currentToken.isGenericTypeDisambiguatingToken
864956
}
865957

958+
mutating func canParseIntegerLiteral() -> Bool {
959+
if self.currentToken.tokenText == "-", self.peek(isAt: .integerLiteral) {
960+
self.consumeAnyToken()
961+
self.consumeAnyToken()
962+
return true
963+
}
964+
if self.consume(if: .integerLiteral) != nil {
965+
return true
966+
}
967+
return false
968+
}
969+
970+
mutating func canParseGenericArgument() -> Bool {
971+
// A generic argument can either be a type or an integer literal (who is
972+
// optionally negative).
973+
self.canParseType() || self.canParseIntegerLiteral()
974+
}
975+
866976
mutating func consumeGenericArguments() -> Bool {
867977
// Parse the opening '<'.
868978
guard self.consume(ifPrefix: "<", as: .leftAngle) != nil else {
@@ -872,21 +982,9 @@ extension Parser.Lookahead {
872982
if !self.at(prefix: ">") {
873983
var loopProgress = LoopProgressCondition()
874984
repeat {
875-
// A generic argument can either be a type or an integer literal (who is
876-
// optionally negative).
877-
if self.canParseType() {
878-
continue
879-
} else if self.currentToken.tokenText == "-",
880-
self.peek(isAt: .integerLiteral)
881-
{
882-
self.consumeAnyToken()
883-
self.consumeAnyToken()
884-
continue
885-
} else if self.consume(if: .integerLiteral) != nil {
886-
continue
985+
guard self.canParseGenericArgument() else {
986+
return false
887987
}
888-
889-
return false
890988
// Parse the comma, if the list continues.
891989
} while self.consume(if: .comma) != nil && self.hasProgressed(&loopProgress)
892990
}

Tests/SwiftParserTest/ExpressionTypeTests.swift

+38
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
@_spi(ExperimentalLanguageFeatures) import SwiftParser
1314
import SwiftSyntax
1415
import XCTest
1516

@@ -105,4 +106,41 @@ final class ExpressionTypeTests: ParserTestCase {
105106
substructureAfterMarker: "1️⃣"
106107
)
107108
}
109+
110+
func testCanParseTypeInlineArray() {
111+
// Make sure we can handle cases where the type is spelled first in
112+
// an InlineArray sugar type.
113+
let cases: [UInt: String] = [
114+
#line: "[3 x Int]",
115+
#line: "[[3 x Int]]",
116+
#line: "[[Int x 3]]",
117+
#line: "[_ x Int]",
118+
#line: "[Int x Int]",
119+
#line: "[@escaping () -> Int x Int]",
120+
#line: "[Int.Type x Int]",
121+
#line: "[sending P & Q x Int]",
122+
#line: "[(some P & Q) -> Int x Int]",
123+
#line: "[~P x Int]",
124+
#line: "[(Int, String) x Int]",
125+
#line: "[G<T> x Int]",
126+
#line: "[[3 x Int] x Int]",
127+
#line: "[[Int] x Int]",
128+
#line: "[_ x Int]",
129+
#line: "[_? x Int]",
130+
#line: "[_?x Int]",
131+
#line: "[_! x Int]",
132+
#line: "[_!x Int]",
133+
#line: "[Int?x Int]",
134+
]
135+
for (line, type) in cases {
136+
assertParse(
137+
"S<\(type), 1️⃣X>.self",
138+
{ ExprSyntax.parse(from: &$0) },
139+
substructure: IdentifierTypeSyntax(name: .identifier("X")),
140+
substructureAfterMarker: "1️⃣",
141+
experimentalFeatures: [.inlineArrayTypeSugar, .valueGenerics],
142+
line: line
143+
)
144+
}
145+
}
108146
}

0 commit comments

Comments
 (0)