Skip to content

Commit e3440e2

Browse files
committed
Handle the Pair, Regex, and IntSeq types during codegen and evaluation
1 parent 4a6da2d commit e3440e2

File tree

18 files changed

+438
-38
lines changed

18 files changed

+438
-38
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ let package = Package(
9292
"Fixtures/Collections.pkl",
9393
"Fixtures/Poly.pkl",
9494
"Fixtures/ApiTypes.pkl",
95+
"Fixtures/Collections2.pkl",
9596
],
9697
swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]
9798
),

Sources/PklSwift/API/IntSeq.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import MessagePack
18+
19+
public struct IntSeq: Decodable, Sendable {
20+
public let start: Int
21+
public let end: Int
22+
public let step: Int
23+
24+
public let intSeq: StrideThrough<Int>
25+
26+
public init(start: Int, end: Int, step: Int = 1) {
27+
self.start = start
28+
self.end = end
29+
self.step = step
30+
self.intSeq = stride(from: start, through: end, by: step)
31+
}
32+
}
33+
34+
extension IntSeq: Hashable {
35+
public func hash(into hasher: inout Hasher) {
36+
self.start.hash(into: &hasher)
37+
self.end.hash(into: &hasher)
38+
self.step.hash(into: &hasher)
39+
}
40+
41+
public static func == (lhs: IntSeq, rhs: IntSeq) -> Bool {
42+
lhs.start == rhs.start
43+
&& lhs.end == rhs.end
44+
&& lhs.step == rhs.step
45+
}
46+
}
47+
48+
extension IntSeq: PklSerializableType {
49+
public static var messageTag: PklValueType { .intSeq }
50+
51+
public static func decode(_ fields: [MessagePackValue], codingPath: [any CodingKey]) throws -> Self {
52+
try checkFieldCount(fields, codingPath: codingPath, min: 4)
53+
return try Self(
54+
start: decodeInt(fields[1], codingPath: codingPath, index: 1),
55+
end: decodeInt(fields[2], codingPath: codingPath, index: 2),
56+
step: decodeInt(fields[3], codingPath: codingPath, index: 3)
57+
)
58+
}
59+
60+
private static func decodeInt(_ value: MessagePackValue, codingPath: [any CodingKey], index: Int) throws -> Int {
61+
guard case .int(let intValue) = value else {
62+
throw DecodingError.dataCorrupted(
63+
.init(
64+
codingPath: codingPath,
65+
debugDescription: "Expected field \(index) to be an integer but got \(value.debugDataTypeDescription)"
66+
))
67+
}
68+
return Int(intValue)
69+
}
70+
71+
}

Sources/PklSwift/API/Pair.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import MessagePack
18+
19+
// Must be marked @unchecked Sendable because A or B can be AnyHashable
20+
public struct Pair<A: Hashable, B: Hashable>: Hashable, @unchecked Sendable {
21+
/// The value of the Pair's first element.
22+
public let first: A
23+
24+
/// The value of the Pair's second element.
25+
public let second: B
26+
27+
public init(_ first: A, _ second: B) {
28+
self.first = first
29+
self.second = second
30+
}
31+
32+
public var tupleValue: (A, B) { (first, second) }
33+
}
34+
35+
extension Pair {
36+
// this can't be a let because Pair is generic
37+
public static var messageTag: PklValueType { .pair }
38+
}
39+
40+
extension Pair: PklSerializableType {
41+
public static func decode(_ fields: [MessagePackValue], codingPath: [any CodingKey]) throws -> Self {
42+
try checkFieldCount(fields, codingPath: codingPath, min: 3)
43+
let first: A = try Self.decodeElement(fields[1], codingPath: codingPath)
44+
let second: B = try Self.decodeElement(fields[2], codingPath: codingPath)
45+
return Self(first, second)
46+
}
47+
48+
private static func decodeElement<T: Decodable>(_ value: MessagePackValue, codingPath: [any CodingKey]) throws -> T {
49+
try value.decode(T.self)
50+
}
51+
52+
private static func decodeElement<T: Hashable>(_ value: MessagePackValue, codingPath: [any CodingKey]) throws -> T {
53+
try (_PklDecoder.decodePolymorphic(value, codingPath: codingPath))?.value as! T
54+
}
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import MessagePack
18+
19+
public struct PklRegex: Sendable {
20+
private let pattern: String
21+
22+
public var regex: Regex<AnyRegexOutput> { try! Regex(pattern) }
23+
24+
public init(_ pattern: String) throws {
25+
self.pattern = pattern
26+
// check that this pattern is valid but don't store the regex
27+
_ = try Regex(pattern)
28+
}
29+
}
30+
31+
extension PklRegex: Hashable {
32+
public func hash(into hasher: inout Hasher) {
33+
self.pattern.hash(into: &hasher)
34+
}
35+
36+
public static func == (lhs: PklRegex, rhs: PklRegex) -> Bool {
37+
lhs.pattern == rhs.pattern
38+
}
39+
}
40+
41+
extension PklRegex: PklSerializableType {
42+
public static var messageTag: PklValueType { .regex }
43+
44+
public static func decode(_ fields: [MessagePackValue], codingPath: [any CodingKey]) throws -> Self {
45+
try checkFieldCount(fields, codingPath: codingPath, min: 2)
46+
guard case .string(let pattern) = fields[1] else {
47+
throw DecodingError.dataCorrupted(
48+
.init(
49+
codingPath: codingPath,
50+
debugDescription: "Expected field 0 to be a string but got \(fields[0].debugDataTypeDescription)"
51+
))
52+
}
53+
return try Self(pattern)
54+
}
55+
}

Sources/PklSwift/Decoder/PklDecoder.swift

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ public enum PklValueType: UInt8, Decodable, Sendable {
4040

4141
public protocol PklSerializableType: Decodable {
4242
static var messageTag: PklValueType { get }
43+
44+
static func decode(_ fields: [MessagePackValue], codingPath: [any CodingKey]) throws -> Self
45+
}
46+
47+
extension PklSerializableType {
48+
static func checkFieldCount(_ fields: [MessagePackValue], codingPath: [any CodingKey], min: Int) throws {
49+
guard fields.count >= min else {
50+
throw DecodingError.dataCorrupted(
51+
.init(
52+
codingPath: codingPath,
53+
debugDescription: "Expected at least \(min) fields but got \(fields.count)"
54+
))
55+
}
56+
}
4357
}
4458

4559
public protocol PklSerializableValueUnitType: PklSerializableType {
@@ -49,35 +63,41 @@ public protocol PklSerializableValueUnitType: PklSerializableType {
4963
init(_: ValueType, unit: UnitType)
5064
}
5165

52-
extension Decodable where Self: PklSerializableValueUnitType {
66+
extension Decodable where Self: PklSerializableType {
5367
public init(from decoder: Decoder) throws {
5468
guard let decoder = decoder as? _PklDecoder else {
5569
fatalError("\(Self.self) can only be decoded using \(_PklDecoder.self), but was: \(decoder)")
5670
}
57-
self = try Self.decodeValueUnitType(from: decoder.value, at: decoder.codingPath)
58-
}
59-
}
60-
61-
extension PklSerializableValueUnitType {
62-
static func decodeValueUnitType(
63-
from value: MessagePackValue,
64-
at codingPath: [CodingKey]
65-
) throws -> Self {
66-
guard case .array(let arr) = value else {
71+
let codingPath = decoder.codingPath
72+
guard case .array(let arr) = decoder.value else {
6773
throw DecodingError.dataCorrupted(
6874
.init(
6975
codingPath: codingPath,
70-
debugDescription: "Expected array but got \(value.debugDataTypeDescription)"
76+
debugDescription: "Expected array but got \(decoder.value.debugDataTypeDescription)"
7177
))
7278
}
7379
let code = try arr[0].decode(PklValueType.self)
80+
guard arr.count > 0 else {
81+
throw DecodingError.dataCorrupted(
82+
.init(
83+
codingPath: codingPath,
84+
debugDescription: "Expected non-empty array"
85+
))
86+
}
7487
guard Self.messageTag == code else {
7588
throw DecodingError.dataCorrupted(
7689
.init(codingPath: codingPath, debugDescription: "Cannot decode \(code) into \(Self.self)"))
7790
}
7891

79-
let value = try arr[1].decode(Self.ValueType.self)
80-
let unit = try arr[2].decode(Self.UnitType.self)
92+
self = try Self.decode(arr, codingPath: codingPath)
93+
}
94+
}
95+
96+
extension Decodable where Self: PklSerializableValueUnitType {
97+
public static func decode(_ fields: [MessagePackValue], codingPath: [any CodingKey]) throws -> Self {
98+
try checkFieldCount(fields, codingPath: codingPath, min: 3)
99+
let value = try fields[1].decode(Self.ValueType.self)
100+
let unit = try fields[2].decode(Self.UnitType.self)
81101
return Self(value, unit: unit)
82102
}
83103
}
@@ -237,6 +257,15 @@ extension _PklDecoder {
237257
case .dataSize:
238258
let decoder = try _PklDecoder(value: propertyValue)
239259
return try PklAny(value: DataSize(from: decoder))
260+
case .pair:
261+
let decoder = try _PklDecoder(value: propertyValue)
262+
return try PklAny(value: Pair<AnyHashable?, AnyHashable?>(from: decoder))
263+
case .regex:
264+
let decoder = try _PklDecoder(value: propertyValue)
265+
return try PklAny(value: PklRegex(from: decoder))
266+
case .intSeq:
267+
let decoder = try _PklDecoder(value: propertyValue)
268+
return try PklAny(value: IntSeq(from: decoder))
240269
case .bytes:
241270
guard case .bin(let bytes) = value[1] else {
242271
throw DecodingError.dataCorrupted(

Tests/PklSwiftTests/Fixtures/AnyType.pkl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ nullable: Any = null
2828
duration: Any = 5.min
2929

3030
dataSize: Any = 10.mb
31+
32+
pair: Any = Pair(1, 2)
33+
34+
regex: Any = Regex("abc")
35+
36+
seq: Any = IntSeq(0, 10).step(2)
Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,22 @@
1-
res1: Duration = 10.h
2-
res2: DataSize = 1.2345.gib
1+
dur: Duration = 20.h
2+
3+
data: DataSize = 2.4680.gib
4+
5+
pair1: Pair = Pair("a", "b")
6+
pair2: Pair<String, Int> = Pair("a", 1)
7+
pair3: Pair<String, Int?> = Pair("a", 1)
8+
pair4: Pair<String, Int?> = Pair("a", null)
9+
10+
// pairMapping1: Mapping<Pair, Any>
11+
// pairMapping2: Mapping<Pair<String, Int>, Any>
12+
13+
pairListing1: Listing<Pair> = new {
14+
pair2
15+
pair3
16+
pair4
17+
}
18+
pairListing2: Listing<Pair<String, Int?>> = pairListing1
19+
20+
regex: Regex = Regex("def")
21+
22+
seq: IntSeq = IntSeq(0, 20).step(3)

Tests/PklSwiftTests/Fixtures/Generated/AnyType.pkl.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ extension AnyType {
2525

2626
public var dataSize: AnyHashable?
2727

28+
public var pair: AnyHashable?
29+
30+
public var regex: AnyHashable?
31+
32+
public var seq: AnyHashable?
33+
2834
public init(
2935
bird: AnyHashable?,
3036
primitive: AnyHashable?,
@@ -34,7 +40,10 @@ extension AnyType {
3440
mapping: AnyHashable?,
3541
nullable: AnyHashable?,
3642
duration: AnyHashable?,
37-
dataSize: AnyHashable?
43+
dataSize: AnyHashable?,
44+
pair: AnyHashable?,
45+
regex: AnyHashable?,
46+
seq: AnyHashable?
3847
) {
3948
self.bird = bird
4049
self.primitive = primitive
@@ -45,6 +54,9 @@ extension AnyType {
4554
self.nullable = nullable
4655
self.duration = duration
4756
self.dataSize = dataSize
57+
self.pair = pair
58+
self.regex = regex
59+
self.seq = seq
4860
}
4961

5062
public init(from decoder: Decoder) throws {
@@ -58,7 +70,10 @@ extension AnyType {
5870
let nullable = try dec.decode(PklSwift.PklAny.self, forKey: PklCodingKey(string: "nullable")).value
5971
let duration = try dec.decode(PklSwift.PklAny.self, forKey: PklCodingKey(string: "duration")).value
6072
let dataSize = try dec.decode(PklSwift.PklAny.self, forKey: PklCodingKey(string: "dataSize")).value
61-
self = Module(bird: bird, primitive: primitive, primitive2: primitive2, array: array, set: set, mapping: mapping, nullable: nullable, duration: duration, dataSize: dataSize)
73+
let pair = try dec.decode(PklSwift.PklAny.self, forKey: PklCodingKey(string: "pair")).value
74+
let regex = try dec.decode(PklSwift.PklAny.self, forKey: PklCodingKey(string: "regex")).value
75+
let seq = try dec.decode(PklSwift.PklAny.self, forKey: PklCodingKey(string: "seq")).value
76+
self = Module(bird: bird, primitive: primitive, primitive2: primitive2, array: array, set: set, mapping: mapping, nullable: nullable, duration: duration, dataSize: dataSize, pair: pair, regex: regex, seq: seq)
6277
}
6378
}
6479

0 commit comments

Comments
 (0)