diff --git a/Package.swift b/Package.swift index 7de7c06d9bc..d185bd43db7 100644 --- a/Package.swift +++ b/Package.swift @@ -30,6 +30,9 @@ let package = Package( ], targets: [ // MARK: - Internal helper targets + .target( + name: "_SwiftSyntaxCShims" + ), .target( name: "_AtomicBool" @@ -74,7 +77,7 @@ let package = Package( .target( name: "SwiftCompilerPlugin", - dependencies: ["SwiftCompilerPluginMessageHandling", "SwiftSyntaxMacros"], + dependencies: ["SwiftCompilerPluginMessageHandling", "SwiftSyntaxMacros", "_SwiftSyntaxCShims"], exclude: ["CMakeLists.txt"] ), diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index f5df62c776e..495ba51db5f 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -6,6 +6,7 @@ # See http://swift.org/LICENSE.txt for license information # See http://swift.org/CONTRIBUTORS.txt for Swift project authors +add_subdirectory(_SwiftSyntaxCShims) add_subdirectory(SwiftBasicFormat) add_subdirectory(SwiftSyntax) add_subdirectory(SwiftDiagnostics) diff --git a/Sources/SwiftCompilerPlugin/CMakeLists.txt b/Sources/SwiftCompilerPlugin/CMakeLists.txt index 1e69e0b690c..4552f3ef02d 100644 --- a/Sources/SwiftCompilerPlugin/CMakeLists.txt +++ b/Sources/SwiftCompilerPlugin/CMakeLists.txt @@ -13,4 +13,6 @@ add_swift_syntax_library(SwiftCompilerPlugin target_link_swift_syntax_libraries(SwiftCompilerPlugin PUBLIC SwiftSyntaxMacros - SwiftCompilerPluginMessageHandling) + SwiftCompilerPluginMessageHandling + _SwiftSyntaxCShims +) diff --git a/Sources/SwiftCompilerPlugin/CompilerPlugin.swift b/Sources/SwiftCompilerPlugin/CompilerPlugin.swift index d5dd1a92b5b..21337467b03 100644 --- a/Sources/SwiftCompilerPlugin/CompilerPlugin.swift +++ b/Sources/SwiftCompilerPlugin/CompilerPlugin.swift @@ -13,19 +13,25 @@ // https://github.com/apple/swift-package-manager/blob/main/Sources/PackagePlugin/Plugin.swift #if swift(>=6.0) +private import _SwiftSyntaxCShims public import SwiftSyntaxMacros -private import Foundation @_spi(PluginMessage) private import SwiftCompilerPluginMessageHandling +#if canImport(Darwin) +private import Darwin +#elseif canImport(Glibc) +private import Glibc +#elseif canImport(ucrt) +private import ucrt +#endif #else +import _SwiftSyntaxCShims import SwiftSyntaxMacros -import Foundation @_spi(PluginMessage) import SwiftCompilerPluginMessageHandling -#endif - -#if os(Windows) -#if swift(>=6.0) -private import ucrt -#else +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(ucrt) import ucrt #endif #endif @@ -86,7 +92,7 @@ extension CompilerPlugin { } } - let pluginPath = CommandLine.arguments.first ?? Bundle.main.executablePath ?? ProcessInfo.processInfo.processName + let pluginPath = CommandLine.arguments.first ?? "" throw CompilerPluginError( message: "macro implementation type '\(moduleName).\(typeName)' could not be found in executable plugin '\(pluginPath)'" @@ -94,6 +100,13 @@ extension CompilerPlugin { } } +struct CompilerPluginError: Error, CustomStringConvertible { + var description: String + init(message: String) { + self.description = message + } +} + struct MacroProviderAdapter: PluginProvider { let plugin: Plugin init(plugin: Plugin) { @@ -104,53 +117,61 @@ struct MacroProviderAdapter: PluginProvider { } } +#if canImport(ucrt) +private let dup = _dup(_:) +private let fileno = _fileno(_:) +private let dup2 = _dup2(_:_:) +private let close = _close(_:) +private let read = _read(_:_:_:) +private let write = _write(_:_:_:) +#endif + extension CompilerPlugin { /// Main entry point of the plugin — sets up a communication channel with /// the plugin host and runs the main message loop. public static func main() throws { + let stdin = _ss_stdin() + let stdout = _ss_stdout() + let stderr = _ss_stderr() + // Duplicate the `stdin` file descriptor, which we will then use for // receiving messages from the plugin host. let inputFD = dup(fileno(stdin)) guard inputFD >= 0 else { - internalError("Could not duplicate `stdin`: \(describe(errno: errno)).") + internalError("Could not duplicate `stdin`: \(describe(errno: _ss_errno())).") } // Having duplicated the original standard-input descriptor, we close // `stdin` so that attempts by the plugin to read console input (which // are usually a mistake) return errors instead of blocking. guard close(fileno(stdin)) >= 0 else { - internalError("Could not close `stdin`: \(describe(errno: errno)).") + internalError("Could not close `stdin`: \(describe(errno: _ss_errno())).") } // Duplicate the `stdout` file descriptor, which we will then use for // sending messages to the plugin host. let outputFD = dup(fileno(stdout)) guard outputFD >= 0 else { - internalError("Could not dup `stdout`: \(describe(errno: errno)).") + internalError("Could not dup `stdout`: \(describe(errno: _ss_errno())).") } // Having duplicated the original standard-output descriptor, redirect // `stdout` to `stderr` so that all free-form text output goes there. guard dup2(fileno(stderr), fileno(stdout)) >= 0 else { - internalError("Could not dup2 `stdout` to `stderr`: \(describe(errno: errno)).") + internalError("Could not dup2 `stdout` to `stderr`: \(describe(errno: _ss_errno())).") } - // Turn off full buffering so printed text appears as soon as possible. - // Windows is much less forgiving than other platforms. If line - // buffering is enabled, we must provide a buffer and the size of the - // buffer. As a result, on Windows, we completely disable all - // buffering, which means that partial writes are possible. - #if os(Windows) - setvbuf(stdout, nil, _IONBF, 0) - #else - setvbuf(stdout, nil, _IOLBF, 0) + #if canImport(ucrt) + // Set I/O to binary mode. Avoid CRLF translation, and Ctrl+Z (0x1A) as EOF. + _ = _setmode(inputFD, _O_BINARY) + _ = _setmode(outputFD, _O_BINARY) #endif // Open a message channel for communicating with the plugin host. let connection = PluginHostConnection( - inputStream: FileHandle(fileDescriptor: inputFD), - outputStream: FileHandle(fileDescriptor: outputFD) + inputStream: inputFD, + outputStream: outputFD ) // Handle messages from the host until the input stream is closed, @@ -168,95 +189,95 @@ extension CompilerPlugin { // Private function to report internal errors and then exit. fileprivate static func internalError(_ message: String) -> Never { - fputs("Internal Error: \(message)\n", stderr) + fputs("Internal Error: \(message)\n", _ss_stderr()) exit(1) } - - // Private function to construct an error message from an `errno` code. - fileprivate static func describe(errno: Int32) -> String { - if let cStr = strerror(errno) { return String(cString: cStr) } - return String(describing: errno) - } } internal struct PluginHostConnection: MessageConnection { - fileprivate let inputStream: FileHandle - fileprivate let outputStream: FileHandle + // File descriptor for input from the host. + fileprivate let inputStream: CInt + // File descriptor for output to the host. + fileprivate let outputStream: CInt func sendMessage(_ message: TX) throws { // Encode the message as JSON. - let payload = try JSONEncoder().encode(message) + let payload = try JSON.encode(message) // Write the header (a 64-bit length field in little endian byte order). - var count = UInt64(payload.count).littleEndian - let header = Swift.withUnsafeBytes(of: &count) { Data($0) } - precondition(header.count == 8) + let count = payload.count + var header = UInt64(count).littleEndian + try withUnsafeBytes(of: &header) { try _write(outputStream, contentsOf: $0) } - // Write the header and payload. - try outputStream._write(contentsOf: header) - try outputStream._write(contentsOf: payload) + // Write the JSON payload. + try payload.withUnsafeBytes { try _write(outputStream, contentsOf: $0) } } func waitForNextMessage(_ ty: RX.Type) throws -> RX? { // Read the header (a 64-bit length field in little endian byte order). - guard - let header = try inputStream._read(upToCount: 8), - header.count != 0 - else { + var header: UInt64 = 0 + do { + try withUnsafeMutableBytes(of: &header) { try _read(inputStream, into: $0) } + } catch IOError.readReachedEndOfInput { + // Connection closed. return nil } - guard header.count == 8 else { - throw PluginMessageError.truncatedHeader - } - - // Decode the count. - let count = header.withUnsafeBytes { - UInt64(littleEndian: $0.loadUnaligned(as: UInt64.self)) - } - guard count >= 2 else { - throw PluginMessageError.invalidPayloadSize - } // Read the JSON payload. - guard - let payload = try inputStream._read(upToCount: Int(count)), - payload.count == count - else { - throw PluginMessageError.truncatedPayload - } + let count = Int(UInt64(littleEndian: header)) + let data = UnsafeMutableRawBufferPointer.allocate(byteCount: count, alignment: 1) + defer { data.deallocate() } + try _read(inputStream, into: data) // Decode and return the message. - return try JSONDecoder().decode(RX.self, from: payload) + return try JSON.decode(ty, from: UnsafeBufferPointer(data.bindMemory(to: UInt8.self))) } +} - enum PluginMessageError: Swift.Error { - case truncatedHeader - case invalidPayloadSize - case truncatedPayload +/// Write the buffer to the file descriptor. Throws an error on failure. +private func _write(_ fd: CInt, contentsOf buffer: UnsafeRawBufferPointer) throws { + guard var ptr = buffer.baseAddress else { return } + let endPtr = ptr.advanced(by: buffer.count) + while ptr != endPtr { + switch write(fd, ptr, numericCast(endPtr - ptr)) { + case -1: throw IOError.writeFailed(errno: _ss_errno()) + case 0: throw IOError.writeFailed(errno: 0) /* unreachable */ + case let n: ptr += Int(n) + } } } -private extension FileHandle { - func _write(contentsOf data: Data) throws { - if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { - return try self.write(contentsOf: data) - } else { - return self.write(data) +/// Fill the buffer from the file descriptor. Throws an error on failure. +/// If the file descriptor reached the end-of-file before filling up the entire +/// buffer, throws IOError.readReachedEndOfInput +private func _read(_ fd: CInt, into buffer: UnsafeMutableRawBufferPointer) throws { + guard var ptr = buffer.baseAddress else { return } + let endPtr = ptr.advanced(by: buffer.count) + while ptr != endPtr { + switch read(fd, ptr, numericCast(endPtr - ptr)) { + case -1: throw IOError.readFailed(errno: _ss_errno()) + case 0: throw IOError.readReachedEndOfInput + case let n: ptr += Int(n) } } +} - func _read(upToCount count: Int) throws -> Data? { - if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { - return try self.read(upToCount: count) - } else { - return self.readData(ofLength: 8) +private enum IOError: Error, CustomStringConvertible { + case readReachedEndOfInput + case readFailed(errno: CInt) + case writeFailed(errno: CInt) + + var description: String { + switch self { + case .readReachedEndOfInput: "read(2) reached end-of-file" + case .readFailed(let errno): "read(2) failed: \(describe(errno: errno))" + case .writeFailed(let errno): "write(2) failed: \(describe(errno: errno))" } } } -struct CompilerPluginError: Error, CustomStringConvertible { - var description: String - init(message: String) { - self.description = message - } +// Private function to construct an error message from an `errno` code. +private func describe(errno: CInt) -> String { + if let cStr = strerror(errno) { return String(cString: cStr) } + return String(describing: errno) } diff --git a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt index f01fc1f2f15..5252fb645fe 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt +++ b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt @@ -13,6 +13,10 @@ add_swift_syntax_library(SwiftCompilerPluginMessageHandling PluginMacroExpansionContext.swift PluginMessageCompatibility.swift PluginMessages.swift + JSON/CodingUtilities.swift + JSON/JSON.swift + JSON/JSONDecoding.swift + JSON/JSONEncoding.swift ) target_link_swift_syntax_libraries(SwiftCompilerPluginMessageHandling PUBLIC diff --git a/Sources/SwiftCompilerPluginMessageHandling/JSON/CodingUtilities.swift b/Sources/SwiftCompilerPluginMessageHandling/JSON/CodingUtilities.swift new file mode 100644 index 00000000000..b423f7caea4 --- /dev/null +++ b/Sources/SwiftCompilerPluginMessageHandling/JSON/CodingUtilities.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// Copied from swift-foundation + +//===----------------------------------------------------------------------===// +// Coding Path Node +//===----------------------------------------------------------------------===// + +// This construction allows overall fewer and smaller allocations as the coding path is modified. +internal enum _CodingPathNode { + case root + indirect case node(CodingKey, _CodingPathNode) + indirect case indexNode(Int, _CodingPathNode) + + var path: [any CodingKey] { + switch self { + case .root: + return [] + case let .node(key, parent): + return parent.path + [key] + case let .indexNode(index, parent): + return parent.path + [_CodingKey(index: index)] + } + } + + @inline(__always) + func appending(_ key: __owned (some CodingKey)?) -> _CodingPathNode { + if let key { + return .node(key, self) + } else { + return self + } + } + + @inline(__always) + func path(byAppending key: __owned (some CodingKey)?) -> [CodingKey] { + if let key { + return self.path + [key] + } + return self.path + } + + // Specializations for indexes, commonly used by unkeyed containers. + @inline(__always) + func appending(index: __owned Int) -> _CodingPathNode { + .indexNode(index, self) + } + + func path(byAppendingIndex index: __owned Int) -> [CodingKey] { + self.path + [_CodingKey(index: index)] + } +} + +//===----------------------------------------------------------------------===// +// Shared Key Type +//===----------------------------------------------------------------------===// + +internal enum _CodingKey: CodingKey { + case string(String) + case int(Int) + case index(Int) + + @inline(__always) + public init?(stringValue: String) { + self = .string(stringValue) + } + + @inline(__always) + public init?(intValue: Int) { + self = .int(intValue) + } + + @inline(__always) + internal init(index: Int) { + self = .index(index) + } + + var stringValue: String { + switch self { + case let .string(str): return str + case let .int(int): return "\(int)" + case let .index(index): return "Index \(index)" + } + } + + var intValue: Int? { + switch self { + case .string: return nil + case let .int(int): return int + case let .index(index): return index + } + } +} diff --git a/Sources/SwiftCompilerPluginMessageHandling/JSON/JSON.swift b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSON.swift new file mode 100644 index 00000000000..a0eb47a0925 --- /dev/null +++ b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSON.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(PluginMessage) +public enum JSON { + /// Encode Swift value to an UInt8 array. + public static func encode(_ value: T) throws -> [UInt8] { + try encodeToJSON(value: value) + } + + /// Decode a JSON data to a Swift value. + public static func decode(_ type: T.Type, from json: UnsafeBufferPointer) throws -> T { + try decodeFromJSON(json: json) + } +} diff --git a/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift new file mode 100644 index 00000000000..041348d658f --- /dev/null +++ b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift @@ -0,0 +1,1328 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if swift(>=6.0) +#if canImport(Darwin) +private import Darwin +#elseif canImport(Glibc) +private import Glibc +#elseif canImport(ucrt) +private import ucrt +#endif +#else +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(ucrt) +import ucrt +#endif +#endif + +func decodeFromJSON(json: UnsafeBufferPointer) throws -> T { + let map: JSONMap + do { + map = try JSONScanner.scan(buffer: json) + } catch let err as JSONError { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: [], + debugDescription: "Corrupted JSON", + underlyingError: err + ) + ) + } + return try map.withValue { value in + let decoder = JSONDecoding(value: value, codingPathNode: .root) + return try T.init(from: decoder) + } +} + +/* + JSONMap is inspired by swift-foundation's JSONMap. + + For JSON payload such as: + + ``` + {"foo": [-1.3, true], "barz": 42} + ``` + + will be scanned by 'JSONScanner' into a map like: + + ``` + == Object Marker + == Array Marker + == Simple String (a variant of String that can has no escapes and can be passed directly to a UTF-8 parser) + == Number Marker + == NULL Marker + map: [ + 0: , -- object marker + 1: 17, | `- number of *map* elements this object occupies + 2: , | --- key 1: 'foo' + 3: , | | |- pointer in the payload + 4: 3, | | `- length + 5: , | --- value 1: array + 6: 6, | | `- number of *map* elements this array occupies + 7: , | | -- arr elm 1: '-1.3' + 8: , | | | + 9: 4, | | | + 10: , | | -- arr elm 2: 'true' + 11: , | --- key 2: 'barz' + 12: , | | + 13: 4, | | + 14: | --- value 2: '42' + 15: , | | + 16: 2, | | + ] + ``` + To decode '.barz' value: + 1. Index 0 indicates it's a object. + 2. Parse a key string at index 2, which is "foo", not a match for "barz" + 3. Skip the key and the value by advancing the index by 'mapSize' of them, 3 and 6. + 4. Parse a key string at index 11, matching "barz" + 5. Parse a value number at the pointer of index 15, length at index 16 +*/ + +private struct JSONMap { + enum Descriptor: Int { + + // MARK: - Keywords; mapSize:1 [desc] + // desc: Descriptor.rawValue + + /// 'null' + case nullKeyword + /// 'true' size:1 + case trueKeyword + /// 'false' size:1 + case falseKeyword + + // MARK: - Scalar values; mapSize:3 [desc, pointer, length] + // desc: Descriptor.rawValue + // pointer: pointer to the start of the value in the source UTF-8 JSON buffer. + // length: the length of the value in the source UTF-8 JSON buffer. + + /// Integer and floating number. + case number + /// ASCII non-escaped string. + case asciiSimpleString + /// Non escaped string. + case simpleString + /// String with escape sequences. + case string + + // MARK: - Collections; mapSize: 2 + variable [desc, size, element...] + // desc: Descriptor.rawValue + // size: the map size this collection occupies. + // element * n: JSON values in this collection. For collections, sequence of key/value pairs. + + /// Object '{ ... }'. Elements are (key, value)... + case object + /// Array '[ ... ]'. + case array + } + let data: [Int] + + /// Top-level value. + func withValue(_ body: (JSONMapValue) throws -> T) rethrows -> T { + try data.withUnsafeBufferPointer { buf in + try body(JSONMapValue(data: buf.baseAddress!)) + } + } +} + +private struct JSONMapBuilder { + var mapData: [Int] + + init() { + mapData = [] + mapData.reserveCapacity(128) // 128 is good enough for most PluginMessage. + } + + /// Record .nullKeyword, .trueKeyword, or .falseKeyword. + @inline(__always) + mutating func record(_ descriptor: JSONMap.Descriptor) { + mapData.append(descriptor.rawValue) + } + + /// Record literal values i.e. numbers and strings, with a range in the source buffer. + @inline(__always) + mutating func record(_ descriptor: JSONMap.Descriptor, range: Range>) { + mapData.append(descriptor.rawValue) + mapData.append(Int(bitPattern: range.lowerBound)) + mapData.append(range.count) + } + + /// Record starting of a collection i.e. .array or .object. Must be paired with + /// closeCollection(handle:) call using the returned handle. + @inline(__always) + mutating func startCollection(_ descriptor: JSONMap.Descriptor) -> Int { + let handle = mapData.count + mapData.append(descriptor.rawValue) + mapData.append(0) // Count, this will be updated in closeCollection() + return handle + } + + /// Close the collection. Accepts a "handle" returned from startCollection(_:). + @inline(__always) + mutating func closeCollection(handle: Int) { + // 'handle': descriptor index. + // 'handle+1': counter index. + mapData[handle + 1] = mapData.count - handle + } + + func finalize() -> JSONMap { + JSONMap(data: mapData) + } +} + +private enum JSONError: Error, CustomStringConvertible { + case unexpectedEndOfFile + case unexpectedCharacter(UInt8, context: String) + + var description: String { + switch self { + case .unexpectedEndOfFile: + return "unexpected end of file" + case .unexpectedCharacter(let c, let ctxt): + let char = c < 0x80 ? String(UnicodeScalar(c)) : "0x" + String(c, radix: 16, uppercase: true) + return "unexpected character '\(char)'; \(ctxt)" + } + } +} + +private struct JSONScanner { + typealias Cursor = UnsafePointer + + let endPtr: Cursor + var ptr: Cursor + var map: JSONMapBuilder + + init(buffer: UnsafeBufferPointer) { + self.ptr = buffer.baseAddress! + self.endPtr = buffer.baseAddress! + buffer.count + self.map = JSONMapBuilder() + } + + var hasData: Bool { + ptr != endPtr + } + + @inline(__always) + mutating func skipWhitespace() { + while hasData { + switch ptr.pointee { + case UInt8(ascii: " "), UInt8(ascii: "\t"), UInt8(ascii: "\n"), UInt8(ascii: "\r"): + ptr += 1 + default: + return + } + } + } + + @inline(__always) + mutating func advance() throws -> UInt8 { + guard hasData else { + throw JSONError.unexpectedEndOfFile + } + let value = ptr.pointee + ptr += 1 + return value + } + + @inline(__always) + mutating func advance(if char: UnicodeScalar) -> Bool { + guard hasData, ptr.pointee == UInt8(ascii: char) else { + return false + } + ptr += 1 + return true + } + + @inline(__always) + mutating func advance(if range: ClosedRange) -> Bool { + guard hasData, range.contains(UnicodeScalar(ptr.pointee)) else { + return false + } + ptr += 1 + return true + } + + @inline(__always) + mutating func expect(ascii char: UnicodeScalar) throws { + guard hasData else { + throw JSONError.unexpectedEndOfFile + } + guard ptr.pointee == UInt8(ascii: char) else { + throw JSONError.unexpectedCharacter(ptr.pointee, context: "expected '\(char)'") + } + ptr += 1 + } + + mutating func scanNull() throws { + try expect(ascii: "u") + try expect(ascii: "l") + try expect(ascii: "l") + map.record(.nullKeyword) + } + + mutating func scanTrue() throws { + try expect(ascii: "r") + try expect(ascii: "u") + try expect(ascii: "e") + map.record(.trueKeyword) + } + + mutating func scanFalse() throws { + try expect(ascii: "a") + try expect(ascii: "l") + try expect(ascii: "s") + try expect(ascii: "e") + map.record(.falseKeyword) + } + + mutating func scanString(start: Cursor) throws { + ptr = start + try expect(ascii: "\"") + + var hasEscape = false + var hasNonASCII = false + while hasData && ptr.pointee != UInt8(ascii: "\"") { + // FIXME: Error for non-escaped control characters. + // FIXME: Error for invalid UTF8 sequences. + if ptr.pointee == UInt8(ascii: "\\") { + hasEscape = true + // eat '\'. Rest of the escape sequence are all ASCII. We just skip them + // ignoring how many bytes are actually for the escape sequence. For + // decoding, they are revisited in _JSONStingDecoder.decodeStringWithEscapes() + _ = try advance() + } else if ptr.pointee >= 0x80 { + hasNonASCII = true + } + _ = try advance() + } + try expect(ascii: "\"") + + let kind: JSONMap.Descriptor + if hasEscape { + kind = .string + } else if hasNonASCII { + kind = .simpleString + } else { + kind = .asciiSimpleString + } + map.record(kind, range: (start + 1)..<(ptr - 1)) + } + + mutating func scanNumber(start: Cursor) throws { + // FIXME: Error for invalid literal e.g. 'e-', '.e+' + ptr = start + _ = advance(if: "-") + while advance(if: "0"..."9") {} + if advance(if: ".") { + while advance(if: "0"..."9") {} + } + if advance(if: "e") || advance(if: "E") { + _ = advance(if: "-") || advance(if: "+") + while advance(if: "0"..."9") {} + } + map.record(.number, range: start..) throws -> JSONMap { + var scanner = JSONScanner(buffer: buffer) + try scanner.scanValue() + if scanner.hasData { + throw JSONError.unexpectedCharacter(scanner.ptr.pointee, context: "after top-level value") + } + + return scanner.map.finalize() + } +} + +/// Represents a single value in a JSONMap. +private struct JSONMapValue { + typealias Pointer = UnsafePointer + /// Pointer to an element descriptor in the map. + let data: Pointer + + @inline(__always) + var endPtr: Pointer { + data.advanced(by: mapSize) + } + + /// The size of this value data in the map. + @inline(__always) + var mapSize: Int { + switch JSONMap.Descriptor(rawValue: data[0]) { + case .nullKeyword, .trueKeyword, .falseKeyword: + return 1 + case .number, .asciiSimpleString, .simpleString, .string: + return 3 + case .array, .object: + return data[1] + case nil: + fatalError("invalid value descriptor") + } + } + + @inline(__always) + func `is`(_ kind: JSONMap.Descriptor) -> Bool { + return data[0] == kind.rawValue + } +} + +// MARK: Keyword primitives +extension JSONMapValue { + @inline(__always) + var isNull: Bool { + return self.is(.nullKeyword) + } + + @inline(__always) + func asBool() -> Bool? { + if self.is(.trueKeyword) { + return true + } + if self.is(.falseKeyword) { + return false + } + return nil + } +} + +// MARK: Scalar values +private enum _JSONStringParser { + /// Decode a non-escaped string value from the buffer. + @inline(__always) + static func decodeSimpleString(source: UnsafeBufferPointer) -> String { + if source.count <= 0 { + return "" + } + return _makeString(unsafeUninitializedCapacity: source.count) { buffer in + buffer.initialize(fromContentsOf: source) + } + } + + /// Decode a string value that includes escape sequences. + static func decodeStringWithEscapes(source: UnsafeBufferPointer) -> String? { + // JSON string with escape sequences must be== 0 2 bytes or longer. + assert(!source.isEmpty) + + // Decode 'source' UTF-8 JSON string literal into the uninitialized + // UTF-8 buffer. Upon error, return 0 and make an empty string. + let decoded = _makeString(unsafeUninitializedCapacity: source.count) { buffer in + + var cursor = source.baseAddress! + let end = cursor + source.count + var mark = cursor + + var dest = buffer.baseAddress! + + @inline(__always) func flush() { + let count = mark.distance(to: cursor) + dest.initialize(from: mark, count: count) + dest += count + } + + while cursor != end { + if cursor.pointee != UInt8(ascii: "\\") { + cursor += 1 + continue + } + + // Found an escape sequence. Flush the skipped source into the buffer. + flush() + + let hadError = decodeEscapeSequence(cursor: &cursor, end: end) { + dest.initialize(to: $0) + dest += 1 + } + guard !hadError else { return 0 } + + // Mark the position of the end of the escape sequence. + mark = cursor + } + + // Flush the remaining non-escaped characters. + flush() + + return buffer.baseAddress!.distance(to: dest) + } + + // If any error is detected, empty string is created. + return decoded.isEmpty ? nil : decoded + } + + /// Decode a JSON escape sequence, advance 'cursor' to end of the escape + /// sequence, and call 'processCodeUnit' with the decoded value. + /// Returns 'true' on error. + /// + /// - Note: We don't report detailed errors for now because we only care + /// well-formed payloads from the compiler. + private static func decodeEscapeSequence( + cursor: inout UnsafePointer, + end: UnsafePointer, + into processCodeUnit: (UInt8) -> Void + ) -> Bool { + assert(cursor.pointee == UInt8(ascii: "\\")) + guard cursor.distance(to: end) >= 2 else { return true } + + // Eat backslash and the next character. + cursor += 2 + switch cursor[-1] { + case UInt8(ascii: "\""): processCodeUnit(UInt8(ascii: "\"")) + case UInt8(ascii: "'"): processCodeUnit(UInt8(ascii: "'")) + case UInt8(ascii: "\\"): processCodeUnit(UInt8(ascii: "\\")) + case UInt8(ascii: "/"): processCodeUnit(UInt8(ascii: "/")) + case UInt8(ascii: "b"): processCodeUnit(0x08) + case UInt8(ascii: "f"): processCodeUnit(0x0C) + case UInt8(ascii: "n"): processCodeUnit(0x0A) + case UInt8(ascii: "r"): processCodeUnit(0x0D) + case UInt8(ascii: "t"): processCodeUnit(0x09) + case UInt8(ascii: "u"): + guard cursor.distance(to: end) >= 4 else { return true } + + // Parse 4 hex digits into a UTF-16 code unit. + let result: UInt16? = _JSONNumberParser.parseHexIntegerDigits( + source: UnsafeBufferPointer(start: cursor, count: 4) + ) + guard let result else { return true } + + // Transcode UTF-16 code unit to UTF-8. + // FIXME: Support surrogate pairs. + let hadError = transcode( + CollectionOfOne(result).makeIterator(), + from: UTF16.self, + to: UTF8.self, + stoppingOnError: true, + into: processCodeUnit + ) + guard !hadError else { return true } + cursor += 4 + default: + // invalid escape sequence. + return true + } + return false + } + + /// SwiftStdlib 5.3 compatibility shim for + /// 'String.init(unsafeUninitializedCapacity:initializingUTF8With:)' + private static func _makeString( + unsafeUninitializedCapacity capacity: Int, + initializingUTF8With initializer: (UnsafeMutableBufferPointer) throws -> Int + ) rethrows -> String { + if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) { + return try String(unsafeUninitializedCapacity: capacity, initializingUTF8With: initializer) + } else { + let buffer = UnsafeMutableBufferPointer.allocate(capacity: capacity) + let count = try initializer(buffer) + return String(decoding: buffer[..(source: UnsafeBufferPointer) -> Integer? { + var source = source[...] + let isNegative = source.first == UInt8(ascii: "-") + if isNegative { + source = source.dropFirst() + } + var value: Integer = 0 + var overflowed: Bool = false + while let digit = source.popFirst() { + let digitValue = Integer(truncatingIfNeeded: digit &- UInt8(ascii: "0")) + guard (0...9).contains(digitValue) else { + return nil + } + (value, overflowed) = value.multipliedReportingOverflow(by: 10) + guard !overflowed else { + return nil + } + (value, overflowed) = + isNegative + ? value.subtractingReportingOverflow(digitValue) + : value.addingReportingOverflow(digitValue) + guard !overflowed else { + return nil + } + } + return value + } + + static func parseFloatingPoint(source: UnsafeBufferPointer) -> Floating? { + // Since source is not NUL terminated, we need to make a temporary storage. + // Depending on the length of the source, prepare the buffer on stack or heap, + // then call 'impl(_:)' (defined below) for the actual operation. + if source.count + 1 <= MemoryLayout.size { + var stash: UInt64 = 0 + return withUnsafeMutableBytes(of: &stash) { + $0.withMemoryRebound(to: UInt8.self, impl) + } + } else { + let stash = UnsafeMutableBufferPointer.allocate(capacity: source.count + 1) + defer { stash.deallocate() } + return impl(stash) + } + + func impl(_ stash: UnsafeMutableBufferPointer) -> Floating? { + // Create a NUL terminated string in the stash. + assert(stash.count >= source.count + 1) + let end = stash.initialize(fromContentsOf: source) + stash.initializeElement(at: end, to: 0) + + var endPtr: UnsafeMutablePointer? = nil + let value: Floating? + if Floating.self == Double.self { + value = Floating(exactly: strtod(stash.baseAddress!, &endPtr)) + } else if Floating.self == Float.self { + value = Floating(exactly: strtof(stash.baseAddress!, &endPtr)) + } else { + preconditionFailure("unsupported floating point type") + } + guard let endPtr, endPtr == stash.baseAddress! + source.count else { + return nil + } + return value + } + } + + static func parseHexIntegerDigits(source: UnsafeBufferPointer) -> Integer? { + var source = source[...] + var value: Integer = 0 + var overflowed: Bool = false + while let digit = source.popFirst() { + let digitValue: Integer + switch digit { + case UInt8(ascii: "0")...UInt8(ascii: "9"): + digitValue = Integer(truncatingIfNeeded: digit &- UInt8(ascii: "0")) + case UInt8(ascii: "a")...UInt8(ascii: "f"): + digitValue = Integer(truncatingIfNeeded: digit &- UInt8(ascii: "a") &+ 10) + case UInt8(ascii: "A")...UInt8(ascii: "F"): + digitValue = Integer(truncatingIfNeeded: digit &- UInt8(ascii: "A") &+ 10) + default: + return nil + } + (value, overflowed) = value.multipliedReportingOverflow(by: 16) + guard !overflowed else { + return nil + } + (value, overflowed) = value.addingReportingOverflow(digitValue) + guard !overflowed else { + return nil + } + } + return value + } +} + +extension JSONMapValue { + /// Get value buffer for .number, .string, and .simpleString. + @inline(__always) + func valueBuffer() -> UnsafeBufferPointer { + UnsafeBufferPointer( + start: UnsafePointer(bitPattern: data[1]), + count: data[2] + ) + } + + @inline(__always) + func asString() -> String? { + if self.is(.asciiSimpleString) || self.is(.simpleString) { + return _JSONStringParser.decodeSimpleString(source: valueBuffer()) + } + if self.is(.string) { + return _JSONStringParser.decodeStringWithEscapes(source: valueBuffer()) + } + return nil + } + + /// Returns true if this value represents a string and equals to 'str'. + /// + /// This is faster than 'value.asString() == str' because this doesn't + /// instantiate 'Swift.String' unless there are escaped characters or + /// non-ASCII characters. + func equals(to str: String) -> Bool { + if self.is(.asciiSimpleString) { + let lhs = valueBuffer() + var str = str + return str.withUTF8 { rhs in + if lhs.count != rhs.count { + return false + } + guard let lBase = lhs.baseAddress, let rBase = rhs.baseAddress else { + // If either `baseAddress` is `nil`, both are empty so returns `true`. + return true + } + return memcmp(lBase, rBase, lhs.count) == 0 + } + } + return self.asString() == str + } + + @inline(__always) + func asFloatingPoint(_: Floating.Type) -> Floating? { + return _JSONNumberParser.parseFloatingPoint(source: self.valueBuffer()) + } + + @inline(__always) + func asInteger(_: Integer.Type) -> Integer? { + // FIXME: Support 42.0 as an integer. + return _JSONNumberParser.parseInteger(source: self.valueBuffer()) + } +} + +// MARK: Collection values +extension JSONMapValue { + struct JSONArray: Collection { + let map: JSONMapValue + + var startIndex: Pointer { map.data.advanced(by: 2) } + var endIndex: Pointer { map.endPtr } + func index(after pointer: Pointer) -> Pointer { + self[pointer].endPtr + } + subscript(pointer: Pointer) -> JSONMapValue { + JSONMapValue(data: pointer) + } + } + + @inline(__always) + func asArray() -> JSONArray? { + guard self.is(.array) else { + return nil + } + return JSONArray(map: self) + } + + struct JSONObject { + let map: JSONMapValue + + struct Iterator: IteratorProtocol { + var currPtr: Pointer + let endPtr: Pointer + + @inline(__always) + init(map: JSONMapValue) { + self.currPtr = map.data.advanced(by: 2) + self.endPtr = map.endPtr + } + + mutating func next() -> (key: JSONMapValue, value: JSONMapValue)? { + guard currPtr != endPtr else { + return nil + } + let key = JSONMapValue(data: currPtr) + let val = JSONMapValue(data: key.endPtr) + currPtr = val.endPtr + return (key, val) + } + } + + @inline(__always) + func makeIterator() -> Iterator { + return Iterator(map: map) + } + + @inline(__always) + func find(_ key: String) -> JSONMapValue? { + // Linear search because, unless there are many keys, creating dictionary + // costs more for preparation. I.e. key string allocation and dictionary + // construction. + var iter = makeIterator() + while let elem = iter.next() { + if elem.key.equals(to: key) { + return elem.value + } + } + return nil + } + + @inline(__always) + func contains(key: String) -> Bool { + return find(key) != nil + } + } + + @inline(__always) + func asObject() -> JSONObject? { + guard self.is(.object) else { + return nil + } + return JSONObject(map: self) + } +} + +private struct JSONDecoding { + var value: JSONMapValue + var codingPathNode: _CodingPathNode +} + +// MARK: Pure decoding functions. +extension JSONDecoding { + @inline(__always) + private static func _checkNotNull( + _ value: JSONMapValue, + expectedType: T.Type, + for codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws { + guard !value.isNull else { + throw DecodingError.valueNotFound( + expectedType, + DecodingError.Context( + codingPath: codingPathNode.path(byAppending: additionalKey), + debugDescription: "Cannot get value of type \(expectedType) -- found null value instead" + ) + ) + } + } + + @inline(__always) + static func _unwrapOrThrow( + _ result: T?, + decoding value: JSONMapValue, + codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws -> T { + if let result = result { + return result + } + try _checkNotNull(value, expectedType: T.self, for: codingPathNode, additionalKey) + throw DecodingError.typeMismatch( + T.self, + .init( + codingPath: codingPathNode.path(byAppending: additionalKey), + debugDescription: "type mismatch" + ) + ) + } + + @inline(__always) + static func _decode( + _ value: JSONMapValue, + as _: Bool.Type, + codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws -> Bool { + try _unwrapOrThrow(value.asBool(), decoding: value, codingPathNode: codingPathNode, additionalKey) + } + + @inline(__always) + static func _decode( + _ value: JSONMapValue, + as _: String.Type, + codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws -> String { + try _unwrapOrThrow(value.asString(), decoding: value, codingPathNode: codingPathNode, additionalKey) + } + + @inline(__always) + static func _decode( + _ value: JSONMapValue, + as type: Integer.Type, + codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws -> Integer { + try _unwrapOrThrow(value.asInteger(type), decoding: value, codingPathNode: codingPathNode, additionalKey) + } + + @inline(__always) + static func _decode( + _ value: JSONMapValue, + as type: Floating.Type, + codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws -> Floating { + try _unwrapOrThrow(value.asFloatingPoint(type), decoding: value, codingPathNode: codingPathNode, additionalKey) + } + + static func _decodeGeneric( + _ value: JSONMapValue, + as type: T.Type, + codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws -> T { + let decoder = Self(value: value, codingPathNode: codingPathNode.appending(additionalKey)) + return try T.init(from: decoder) + } +} + +extension JSONDecoding: Decoder { + var codingPath: [any CodingKey] { + codingPathNode.path + } + var userInfo: [CodingUserInfoKey: Any] { [:] } + + fileprivate struct KeyedContainer { + var codingPathNode: _CodingPathNode + var mapping: JSONMapValue.JSONObject + } + + fileprivate struct UnkeyedContainer { + var codingPathNode: _CodingPathNode + var currentIndex: Int + var array: JSONMapValue.JSONArray + var _currMapIdx: JSONMapValue.JSONArray.Index + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + return try KeyedDecodingContainer( + KeyedContainer( + value: value, + codingPathNode: codingPathNode + ) + ) + } + + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { + return try UnkeyedContainer( + value: value, + codingPathNode: codingPathNode + ) + } + + func singleValueContainer() throws -> any SingleValueDecodingContainer { + return self + } +} + +extension JSONDecoding: SingleValueDecodingContainer { + func decodeNil() -> Bool { + value.isNull + } + + func decode(_ type: Bool.Type) throws -> Bool { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: String.Type) throws -> String { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: Double.Type) throws -> Double { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: Float.Type) throws -> Float { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: Int.Type) throws -> Int { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: Int8.Type) throws -> Int8 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: Int16.Type) throws -> Int16 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: Int32.Type) throws -> Int32 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: Int64.Type) throws -> Int64 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: UInt.Type) throws -> UInt { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: UInt8.Type) throws -> UInt8 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: UInt16.Type) throws -> UInt16 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: UInt32.Type) throws -> UInt32 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: UInt64.Type) throws -> UInt64 { + try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } + + func decode(_ type: T.Type) throws -> T where T: Decodable { + try JSONDecoding._decodeGeneric(value, as: type, codingPathNode: codingPathNode, _CodingKey?.none) + } +} + +extension JSONDecoding.KeyedContainer: KeyedDecodingContainerProtocol { + var codingPath: [any CodingKey] { + codingPathNode.path + } + + var allKeys: [Key] { + var iter = mapping.makeIterator() + var keys: [Key] = [] + while let elem = iter.next() { + if let key = Key(stringValue: elem.key.asString()!) { + keys.append(key) + } + } + return keys + } + + func contains(_ key: Key) -> Bool { + return mapping.contains(key: key.stringValue) + } + + @inline(__always) + func _getOrThrow(forKey key: Key) throws -> JSONMapValue { + if let value = mapping.find(key.stringValue) { + return value + } + throw DecodingError.keyNotFound( + key, + .init( + codingPath: codingPathNode.path, + debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\")." + ) + ) + } + + func decodeNil(forKey key: Key) throws -> Bool { + try _getOrThrow(forKey: key).isNull + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + try JSONDecoding._decode(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T { + try JSONDecoding._decodeGeneric(_getOrThrow(forKey: key), as: type, codingPathNode: codingPathNode, key) + } + + func nestedContainer( + keyedBy keyType: NestedKey.Type, + forKey key: Key + ) throws -> KeyedDecodingContainer { + return try KeyedDecodingContainer( + JSONDecoding.KeyedContainer( + value: try _getOrThrow(forKey: key), + codingPathNode: codingPathNode.appending(key) + ) + ) + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer { + return try JSONDecoding.UnkeyedContainer( + value: _getOrThrow(forKey: key), + codingPathNode: codingPathNode.appending(key) + ) + } + + func superDecoder() throws -> any Decoder { + fatalError("unimplemented") + } + + func superDecoder(forKey key: Key) throws -> any Decoder { + fatalError("unimplemented") + } + + init(value: JSONMapValue, codingPathNode: _CodingPathNode) throws { + guard let mapping = value.asObject() else { + throw DecodingError.typeMismatch( + [String: Any].self, + .init( + codingPath: codingPathNode.path, + debugDescription: "not an object" + ) + ) + } + self.codingPathNode = codingPathNode + self.mapping = mapping + } +} + +extension JSONDecoding.UnkeyedContainer: UnkeyedDecodingContainer { + var codingPath: [any CodingKey] { + codingPathNode.path + } + + var count: Int? { + array.count + } + + var isAtEnd: Bool { + _currMapIdx == array.endIndex + } + + @inline(__always) + mutating func advanceToNextValue() { + _currMapIdx = array.index(after: _currMapIdx) + currentIndex += 1 + } + + @inline(__always) + mutating func _getOrThrow() throws -> (index: any CodingKey, value: JSONMapValue) { + let idx = currentIndex + guard !isAtEnd else { + throw DecodingError.valueNotFound( + Any.self, + .init( + codingPath: codingPathNode.path(byAppendingIndex: idx), + debugDescription: "Unkeyed container is at end" + ) + ) + } + let value = array[_currMapIdx] + advanceToNextValue() + return (_CodingKey(index: idx), value) + } + + @inline(__always) + mutating func _decodeInteger(_ type: Integer.Type) throws -> Integer { + let (idx, value) = try _getOrThrow() + return try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, idx) + } + + @inline(__always) + mutating func _decodeFloating(_ type: Floating.Type) throws -> Floating { + let (idx, value) = try _getOrThrow() + return try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, idx) + } + + mutating func decodeNil() throws -> Bool { + if !isAtEnd && array[_currMapIdx].isNull { + advanceToNextValue() + return true + } + // The protocol states: + // If the value is not null, does not increment currentIndex. + return false + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { + let (idx, value) = try _getOrThrow() + return try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, idx) + } + + mutating func decode(_ type: String.Type) throws -> String { + let (idx, value) = try _getOrThrow() + return try JSONDecoding._decode(value, as: type, codingPathNode: codingPathNode, idx) + } + + mutating func decode(_ type: Double.Type) throws -> Double { + try _decodeFloating(type) + } + + mutating func decode(_ type: Float.Type) throws -> Float { + try _decodeFloating(type) + } + + mutating func decode(_ type: Int.Type) throws -> Int { + try _decodeInteger(type) + } + + mutating func decode(_ type: Int8.Type) throws -> Int8 { + try _decodeInteger(type) + } + + mutating func decode(_ type: Int16.Type) throws -> Int16 { + try _decodeInteger(type) + } + + mutating func decode(_ type: Int32.Type) throws -> Int32 { + try _decodeInteger(type) + } + + mutating func decode(_ type: Int64.Type) throws -> Int64 { + try _decodeInteger(type) + } + + mutating func decode(_ type: UInt.Type) throws -> UInt { + try _decodeInteger(type) + } + + mutating func decode(_ type: UInt8.Type) throws -> UInt8 { + try _decodeInteger(type) + } + + mutating func decode(_ type: UInt16.Type) throws -> UInt16 { + try _decodeInteger(type) + } + + mutating func decode(_ type: UInt32.Type) throws -> UInt32 { + try _decodeInteger(type) + } + + mutating func decode(_ type: UInt64.Type) throws -> UInt64 { + try _decodeInteger(type) + } + + mutating func decode(_ type: T.Type) throws -> T where T: Decodable { + let (idx, value) = try _getOrThrow() + return try JSONDecoding._decodeGeneric(value, as: type, codingPathNode: codingPathNode, idx) + } + + mutating func nestedContainer( + keyedBy keyType: NestedKey.Type + ) throws -> KeyedDecodingContainer { + let (idx, value) = try _getOrThrow() + return try KeyedDecodingContainer( + JSONDecoding.KeyedContainer( + value: value, + codingPathNode: codingPathNode.appending(idx) + ) + ) + } + + mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { + let (idx, value) = try _getOrThrow() + return try JSONDecoding.UnkeyedContainer( + value: value, + codingPathNode: codingPathNode.appending(idx) + ) + } + + mutating func superDecoder() throws -> any Decoder { + fatalError("unimplemented") + } + + init(value: JSONMapValue, codingPathNode: _CodingPathNode) throws { + guard let array = value.asArray() else { + throw DecodingError.typeMismatch( + [Any].self, + .init( + codingPath: codingPathNode.path, + debugDescription: "not an array" + ) + ) + } + self.codingPathNode = codingPathNode + self.currentIndex = 0 + self.array = array + self._currMapIdx = array.startIndex + } +} diff --git a/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONEncoding.swift b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONEncoding.swift new file mode 100644 index 00000000000..411e8665d6b --- /dev/null +++ b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONEncoding.swift @@ -0,0 +1,614 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +func encodeToJSON(value: some Encodable) throws -> [UInt8] { + let encoder = JSONEncoding() + try value.encode(to: encoder) + return JSONWriter.serialize(encoder.reference ?? .null) +} + +/// Intermediate representation for serializing JSON structure. +private class JSONReference { + enum Backing { + case null + case trueKeyword + case falseKeyword + case string(String) + case number(String) + case array([JSONReference]) + case object([String: JSONReference]) + } + + var backing: Backing + + init(backing: Backing) { + self.backing = backing + } + + var count: Int { + switch backing { + case .array(let array): return array.count + case .object(let dict): return dict.count + default: preconditionFailure("Count does not apply to scalar") + } + } + + func set(key: String, value: JSONReference) { + guard case .object(var dict) = backing else { + preconditionFailure() + } + backing = .null // Ensure 'dict' uniquely referenced. + dict[key] = value + backing = .object(dict) + } + + func append(_ value: JSONReference) { + guard case .array(var arr) = backing else { + preconditionFailure() + } + backing = .null // Ensure 'arr' uniquely referenced. + arr.append(value) + backing = .array(arr) + } + + static let null: JSONReference = .init(backing: .null) + static let trueKeyword: JSONReference = .init(backing: .trueKeyword) + static let falseKeyword: JSONReference = .init(backing: .falseKeyword) + + @inline(__always) + static func newArray() -> JSONReference { + .init(backing: .array([])) + } + + @inline(__always) + static func newObject() -> JSONReference { + .init(backing: .object([:])) + } + + @inline(__always) + static func string(_ str: String) -> JSONReference { + .init(backing: .string(str)) + } + + @inline(__always) + static func number(_ integer: some BinaryInteger & LosslessStringConvertible) -> JSONReference { + .init(backing: .number(String(integer))) + } + + @inline(__always) + static func number(_ floating: some BinaryFloatingPoint & LosslessStringConvertible) -> JSONReference { + // FIXME: Error for NaN, Inf. + .init(backing: .number(String(floating))) + } +} + +/// Serialize JSONReference to [UInt8] data. +private struct JSONWriter { + var data: [UInt8] + init() { + data = [] + } + + mutating func write(_ ascii: UInt8) { + data.append(ascii) + } + + mutating func write(ascii: UnicodeScalar) { + data.append(UInt8(ascii: ascii)) + } + + mutating func write(string: StaticString) { + string.withUTF8Buffer { buffer in + data.append(contentsOf: buffer) + } + } + + mutating func write(utf8: some Collection) { + data.append(contentsOf: utf8) + } + + mutating func serialize(value: JSONReference) { + switch value.backing { + case .null: + write(string: "null") + case .trueKeyword: + write(string: "true") + case .falseKeyword: + write(string: "false") + case .string(let string): + serialize(string: string) + case .number(var string): + string.withUTF8 { + write(utf8: $0) + } + case .array(let array): + serialize(array: array) + case .object(let dictionary): + serialize(object: dictionary) + } + } + + mutating func serialize(string: String) { + var string = string + write(ascii: "\"") + string.withUTF8 { utf8 in + let start = utf8.baseAddress! + let end = start + utf8.count + var mark = start + + for cursor in start..> shift) & 0xF + write(d < 10 ? (_0 + d) : (_A + d - 10)) + } + default: + // Accumulate this byte. + break + } + } + + // Append accumulated bytes. + if end > mark { + write(utf8: UnsafeBufferPointer(start: mark, count: end - mark)) + } + } + write(ascii: "\"") + } + + mutating func serialize(array: [JSONReference]) { + write(ascii: "[") + var first = true + for elem in array { + if first { + first = false + } else { + write(ascii: ",") + } + serialize(value: elem) + } + write(ascii: "]") + } + + mutating func serialize(object: [String: JSONReference]) { + write(ascii: "{") + var first = true + for key in object.keys.sorted() { + if first { + first = false + } else { + write(ascii: ",") + } + serialize(string: key) + write(ascii: ":") + serialize(value: object[key]!) + } + write(ascii: "}") + } + + static func serialize(_ value: JSONReference) -> [UInt8] { + var writer = JSONWriter() + writer.serialize(value: value) + return writer.data + } +} + +private class JSONEncoding { + /// Storage of the encoded data. + var reference: JSONReference? + + var codingPathNode: _CodingPathNode + + init(codingPathNode: _CodingPathNode = .root) { + self.reference = nil + self.codingPathNode = codingPathNode + } +} + +// MARK: Pure encoding functions. +extension JSONEncoding { + func _encode(_ value: Bool) -> JSONReference { + value ? .trueKeyword : .falseKeyword + } + + func _encode(_ value: some BinaryFloatingPoint & LosslessStringConvertible) -> JSONReference { + .number(value) + } + + func _encode(_ value: some BinaryInteger & LosslessStringConvertible) -> JSONReference { + .number(value) + } + + func _encode(_ value: String) -> JSONReference { + .string(value) + } + + func _encodeGeneric( + _ value: T, + codingPathNode: _CodingPathNode, + _ additionalKey: (some CodingKey)? + ) throws -> JSONReference { + // Temporarily reset the state and perform the encoding. + let old = (self.reference, self.codingPathNode) + defer { (self.reference, self.codingPathNode) = old } + + self.reference = nil + self.codingPathNode = codingPathNode.appending(additionalKey) + + try value.encode(to: self) + guard let result = self.reference else { + throw EncodingError.invalidValue( + T.self, + .init( + codingPath: self.codingPathNode.path, + debugDescription: "nothing was encoded" + ) + ) + } + return result + } +} + +// MARK: Encoder conformance. +extension JSONEncoding: Encoder { + var codingPath: [any CodingKey] { + codingPathNode.path + } + + var userInfo: [CodingUserInfoKey: Any] { [:] } + + fileprivate struct KeyedContainer { + var encoder: JSONEncoding + var reference: JSONReference + var codingPathNode: _CodingPathNode + } + + fileprivate struct UnkeyedContainer { + var encoder: JSONEncoding + var reference: JSONReference + var codingPathNode: _CodingPathNode + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + reference = .newObject() + return KeyedEncodingContainer( + KeyedContainer( + encoder: self, + reference: reference!, + codingPathNode: codingPathNode + ) + ) + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + reference = .newArray() + return UnkeyedContainer( + encoder: self, + reference: reference!, + codingPathNode: codingPathNode + ) + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + return self + } +} + +extension JSONEncoding: SingleValueEncodingContainer { + func encodeNil() throws { + reference = .null + } + + func encode(_ value: Bool) throws { + reference = _encode(value) + } + + func encode(_ value: String) throws { + reference = _encode(value) + } + + func encode(_ value: Double) throws { + reference = _encode(value) + } + + func encode(_ value: Float) throws { + reference = _encode(value) + } + + func encode(_ value: Int) throws { + reference = _encode(value) + } + + func encode(_ value: Int8) throws { + reference = _encode(value) + } + + func encode(_ value: Int16) throws { + reference = _encode(value) + } + + func encode(_ value: Int32) throws { + reference = _encode(value) + } + + func encode(_ value: Int64) throws { + reference = _encode(value) + } + + func encode(_ value: UInt) throws { + reference = _encode(value) + } + + func encode(_ value: UInt8) throws { + reference = _encode(value) + } + + func encode(_ value: UInt16) throws { + reference = _encode(value) + } + + func encode(_ value: UInt32) throws { + reference = _encode(value) + } + + func encode(_ value: UInt64) throws { + reference = _encode(value) + } + + func encode(_ value: T) throws { + reference = try _encodeGeneric(value, codingPathNode: codingPathNode, (_CodingKey)?.none) + } +} + +extension JSONEncoding.KeyedContainer: KeyedEncodingContainerProtocol { + var codingPath: [any CodingKey] { + codingPathNode.path + } + + func _set(key: Key, value: JSONReference) { + reference.set(key: key.stringValue, value: value) + } + + mutating func encodeNil(forKey key: Key) throws { + _set(key: key, value: .null) + } + + mutating func encode(_ value: Bool, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: String, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: Double, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: Float, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: Int, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: Int8, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: Int16, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: Int32, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: Int64, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: UInt, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: UInt8, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: UInt16, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: UInt32, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: UInt64, forKey key: Key) throws { + _set(key: key, value: encoder._encode(value)) + } + + mutating func encode(_ value: T, forKey key: Key) throws { + _set(key: key, value: try encoder._encodeGeneric(value, codingPathNode: codingPathNode, key)) + } + + mutating func nestedContainer( + keyedBy keyType: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let ref: JSONReference = .newObject() + _set(key: key, value: ref) + return KeyedEncodingContainer( + JSONEncoding.KeyedContainer( + encoder: encoder, + reference: ref, + codingPathNode: codingPathNode.appending(key) + ) + ) + } + + mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { + let ref: JSONReference = .newArray() + _set(key: key, value: ref) + return JSONEncoding.UnkeyedContainer( + encoder: encoder, + reference: ref, + codingPathNode: codingPathNode.appending(key) + ) + } + + mutating func superEncoder() -> any Encoder { + fatalError("unimplemented") + } + + mutating func superEncoder(forKey key: Key) -> any Encoder { + fatalError("unimplemented") + } +} + +extension JSONEncoding.UnkeyedContainer: UnkeyedEncodingContainer { + var codingPath: [any CodingKey] { + codingPathNode.path + } + + var count: Int { + reference.count + } + + func encodeNil() throws { + reference.append(.null) + } + + func encode(_ value: Bool) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: String) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: Double) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: Float) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: Int) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: Int8) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: Int16) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: Int32) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: Int64) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: UInt) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: UInt8) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: UInt16) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: UInt32) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: UInt64) throws { + reference.append(encoder._encode(value)) + } + + func encode(_ value: T) throws where T: Encodable { + let idx = reference.count + let ref = try encoder._encodeGeneric(value, codingPathNode: codingPathNode, _CodingKey.index(idx)) + reference.append(ref) + } + + func nestedContainer( + keyedBy keyType: NestedKey.Type + ) -> KeyedEncodingContainer { + let ref: JSONReference = .newObject() + let idx = count + reference.append(ref) + return KeyedEncodingContainer( + JSONEncoding.KeyedContainer( + encoder: encoder, + reference: ref, + codingPathNode: codingPathNode.appending(index: idx) + ) + ) + } + + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { + let ref: JSONReference = .newObject() + let idx = count + reference.append(ref) + return JSONEncoding.UnkeyedContainer( + encoder: encoder, + reference: ref, + codingPathNode: codingPathNode.appending(index: idx) + ) + } + + func superEncoder() -> any Encoder { + fatalError("unimplmented") + } +} diff --git a/Sources/_SwiftSyntaxCShims/CMakeLists.txt b/Sources/_SwiftSyntaxCShims/CMakeLists.txt new file mode 100644 index 00000000000..b85340a7456 --- /dev/null +++ b/Sources/_SwiftSyntaxCShims/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(_SwiftSyntaxCShims INTERFACE) +target_include_directories(_SwiftSyntaxCShims INTERFACE "include") +set_property(GLOBAL APPEND PROPERTY SWIFT_EXPORTS _SwiftSyntaxCShims) +install(TARGETS _SwiftSyntaxCShims EXPORT SwiftSyntaxTargets) diff --git a/Sources/_SwiftSyntaxCShims/dummy.c b/Sources/_SwiftSyntaxCShims/dummy.c new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/Sources/_SwiftSyntaxCShims/dummy.c @@ -0,0 +1 @@ + diff --git a/Sources/_SwiftSyntaxCShims/include/_stdio.h b/Sources/_SwiftSyntaxCShims/include/_stdio.h new file mode 100644 index 00000000000..17c5e5b34dc --- /dev/null +++ b/Sources/_SwiftSyntaxCShims/include/_stdio.h @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#include +#include + +typedef FILE *_ss_ptr_FILE; + +static _ss_ptr_FILE _ss_stdout(void) { + return stdout; +} + +static _ss_ptr_FILE _ss_stdin(void) { + return stdin; +} + +static _ss_ptr_FILE _ss_stderr(void) { + return stderr; +} + +static int _ss_errno(void) { + return errno; +} diff --git a/Sources/_SwiftSyntaxCShims/include/module.modulemap b/Sources/_SwiftSyntaxCShims/include/module.modulemap new file mode 100644 index 00000000000..db118d217dd --- /dev/null +++ b/Sources/_SwiftSyntaxCShims/include/module.modulemap @@ -0,0 +1,4 @@ +module _SwiftSyntaxCShims { + header "_stdio.h" + export * +} diff --git a/Tests/SwiftCompilerPluginTest/JSONTests.swift b/Tests/SwiftCompilerPluginTest/JSONTests.swift new file mode 100644 index 00000000000..bb399b9f3f2 --- /dev/null +++ b/Tests/SwiftCompilerPluginTest/JSONTests.swift @@ -0,0 +1,317 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_spi(PluginMessage) import SwiftCompilerPluginMessageHandling +import XCTest + +final class JSONTests: XCTestCase { + + func testPrimitive() { + assertRoundTrip(of: true, expectedJSON: "true") + assertRoundTrip(of: false, expectedJSON: "false") + assertRoundTrip(of: Bool?.none, expectedJSON: "null") + assertRoundTrip(of: "", expectedJSON: "\"\"") + assertRoundTrip(of: 0, expectedJSON: "0") + assertRoundTrip(of: 0 as Int8, expectedJSON: "0") + assertRoundTrip(of: 0.0 as Float, expectedJSON: "0.0") + assertRoundTrip(of: 0.0 as Double, expectedJSON: "0.0") + } + + func testEmptyStruct() { + let value = EmptyStruct() + assertRoundTrip(of: value, expectedJSON: "{}") + } + + func testEmptyClass() { + let value = EmptyClass() + assertRoundTrip(of: value, expectedJSON: "{}") + } + + func testTrivialEnumDefault() { + assertRoundTrip(of: Direction.left, expectedJSON: #"{"left":{}}"#) + assertRoundTrip(of: Direction.right, expectedJSON: #"{"right":{}}"#) + } + + func testTrivialEnumRawValue() { + assertRoundTrip(of: Animal.dog, expectedJSON: #""dog""#) + assertRoundTrip(of: Animal.cat, expectedJSON: #""cat""#) + } + + func testTrivialEnumCustom() { + assertRoundTrip(of: Switch.off, expectedJSON: "false") + assertRoundTrip(of: Switch.on, expectedJSON: "true") + } + + func testEnumWithAssociated() { + let tree: Tree = .dictionary([ + "name": .string("John Doe"), + "data": .array([.int(12), .string("foo")]), + ]) + assertRoundTrip( + of: tree, + expectedJSON: #""" + {"dictionary":{"_0":{"data":{"array":{"_0":[{"int":{"_0":12}},{"string":{"_0":"foo"}}]}},"name":{"string":{"_0":"John Doe"}}}}} + """# + ) + } + + func testArrayOfInt() { + let arr: [Int] = [12, 42] + assertRoundTrip(of: arr, expectedJSON: "[12,42]") + let empty: [Int] = [] + assertRoundTrip(of: empty, expectedJSON: "[]") + } + + func testComplexStruct() { + let empty = ComplexStruct(result: nil, diagnostics: [], elapsed: 0.0) + assertRoundTrip(of: empty, expectedJSON: #"{"diagnostics":[],"elapsed":0.0}"#) + + let value = ComplexStruct( + result: "\tresult\nfoo", + diagnostics: [ + .init( + message: "error 🛑", + animal: .cat, + data: [nil, 42] + ) + ], + elapsed: 42.3e32 + ) + assertRoundTrip( + of: value, + expectedJSON: #""" + {"diagnostics":[{"animal":"cat","data":[null,42],"message":"error 🛑"}],"elapsed":4.23e+33,"result":"\tresult\nfoo"} + """# + ) + } + + func testEscapedString() { + assertRoundTrip( + of: "\n\"\\\u{A9}\u{0}\u{07}\u{1B}", + expectedJSON: #""" + "\n\"\\©\u0000\u0007\u001B" + """# + ) + } + + func testParseError() { + assertParseError( + #"{"foo": 1"#, + message: "unexpected end of file" + ) + assertParseError( + #""foo"#, + message: "unexpected end of file" + ) + assertParseError( + "\n", + message: "unexpected end of file" + ) + assertParseError( + "trua", + message: "unexpected character 'a'; expected 'e'" + ) + assertParseError( + "[true, #foo]", + message: "unexpected character '#'; value start" + ) + assertParseError( + "{}true", + message: "unexpected character 't'; after top-level value" + ) + } + + func testInvalidStringDecoding() { + assertInvalidStrng(#""foo\"#) // EOF after '\' + assertInvalidStrng(#""\x""#) // Unknown character after '\' + assertInvalidStrng(#""\u1""#) // Missing 4 digits after '\u' + assertInvalidStrng(#""\u12""#) + assertInvalidStrng(#""\u123""#) + assertInvalidStrng(#""\uEFGH""#) // Invalid HEX characters. + } + + func testStringSurrogatePairDecoding() { + // FIXME: Escaped surrogate pairs are not supported. + // Currently parsed as "invalid", but this should be valid '𐐷' (U+10437) character + assertInvalidStrng(#"\uD801\uDC37"#) + } + + func testTypeCoercion() { + assertRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [Int32].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [Int64].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [UInt].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [UInt8].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [UInt16].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [UInt32].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [UInt64].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [Float].self) + assertRoundTripTypeCoercionFailure(of: [false, true], as: [Double].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [Int], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [Int8], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [Int16], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [Int32], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [Int64], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [UInt], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [UInt8], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self) + assertRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) + } + + func testFloatingPointBufferBoundary() throws { + // Make sure floating point parsing does not read past the decoding JSON buffer. + var str = "0.199" + try str.withUTF8 { buf in + let truncated = UnsafeBufferPointer(rebasing: buf[0..<3]) + XCTAssertEqual(try JSON.decode(Double.self, from: truncated), 0.1) + XCTAssertEqual(try JSON.decode(Float.self, from: truncated), 0.1) + } + } + + private func assertRoundTrip( + of value: T, + expectedJSON: String, + file: StaticString = #file, + line: UInt = #line + ) { + let payload: [UInt8] + do { + payload = try JSON.encode(value) + } catch let error { + XCTFail("Failed to encode \(T.self) to JSON: \(error)", file: file, line: line) + return + } + + let jsonStr = String(decoding: payload, as: UTF8.self) + XCTAssertEqual(jsonStr, expectedJSON, file: file, line: line) + + let decoded: T + do { + decoded = try payload.withUnsafeBufferPointer { + try JSON.decode(T.self, from: $0) + } + } catch let error { + XCTFail("Failed to decode \(T.self) from JSON: \(error)", file: file, line: line) + return + } + XCTAssertEqual(value, decoded, file: file, line: line) + } + + private func assertRoundTripTypeCoercionFailure( + of value: T, + as type: U.Type, + file: StaticString = #file, + line: UInt = #line + ) { + do { + let data = try JSONEncoder().encode(value) + let _ = try JSONDecoder().decode(U.self, from: data) + XCTFail("Coercion from \(T.self) to \(U.self) was expected to fail.", file: file, line: line) + } catch DecodingError.typeMismatch(_, _) { + // Success + } catch { + XCTFail("unexpected error", file: file, line: line) + } + } + + private func assertInvalidStrng(_ json: String, file: StaticString = #file, line: UInt = #line) { + do { + var json = json + _ = try json.withUTF8 { try JSON.decode(String.self, from: $0) } + XCTFail("decoding should fail", file: file, line: line) + } catch {} + } + + private func assertParseError(_ json: String, message: String, file: StaticString = #file, line: UInt = #line) { + do { + var json = json + _ = try json.withUTF8 { try JSON.decode(Bool.self, from: $0) } + XCTFail("decoding should fail", file: file, line: line) + } catch DecodingError.dataCorrupted(let context) { + XCTAssertEqual( + String(describing: try XCTUnwrap(context.underlyingError, file: file, line: line)), + message + ) + } catch { + XCTFail("unexpected error", file: file, line: line) + } + } +} + +// MARK: - Test Types + +fileprivate struct EmptyStruct: Codable, Equatable { + static func == (_ lhs: EmptyStruct, _ rhs: EmptyStruct) -> Bool { + return true + } +} + +fileprivate class EmptyClass: Codable, Equatable { + static func == (_ lhs: EmptyClass, _ rhs: EmptyClass) -> Bool { + return true + } +} + +fileprivate enum Direction: Codable { + case right + case left +} + +fileprivate enum Animal: String, Codable { + case dog + case cat +} + +fileprivate enum Switch: Codable { + case off + case on + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + switch try container.decode(Bool.self) { + case false: self = .off + case true: self = .on + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .off: try container.encode(false) + case .on: try container.encode(true) + } + } +} + +fileprivate enum Tree: Codable, Equatable { + indirect case int(Int) + indirect case string(String) + indirect case array([Self]) + indirect case dictionary([String: Self]) +} + +fileprivate struct ComplexStruct: Codable, Equatable { + struct Diagnostic: Codable, Equatable { + var message: String + var animal: Animal + var data: [Int?] + } + + var result: String? + var diagnostics: [Diagnostic] + var elapsed: Double +}