From a4ea7ae014103d0fcac63895f15bca3a2347ca73 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 16 Jan 2025 14:43:43 -0800 Subject: [PATCH] Depend on CasePathsCore This is a breaking change, which means it should only be released in a 1.0 major update. --- Package.resolved | 17 +- Package.swift | 10 +- Sources/Parsing/Conversions/Enum.swift | 60 ++-- Sources/swift-parsing-benchmark/JSON.swift | 298 ++++++++++---------- Tests/ParsingTests/OneOfTests.swift | 312 ++++++++++----------- 5 files changed, 361 insertions(+), 336 deletions(-) diff --git a/Package.resolved b/Package.resolved index 908ee008e9..d462e45918 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", - "version" : "1.0.0" + "revision" : "e7039aaa4d9cf386fa8324a89f258c3f2c54d751", + "version" : "1.6.0" } }, { @@ -36,13 +36,22 @@ "version" : "1.0.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "302891700c7fa3b92ebde9fe7b42933f8349f3c7", - "version" : "1.0.0" + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" } } ], diff --git a/Package.swift b/Package.swift index dcdee85ea1..7134234371 100644 --- a/Package.swift +++ b/Package.swift @@ -18,18 +18,21 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.6.0"), .package(url: "https://github.com/google/swift-benchmark", from: "0.1.1"), ], targets: [ .target( name: "Parsing", - dependencies: [.product(name: "CasePaths", package: "swift-case-paths")] + dependencies: [ + .product(name: "CasePathsCore", package: "swift-case-paths"), + ] ), .testTarget( name: "ParsingTests", dependencies: [ - "Parsing" + "Parsing", + .product(name: "CasePaths", package: "swift-case-paths") ] ), .executableTarget( @@ -37,6 +40,7 @@ let package = Package( dependencies: [ "Parsing", .product(name: "Benchmark", package: "swift-benchmark"), + .product(name: "CasePaths", package: "swift-case-paths") ] ), ] diff --git a/Sources/Parsing/Conversions/Enum.swift b/Sources/Parsing/Conversions/Enum.swift index 8818d9a72f..4e1d1bb798 100644 --- a/Sources/Parsing/Conversions/Enum.swift +++ b/Sources/Parsing/Conversions/Enum.swift @@ -1,4 +1,4 @@ -import CasePaths +import CasePathsCore extension Conversion { /// Converts the associated values of an enum case into the case, and an enum case into its @@ -7,12 +7,13 @@ extension Conversion { /// Useful for transforming the output of a ``ParserPrinter`` into an enum: /// /// ```swift + /// @CasePathable /// enum Expression { /// case add(Int, Int) /// ... /// } /// - /// let add = ParsePrint(.case(Expression.add)) { + /// let add = ParsePrint(.case(\Expression.Cases.add)) { /// Int.parser() /// "+" /// Int.parser() @@ -29,35 +30,46 @@ extension Conversion { /// and extract the associated values from the case. @inlinable public static func `case`( - _ initializer: @escaping (Values) -> Enum - ) -> Self where Self == CasePath { - /initializer + _ keyPath: CaseKeyPath + ) -> Self where Self == Conversions.EnumConversion { + Self(casePath: AnyCasePath(keyPath)) } + /// A performance-optimized version of ``case(_:)``. @inlinable - public static func `case`( - _ initializer: Enum - ) -> Self where Self == CasePath { - /initializer + public static func `case`( + _ keyPath: KeyPath> + ) -> Self where Self == Conversions.EnumConversion { + Self(casePath: Enum.allCasePaths[keyPath: keyPath]) } } -extension CasePath: Conversion { - @inlinable - public func apply(_ input: Value) -> Root { - self.embed(input) - } +extension Conversions { + public struct EnumConversion: Conversion { + @usableFromInline + let casePath: AnyCasePath - @inlinable - public func unapply(_ output: Root) throws -> Value { - guard let value = self.extract(from: output) - else { - throw ConvertingError( - """ - case: Failed to extract \(Value.self) from \(output). - """ - ) + @inlinable + init(casePath: AnyCasePath) { + self.casePath = casePath + } + + @inlinable + public func apply(_ input: Value) -> Root { + casePath.embed(input) + } + + @inlinable + public func unapply(_ output: Root) throws -> Value { + guard let value = casePath.extract(from: output) + else { + throw ConvertingError( + """ + case: Failed to extract \(Value.self) from \(output). + """ + ) + } + return value } - return value } } diff --git a/Sources/swift-parsing-benchmark/JSON.swift b/Sources/swift-parsing-benchmark/JSON.swift index 8227de22cb..84b6d848f0 100644 --- a/Sources/swift-parsing-benchmark/JSON.swift +++ b/Sources/swift-parsing-benchmark/JSON.swift @@ -1,175 +1,175 @@ import Benchmark +import CasePaths import Foundation import Parsing -/// This benchmark shows how to create a naive JSON parser with combinators. -/// -/// It is mostly implemented according to the [spec](https://www.json.org/json-en.html) (we take a -/// shortcut and use `Double.parser()`, which behaves accordingly). -let jsonSuite = BenchmarkSuite(name: "JSON") { suite in - #if swift(>=5.8) - struct JSONValue: ParserPrinter { - enum Output: Equatable { - case array([Self]) - case boolean(Bool) - case null - case number(Double) - case object([String: Self]) - case string(String) - } +struct JSONValue: ParserPrinter { + @CasePathable + enum Output: Equatable { + case array([Self]) + case boolean(Bool) + case null + case number(Double) + case object([String: Self]) + case string(String) + } - var body: some ParserPrinter { - Whitespace() - OneOf { - JSONObject().map(.case(Output.object)) - JSONArray().map(.case(Output.array)) - JSONString().map(.case(Output.string)) - Double.parser().map(.case(Output.number)) - Bool.parser().map(.case(Output.boolean)) - "null".utf8.map { Output.null } - } - Whitespace() - } + var body: some ParserPrinter { + Whitespace() + OneOf { + JSONObject().map(.case(\Output.AllCasePaths.object)) + JSONArray().map(.case(\.array)) + JSONString().map(.case(\.string)) + Double.parser().map(.case(\.number)) + Bool.parser().map(.case(\.boolean)) + "null".utf8.map { Output.null } } + Whitespace() + } +} - struct JSONString: ParserPrinter { - var body: some ParserPrinter { - "\"".utf8 - Many(into: "") { string, fragment in - string.append(contentsOf: fragment) - } decumulator: { string in - string.map(String.init).reversed().makeIterator() - } element: { - OneOf { - Prefix(1) { $0.isUnescapedJSONStringByte }.map(.string) +struct JSONString: ParserPrinter { + var body: some ParserPrinter { + "\"".utf8 + Many(into: "") { string, fragment in + string.append(contentsOf: fragment) + } decumulator: { string in + string.map(String.init).reversed().makeIterator() + } element: { + OneOf { + Prefix(1) { $0.isUnescapedJSONStringByte }.map(.string) - Parse { - "\\".utf8 + Parse { + "\\".utf8 - OneOf { - "\"".utf8.map { "\"" } - "\\".utf8.map { "\\" } - "/".utf8.map { "/" } - "b".utf8.map { "\u{8}" } - "f".utf8.map { "\u{c}" } - "n".utf8.map { "\n" } - "r".utf8.map { "\r" } - "t".utf8.map { "\t" } - ParsePrint(.unicode) { - Prefix(4) { $0.isHexDigit } - } - } + OneOf { + "\"".utf8.map { "\"" } + "\\".utf8.map { "\\" } + "/".utf8.map { "/" } + "b".utf8.map { "\u{8}" } + "f".utf8.map { "\u{c}" } + "n".utf8.map { "\n" } + "r".utf8.map { "\r" } + "t".utf8.map { "\t" } + ParsePrint(.unicode) { + Prefix(4) { $0.isHexDigit } } } - } terminator: { - "\"".utf8 } } + } terminator: { + "\"".utf8 } + } +} - struct JSONObject: ParserPrinter { - var body: some ParserPrinter { - "{".utf8 - Many(into: [String: JSONValue.Output]()) { - (object: inout [String: JSONValue.Output], pair: (String, JSONValue.Output)) in - let (name, value) = pair - object[name] = value - } decumulator: { object in - (object.sorted(by: { $0.key < $1.key }) as [(String, JSONValue.Output)]) - .reversed() - .makeIterator() - } element: { - Whitespace() - JSONString() - Whitespace() - ":".utf8 - JSONValue() - } separator: { - ",".utf8 - } terminator: { - "}".utf8 - } - } +struct JSONObject: ParserPrinter { + var body: some ParserPrinter { + "{".utf8 + Many(into: [String: JSONValue.Output]()) { + (object: inout [String: JSONValue.Output], pair: (String, JSONValue.Output)) in + let (name, value) = pair + object[name] = value + } decumulator: { object in + (object.sorted(by: { $0.key < $1.key }) as [(String, JSONValue.Output)]) + .reversed() + .makeIterator() + } element: { + Whitespace() + JSONString() + Whitespace() + ":".utf8 + JSONValue() + } separator: { + ",".utf8 + } terminator: { + "}".utf8 } + } +} - struct JSONArray: ParserPrinter { - var body: some ParserPrinter { - "[".utf8 - Many { - JSONValue() - } separator: { - ",".utf8 - } terminator: { - "]".utf8 - } +struct JSONArray: ParserPrinter { + var body: some ParserPrinter { + "[".utf8 + Many { + JSONValue() + } separator: { + ",".utf8 + } terminator: { + "]".utf8 + } + } +} + +/// This benchmark shows how to create a naive JSON parser with combinators. +/// +/// It is mostly implemented according to the [spec](https://www.json.org/json-en.html) (we take a +/// shortcut and use `Double.parser()`, which behaves accordingly). +let jsonSuite = BenchmarkSuite(name: "JSON") { suite in + let json = JSONValue() + let input = #""" + { + "hello": true, + "goodbye": 42.42, + "whatever": null, + "xs": [1, "hello", null, false], + "ys": { + "0": 2, + "1": "goodbye\n" } } + """# + var jsonOutput: JSONValue.Output! + suite.benchmark("Parser") { + var input = input[...].utf8 + jsonOutput = try json.parse(&input) + } tearDown: { + precondition( + jsonOutput + == .object([ + "hello": .boolean(true), + "goodbye": .number(42.42), + "whatever": .null, + "xs": .array([.number(1), .string("hello"), .null, .boolean(false)]), + "ys": .object([ + "0": .number(2), + "1": .string("goodbye\n"), + ]), + ]) + ) + precondition( + try! Substring(json.print(jsonOutput)) + == """ + {\ + "goodbye":42.42,\ + "hello":true,\ + "whatever":null,\ + "xs":[1.0,"hello",null,false],\ + "ys":{"0":2.0,"1":"goodbye\\n"}\ + } + """ + ) + precondition(try! json.parse(json.print(jsonOutput)) == jsonOutput) + } - let json = JSONValue() - let input = #""" - { + let dataInput = Data(input.utf8) + var objectOutput: Any! + suite.benchmark("JSONSerialization") { + objectOutput = try JSONSerialization.jsonObject(with: dataInput, options: []) + } tearDown: { + precondition( + (objectOutput as! NSDictionary) == [ "hello": true, "goodbye": 42.42, - "whatever": null, - "xs": [1, "hello", null, false], - "ys": { + "whatever": NSNull(), + "xs": [1, "hello", nil, false] as [Any?], + "ys": [ "0": 2, - "1": "goodbye\n" - } - } - """# - var jsonOutput: JSONValue.Output! - suite.benchmark("Parser") { - var input = input[...].utf8 - jsonOutput = try json.parse(&input) - } tearDown: { - precondition( - jsonOutput - == .object([ - "hello": .boolean(true), - "goodbye": .number(42.42), - "whatever": .null, - "xs": .array([.number(1), .string("hello"), .null, .boolean(false)]), - "ys": .object([ - "0": .number(2), - "1": .string("goodbye\n"), - ]), - ]) - ) - precondition( - try! Substring(json.print(jsonOutput)) - == """ - {\ - "goodbye":42.42,\ - "hello":true,\ - "whatever":null,\ - "xs":[1.0,"hello",null,false],\ - "ys":{"0":2.0,"1":"goodbye\\n"}\ - } - """ - ) - precondition(try! json.parse(json.print(jsonOutput)) == jsonOutput) - } - - let dataInput = Data(input.utf8) - var objectOutput: Any! - suite.benchmark("JSONSerialization") { - objectOutput = try JSONSerialization.jsonObject(with: dataInput, options: []) - } tearDown: { - precondition( - (objectOutput as! NSDictionary) == [ - "hello": true, - "goodbye": 42.42, - "whatever": NSNull(), - "xs": [1, "hello", nil, false] as [Any?], - "ys": [ - "0": 2, - "1": "goodbye\n", - ] as [String: Any], - ] - ) - } - #endif + "1": "goodbye\n", + ] as [String: Any], + ] + ) + } } extension UTF8.CodeUnit { diff --git a/Tests/ParsingTests/OneOfTests.swift b/Tests/ParsingTests/OneOfTests.swift index 9c77ec68d7..0ae7c25ebe 100644 --- a/Tests/ParsingTests/OneOfTests.swift +++ b/Tests/ParsingTests/OneOfTests.swift @@ -1,3 +1,4 @@ +import CasePaths import Parsing import XCTest @@ -207,172 +208,171 @@ final class OneOfTests: XCTestCase { } } - #if swift(>=5.8) - func testJSON() { - struct JSONValue: ParserPrinter { - enum Output: Equatable { - case array([Self]) - case boolean(Bool) - case null - case number(Double) - case object([String: Self]) - case string(String) - } - - var body: some ParserPrinter { - Whitespace() - OneOf { - JSONObject().map(.case(Output.object)) - JSONArray().map(.case(Output.array)) - JSONString().map(.case(Output.string)) - Double.parser().map(.case(Output.number)) - Bool.parser().map(.case(Output.boolean)) - "null".utf8.map { Output.null } - } - Whitespace() + func testJSON() { + let input = #""" + { + "hello": true, + "goodbye": 42.42, + "whatever": null, + "xs": [1, "hello, null, false], + "ys": { + "0": 2, + "1": "goodbye" } } + """# - struct JSONString: ParserPrinter { - var body: some ParserPrinter { - "\"".utf8 - Many(into: "") { string, fragment in - string.append(contentsOf: fragment) - } decumulator: { string in - string.map(String.init).reversed().makeIterator() - } element: { - OneOf { - Prefix(1) { $0.isUnescapedJSONStringByte }.map(.string) - - Parse { - "\\".utf8 - - OneOf { - "\"".utf8.map { "\"" } - "\\".utf8.map { "\\" } - "/".utf8.map { "/" } - "b".utf8.map { "\u{8}" } - "f".utf8.map { "\u{c}" } - "n".utf8.map { "\n" } - "r".utf8.map { "\r" } - "t".utf8.map { "\t" } - ParsePrint(.unicode) { - Prefix(4) { $0.isHexDigit } - } - } - } - } - } terminator: { - "\"".utf8 - } - } - } + XCTAssertThrowsError(try JSONValue().parse(input)) { error in + XCTAssertEqual( + #""" + error: multiple failures occurred - struct JSONObject: ParserPrinter { - var body: some ParserPrinter { - "{".utf8 - Many(into: [String: JSONValue.Output]()) { - (object: inout [String: JSONValue.Output], pair: (String, JSONValue.Output)) in - let (name, value) = pair - object[name] = value - } decumulator: { object in - (object.sorted(by: { $0.key < $1.key }) as [(String, JSONValue.Output)]) - .reversed() - .makeIterator() - } element: { - Whitespace() - JSONString() - Whitespace() - ":".utf8 - JSONValue() - } separator: { - ",".utf8 - } terminator: { - "}".utf8 - } - } - } + error: unexpected input + --> input:5:34 + 5 | …hello, null, false], + | ^ expected 1 element satisfying predicate + | ^ expected "\\" + | ^ expected "\"" - struct JSONArray: ParserPrinter { - var body: some ParserPrinter { - "[".utf8 - Many { - JSONValue() - } separator: { - ",".utf8 - } terminator: { - "]".utf8 - } - } - } + error: unexpected input + --> input:5:13 + 5 | "xs": [1, "hello, null, false], + | ^ expected "{" + | ^ expected "[" + | ^ expected double + | ^ expected "true" or "false" + | ^ expected "null" + + error: unexpected input + --> input:5:11 + 5 | "xs": [1, "hello, null, false], + | ^ expected "]" - let input = #""" - { - "hello": true, - "goodbye": 42.42, - "whatever": null, - "xs": [1, "hello, null, false], - "ys": { - "0": 2, - "1": "goodbye" + error: unexpected input + --> input:5:9 + 5 | "xs": [1, "hello, null, false], + | ^ expected "{" + | ^ expected "\"" + | ^ expected double + | ^ expected "true" or "false" + | ^ expected "null" + + error: unexpected input + --> input:4:19 + 4 | "whatever": null, + | ^ expected "}" + + error: unexpected input + --> input:1:1 + 1 | { + | ^ expected "[" + | ^ expected "\"" + | ^ expected double + | ^ expected "true" or "false" + | ^ expected "null" + """#, + "\(error)" + ) + } + } +} + +struct JSONValue: ParserPrinter { + @CasePathable + enum Output: Equatable { + case array([Self]) + case boolean(Bool) + case null + case number(Double) + case object([String: Self]) + case string(String) + } + + var body: some ParserPrinter { + Whitespace() + OneOf { + JSONObject().map(.case(\Output.AllCasePaths.object)) + JSONArray().map(.case(\.array)) + JSONString().map(.case(\.string)) + Double.parser().map(.case(\.number)) + Bool.parser().map(.case(\.boolean)) + "null".utf8.map { Output.null } + } + Whitespace() + } +} + +struct JSONString: ParserPrinter { + var body: some ParserPrinter { + "\"".utf8 + Many(into: "") { string, fragment in + string.append(contentsOf: fragment) + } decumulator: { string in + string.map(String.init).reversed().makeIterator() + } element: { + OneOf { + Prefix(1) { $0.isUnescapedJSONStringByte }.map(.string) + + Parse { + "\\".utf8 + + OneOf { + "\"".utf8.map { "\"" } + "\\".utf8.map { "\\" } + "/".utf8.map { "/" } + "b".utf8.map { "\u{8}" } + "f".utf8.map { "\u{c}" } + "n".utf8.map { "\n" } + "r".utf8.map { "\r" } + "t".utf8.map { "\t" } + ParsePrint(.unicode) { + Prefix(4) { $0.isHexDigit } + } } } - """# - - XCTAssertThrowsError(try JSONValue().parse(input)) { error in - XCTAssertEqual( - #""" - error: multiple failures occurred - - error: unexpected input - --> input:5:34 - 5 | …hello, null, false], - | ^ expected 1 element satisfying predicate - | ^ expected "\\" - | ^ expected "\"" - - error: unexpected input - --> input:5:13 - 5 | "xs": [1, "hello, null, false], - | ^ expected "{" - | ^ expected "[" - | ^ expected double - | ^ expected "true" or "false" - | ^ expected "null" - - error: unexpected input - --> input:5:11 - 5 | "xs": [1, "hello, null, false], - | ^ expected "]" - - error: unexpected input - --> input:5:9 - 5 | "xs": [1, "hello, null, false], - | ^ expected "{" - | ^ expected "\"" - | ^ expected double - | ^ expected "true" or "false" - | ^ expected "null" - - error: unexpected input - --> input:4:19 - 4 | "whatever": null, - | ^ expected "}" - - error: unexpected input - --> input:1:1 - 1 | { - | ^ expected "[" - | ^ expected "\"" - | ^ expected double - | ^ expected "true" or "false" - | ^ expected "null" - """#, - "\(error)" - ) } + } terminator: { + "\"".utf8 + } + } +} + +struct JSONObject: ParserPrinter { + var body: some ParserPrinter { + "{".utf8 + Many(into: [String: JSONValue.Output]()) { + (object: inout [String: JSONValue.Output], pair: (String, JSONValue.Output)) in + let (name, value) = pair + object[name] = value + } decumulator: { object in + (object.sorted(by: { $0.key < $1.key }) as [(String, JSONValue.Output)]) + .reversed() + .makeIterator() + } element: { + Whitespace() + JSONString() + Whitespace() + ":".utf8 + JSONValue() + } separator: { + ",".utf8 + } terminator: { + "}".utf8 + } + } +} + +struct JSONArray: ParserPrinter { + var body: some ParserPrinter { + "[".utf8 + Many { + JSONValue() + } separator: { + ",".utf8 + } terminator: { + "]".utf8 } - #endif + } } extension UTF8.CodeUnit {