Skip to content

Commit 464d634

Browse files
committed
adding better tuple support
1 parent 1eebf61 commit 464d634

File tree

8 files changed

+333
-3
lines changed

8 files changed

+333
-3
lines changed

Examples/Completed/conditionals/code.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,19 @@ case 100..<1000:
6767
default:
6868
naturalCount = "many"
6969
}
70-
print("There are \(naturalCount) \(countedThings).")
70+
print("There are \(naturalCount) \(countedThings).")
71+
72+
// Switch with tuple matching
73+
let somePoint = (1, 1)
74+
switch somePoint {
75+
case (0, 0):
76+
print("(0, 0) is at the origin")
77+
case (_, 0):
78+
print("(\(somePoint.0), 0) is on the x-axis")
79+
case (0, _):
80+
print("(0, \(somePoint.1)) is on the y-axis")
81+
case (-2...2, -2...2):
82+
print("(\(somePoint.0), \(somePoint.1)) is inside the box")
83+
default:
84+
print("(\(somePoint.0), \(somePoint.1)) is outside of the box")
85+
}

Examples/Completed/conditionals/dsl.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,25 @@ Switch("approximateCount") {
104104
Assignment("naturalCount", Literal.string("many"))
105105
}
106106
}
107-
Call("print", "There are \\(naturalCount) \\(countedThings).")
107+
Call("print", "There are \\(naturalCount) \\(countedThings).")
108+
Variable(.let, "somePoint", equals: TupleLiteral([.int(1), .int(1)])).withExplicitType()
109+
.comment {
110+
Line("Switch with tuple matching")
111+
}
112+
Switch("somePoint") {
113+
SwitchCase(Tuple.pattern([0, 0])) {
114+
Call("print", "(0, 0) is at the origin")
115+
}
116+
SwitchCase(Tuple.pattern([nil, 0])) {
117+
Call("print", "(\\(somePoint.0), 0) is on the x-axis")
118+
}
119+
SwitchCase(Tuple.pattern([0, nil])) {
120+
Call("print", "(0, \\(somePoint.1)) is on the y-axis")
121+
}
122+
SwitchCase(Tuple.pattern([(-2...2), (-2...2)])) {
123+
Call("print", "(\\(somePoint.0), \\(somePoint.1)) is inside the box")
124+
}
125+
Default {
126+
Call("print", "(\\(somePoint.0), \\(somePoint.1)) is outside of the box")
127+
}
128+
}

Examples/Completed/conditionals/syntax.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

Sources/SyntaxKit/Enum.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ public struct EnumCase: CodeBlock {
242242
equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space),
243243
value: DeclReferenceExprSyntax(baseName: .identifier(value))
244244
)
245+
case .tuple:
246+
fatalError("Tuple is not supported as a raw value for enum cases.")
245247
}
246248
}
247249

Sources/SyntaxKit/Literal.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public enum Literal: CodeBlock {
5252
case boolean(Bool)
5353
/// A reference to a variable or identifier (outputs without quotes).
5454
case ref(String)
55+
/// A tuple literal.
56+
case tuple([Literal?])
5557

5658
/// The SwiftSyntax representation of this literal.
5759
public var syntax: SyntaxProtocol {
@@ -75,12 +77,90 @@ public enum Literal: CodeBlock {
7577
return BooleanLiteralExprSyntax(literal: value ? .keyword(.true) : .keyword(.false))
7678
case .ref(let value):
7779
return DeclReferenceExprSyntax(baseName: .identifier(value))
80+
case .tuple(let elements):
81+
let tupleElements = TupleExprElementListSyntax(
82+
elements.enumerated().map { index, element in
83+
let elementExpr: ExprSyntax
84+
if let element = element {
85+
elementExpr =
86+
element.syntax.as(ExprSyntax.self)
87+
?? ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("")))
88+
} else {
89+
// Wildcard pattern - use underscore
90+
elementExpr = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("_")))
91+
}
92+
return TupleExprElementSyntax(
93+
label: nil,
94+
colon: nil,
95+
expression: elementExpr,
96+
trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil
97+
)
98+
}
99+
)
100+
return TupleExprSyntax(
101+
leftParen: .leftParenToken(),
102+
elements: tupleElements,
103+
rightParen: .rightParenToken()
104+
)
78105
}
79106
}
80107
}
81108

82109
// MARK: - LiteralValue Implementations
83110

111+
/// A tuple value that can be used as a literal.
112+
public struct TupleLiteral: LiteralValue {
113+
private let elements: [Literal?]
114+
115+
/// Creates a tuple with the given elements.
116+
/// - Parameter elements: The tuple elements, where `nil` represents a wildcard.
117+
public init(_ elements: [Literal?]) {
118+
self.elements = elements
119+
}
120+
121+
/// The Swift type name for this tuple.
122+
public var typeName: String {
123+
let elementTypes = elements.map { element in
124+
if let element = element {
125+
switch element {
126+
case .integer: return "Int"
127+
case .float: return "Double"
128+
case .string: return "String"
129+
case .boolean: return "Bool"
130+
case .nil: return "Any?"
131+
case .ref: return "Any"
132+
case .tuple: return "Any"
133+
}
134+
} else {
135+
return "Any"
136+
}
137+
}
138+
return "(\(elementTypes.joined(separator: ", ")))"
139+
}
140+
141+
/// Renders this tuple as a Swift literal string.
142+
public var literalString: String {
143+
let elementStrings = elements.map { element in
144+
if let element = element {
145+
switch element {
146+
case .integer(let value): return String(value)
147+
case .float(let value): return String(value)
148+
case .string(let value): return "\"\(value)\""
149+
case .boolean(let value): return value ? "true" : "false"
150+
case .nil: return "nil"
151+
case .ref(let value): return value
152+
case .tuple(let tupleElements):
153+
let tuple = TupleLiteral(tupleElements)
154+
return tuple.literalString
155+
}
156+
} else {
157+
return "_"
158+
}
159+
}
160+
return "(\(elementStrings.joined(separator: ", ")))"
161+
}
162+
}
163+
84164
extension Array: LiteralValue where Element == String {
85165
/// The Swift type name for an array of strings.
86166
public var typeName: String { "[String]" }
@@ -122,3 +202,27 @@ extension Dictionary: LiteralValue where Key == Int, Value == String {
122202
return "[\(elements)]"
123203
}
124204
}
205+
206+
// MARK: - Convenience Methods
207+
208+
extension Literal {
209+
/// Creates a tuple literal from an array of optional literals (for patterns with wildcards).
210+
public static func tuplePattern(_ elements: [Literal?]) -> Literal {
211+
.tuple(elements)
212+
}
213+
214+
/// Creates an integer literal.
215+
public static func int(_ value: Int) -> Literal {
216+
.integer(value)
217+
}
218+
219+
/// Converts a Literal.tuple to a TupleLiteral for use in Variable declarations.
220+
public var asTupleLiteral: TupleLiteral? {
221+
switch self {
222+
case .tuple(let elements):
223+
return TupleLiteral(elements)
224+
default:
225+
return nil
226+
}
227+
}
228+
}

Sources/SyntaxKit/Tuple.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ public struct Tuple: CodeBlock {
4141
self.elements = content()
4242
}
4343

44+
/// Creates a tuple pattern for switch cases.
45+
/// - Parameter elements: Array of pattern elements, where `nil` represents a wildcard pattern.
46+
public static func pattern(_ elements: [PatternConvertible?]) -> TuplePattern {
47+
TuplePattern(elements: elements)
48+
}
49+
4450
public var syntax: SyntaxProtocol {
4551
guard !elements.isEmpty else {
4652
fatalError("Tuple must contain at least one element.")
@@ -69,3 +75,45 @@ public struct Tuple: CodeBlock {
6975
return tupleExpr
7076
}
7177
}
78+
79+
/// A tuple pattern for switch cases.
80+
public struct TuplePattern: PatternConvertible {
81+
private let elements: [PatternConvertible?]
82+
83+
internal init(elements: [PatternConvertible?]) {
84+
self.elements = elements
85+
}
86+
87+
public var patternSyntax: PatternSyntax {
88+
let patternElements = TuplePatternElementListSyntax(
89+
elements.enumerated().map { index, element in
90+
let patternElement: TuplePatternElementSyntax
91+
if let element = element {
92+
patternElement = TuplePatternElementSyntax(
93+
label: nil,
94+
colon: nil,
95+
pattern: element.patternSyntax,
96+
trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil
97+
)
98+
} else {
99+
// Wildcard pattern
100+
patternElement = TuplePatternElementSyntax(
101+
label: nil,
102+
colon: nil,
103+
pattern: PatternSyntax(WildcardPatternSyntax(wildcard: .wildcardToken())),
104+
trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil
105+
)
106+
}
107+
return patternElement
108+
}
109+
)
110+
111+
return PatternSyntax(
112+
TuplePatternSyntax(
113+
leftParen: .leftParenToken(),
114+
elements: patternElements,
115+
rightParen: .rightParenToken()
116+
)
117+
)
118+
}
119+
}

Tests/SyntaxKitTests/ConditionalsExampleTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,41 @@ import Testing
183183
Call("print") {
184184
ParameterExp(name: "", value: "\"There are \\(naturalCount) \\(countedThings).\"")
185185
}
186+
187+
// MARK: - Tuple literal and tuple pattern switch
188+
Variable(.let, name: "somePoint", equals: TupleLiteral([.int(1), .int(1)]))
189+
.comment {
190+
Line("Switch with tuple matching")
191+
}
192+
Switch("somePoint") {
193+
SwitchCase(Tuple.pattern([0, 0])) {
194+
Call("print") {
195+
ParameterExp(name: "", value: "\"(0, 0) is at the origin\"")
196+
}
197+
}
198+
SwitchCase(Tuple.pattern([nil, 0])) {
199+
Call("print") {
200+
ParameterExp(name: "", value: "\"(\\(somePoint.0), 0) is on the x-axis\"")
201+
}
202+
}
203+
SwitchCase(Tuple.pattern([0, nil])) {
204+
Call("print") {
205+
ParameterExp(name: "", value: "\"(0, \\(somePoint.1)) is on the y-axis\"")
206+
}
207+
}
208+
SwitchCase(Tuple.pattern([(-2...2), (-2...2)])) {
209+
Call("print") {
210+
ParameterExp(
211+
name: "", value: "\"(\\(somePoint.0), \\(somePoint.1)) is inside the box\"")
212+
}
213+
}
214+
Default {
215+
Call("print") {
216+
ParameterExp(
217+
name: "", value: "\"(\\(somePoint.0), \\(somePoint.1)) is outside of the box\"")
218+
}
219+
}
220+
}
186221
}
187222

188223
// Generate Swift from DSL

Tests/SyntaxKitTests/LiteralValueTests.swift

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,109 @@ internal struct LiteralValueTests {
108108
#expect(literal2.contains("2: \"b\""))
109109
#expect(literal2.contains("3: \"c\""))
110110
}
111+
112+
// MARK: - TupleLiteral Tests
113+
114+
@Test internal func testTupleLiteralTypeName() {
115+
let tuple1 = TupleLiteral([.int(1), .int(2)])
116+
#expect(tuple1.typeName == "(Int, Int)")
117+
118+
let tuple2 = TupleLiteral([.string("hello"), .int(42), .boolean(true)])
119+
#expect(tuple2.typeName == "(String, Int, Bool)")
120+
121+
let tuple3 = TupleLiteral([.int(1), nil, .string("test")])
122+
#expect(tuple3.typeName == "(Int, Any, String)")
123+
124+
let tuple4 = TupleLiteral([nil, nil])
125+
#expect(tuple4.typeName == "(Any, Any)")
126+
}
127+
128+
@Test internal func testTupleLiteralString() {
129+
let tuple1 = TupleLiteral([.int(1), .int(2)])
130+
#expect(tuple1.literalString == "(1, 2)")
131+
132+
let tuple2 = TupleLiteral([.string("hello"), .int(42), .boolean(true)])
133+
#expect(tuple2.literalString == "(\"hello\", 42, true)")
134+
135+
let tuple3 = TupleLiteral([.int(1), nil, .string("test")])
136+
#expect(tuple3.literalString == "(1, _, \"test\")")
137+
138+
let tuple4 = TupleLiteral([nil, nil])
139+
#expect(tuple4.literalString == "(_, _)")
140+
141+
let tuple5 = TupleLiteral([.float(3.14), .nil])
142+
#expect(tuple5.literalString == "(3.14, nil)")
143+
}
144+
145+
@Test internal func testTupleLiteralWithNestedTuples() {
146+
let nestedTuple = TupleLiteral([.int(1), .tuple([.string("nested"), .int(2)])])
147+
#expect(nestedTuple.typeName == "(Int, Any)")
148+
#expect(nestedTuple.literalString == "(1, (\"nested\", 2))")
149+
}
150+
151+
@Test internal func testTupleLiteralWithRef() {
152+
let tuple = TupleLiteral([.ref("variable"), .int(42)])
153+
#expect(tuple.typeName == "(Any, Int)")
154+
#expect(tuple.literalString == "(variable, 42)")
155+
}
156+
157+
@Test internal func testEmptyTupleLiteral() {
158+
let tuple = TupleLiteral([])
159+
#expect(tuple.typeName == "()")
160+
#expect(tuple.literalString == "()")
161+
}
162+
163+
// MARK: - TupleLiteral Code Generation Tests
164+
165+
@Test internal func testVariableWithTupleLiteral() {
166+
let tuple = TupleLiteral([.int(1), .int(2)])
167+
let variable = Variable(.let, name: "point", equals: tuple)
168+
169+
let generated = variable.syntax.description
170+
#expect(generated.contains("let point = (1, 2)"))
171+
#expect(generated.contains("point"))
172+
}
173+
174+
@Test internal func testVariableWithTupleLiteralWithExplicitType() {
175+
let tuple = TupleLiteral([.int(1), .int(2)])
176+
let variable = Variable(.let, name: "point", equals: tuple).withExplicitType()
177+
178+
let generated = variable.syntax.description
179+
#expect(generated.contains("let point : (Int, Int) = (1, 2)"))
180+
#expect(generated.contains("point"))
181+
}
182+
183+
@Test internal func testVariableWithComplexTupleLiteral() {
184+
let tuple = TupleLiteral([.string("hello"), .int(42), .boolean(true)])
185+
let variable = Variable(.let, name: "data", equals: tuple).withExplicitType()
186+
187+
let generated = variable.syntax.description
188+
#expect(generated.contains("let data : (String, Int, Bool) = (\"hello\", 42, true)"))
189+
#expect(generated.contains("data"))
190+
}
191+
192+
@Test internal func testVariableWithTupleLiteralWithWildcards() {
193+
let tuple = TupleLiteral([.int(1), nil, .string("test")])
194+
let variable = Variable(.let, name: "partial", equals: tuple).withExplicitType()
195+
196+
let generated = variable.syntax.description
197+
#expect(generated.contains("let partial : (Int, Any, String) = (1, _, \"test\")"))
198+
#expect(generated.contains("partial"))
199+
}
200+
201+
@Test internal func testLiteralAsTupleLiteralConversion() {
202+
let literal = Literal.tuple([.int(1), .int(2)])
203+
let tupleLiteral = literal.asTupleLiteral
204+
205+
#expect(tupleLiteral != nil)
206+
#expect(tupleLiteral?.typeName == "(Int, Int)")
207+
#expect(tupleLiteral?.literalString == "(1, 2)")
208+
}
209+
210+
@Test internal func testNonTupleLiteralAsTupleLiteralConversion() {
211+
let literal = Literal.int(42)
212+
let tupleLiteral = literal.asTupleLiteral
213+
214+
#expect(tupleLiteral == nil)
215+
}
111216
}

0 commit comments

Comments
 (0)