diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..063da96 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.2-snapshot-2025-06-14 \ No newline at end of file diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..01f2b61 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "c86767f39732a155364f2233ee80cf72fff886f23a4c21aaf663cf4c743e2e45", + "pins" : [ + { + "identity" : "swift-binary-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-binary-parsing.git", + "state" : { + "branch" : "main", + "revision" : "d4724d1d6f5da8526320f1990df9e2bd59dd2911" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 1ae033f..a08faf8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,11 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "ExternalTermFormat", + platforms: [.macOS(.v15)], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( @@ -13,23 +14,33 @@ let package = Package( "ExternalTermFormat" ]), ], - traits: ["Embedded"], + dependencies: [ + .package(url: "https://github.com/apple/swift-binary-parsing.git", branch: "main") + ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "ExternalTermFormat", - cSettings: [ - .unsafeFlags(["-fdeclspec"], .when(traits: ["Embedded"])) - ], + dependencies: [.product(name: "BinaryParsing", package: "swift-binary-parsing")], swiftSettings: [ - .enableExperimentalFeature("Embedded", .when(traits: ["Embedded"])), - .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"], .when(traits: ["Embedded"])) + .enableExperimentalFeature("Span"), + .enableExperimentalFeature("ValueGenerics"), + .enableExperimentalFeature("LifetimeDependence"), ] ), + .executableTarget( + name: "ExternalTermFormatBenchmarks", + dependencies: ["ExternalTermFormat"] + ), .testTarget( name: "ExternalTermFormatTests", - dependencies: ["ExternalTermFormat"] + dependencies: [.target(name: "ExternalTermFormat")], + swiftSettings: [ + .enableExperimentalFeature("Span"), + .enableExperimentalFeature("ValueGenerics"), + .enableExperimentalFeature("LifetimeDependence"), + ] ), ] ) diff --git a/README.md b/README.md index 0ead06d..6c1bb0b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,23 @@ # External Term Format +> [!NOTE] +> This package requires Swift 6.2+ for its `Span` type. +> When running on macOS on an OS older than 26.0, make sure to set the `DYLD_LIBRARY_PATH`: +> ``` +> swiftly run swift build +> DYLD_LIBRARY_PATH="/Library/Developer/Toolchains/swift-6.2-DEVELOPMENT-SNAPSHOT-2025-06-14-a.xctoolchain/usr/lib/swift/macosx" \ +> .build/arm64-apple-macosx/debug/ExternalTermFormatBenchmarks +> ``` + Encoding/decoding for Erlang's [External Term Format](https://www.erlang.org/doc/apps/erts/erl_ext_dist.html). +Compatible with Embedded Swift. + +The entire package is built on the `Term` enum, which reflects all possible encoded values in Erlang's external term format. + -Use `TermBuffer` to encode/decode raw bytes. +``` +let greeting = Term.atom("Hello, world!") +greeting.encoded() // [131, ] -This package is fully compatible with Embedded Swift runtimes. \ No newline at end of file +let decoded = Term(parsing: [131, ]) +``` \ No newline at end of file diff --git a/Sources/ExternalTermFormat/ExternalTermFormat.swift b/Sources/ExternalTermFormat/ExternalTermFormat.swift deleted file mode 100644 index 02a3c00..0000000 --- a/Sources/ExternalTermFormat/ExternalTermFormat.swift +++ /dev/null @@ -1,173 +0,0 @@ -public struct TermBuffer { - public private(set) var buffer: [UInt8] = [] - - public var index: Int = 0 - - public init() {} - - public init(_ buffer: [UInt8]) { - self.buffer = buffer - } - - public mutating func encodeVersion() { - append(131) - } - - public mutating func decodeVersion() throws(TermDecodingError) -> UInt8 { - guard try peek() == 131 - else { throw TermDecodingError.wrongTag } - _ = try consume() - return 131 - } - - mutating func append(_ tag: TermKind) { - append(tag.rawValue) - } - - mutating func append(_ value: UInt8) { - if self.buffer.isEmpty { - self.buffer.append(value) - } else { - self.buffer.insert(value, at: index + 1) - index += 1 - } - } - - public mutating func append(contentsOf other: some Sequence) { - for byte in other { - append(byte) - } - } - - mutating func insert(contentsOf other: some Collection, at index: Int) { - buffer.insert(contentsOf: other, at: index) - } -} - -extension TermBuffer { - func peek(_ count: Int) throws(TermDecodingError) -> [UInt8] { - guard buffer.indices.contains(index + count) - else { throw TermDecodingError.outOfBounds } - return Array(buffer[index...(index + count)]) - } - - mutating func consume(_ count: Int) throws(TermDecodingError) -> [UInt8] { - guard buffer.indices.contains(index + count - 1) - else { throw TermDecodingError.outOfBounds } - defer { index += count } - return Array(buffer[index..<(index + count)]) - } - - func peek() throws(TermDecodingError) -> UInt8 { - guard buffer.indices.contains(index) - else { throw TermDecodingError.outOfBounds } - return buffer[index] - } - - mutating func consume() throws(TermDecodingError) -> UInt8 { - guard buffer.indices.contains(index) - else { throw TermDecodingError.outOfBounds } - defer { index += 1 } - return buffer[index] - } -} - -extension TermBuffer { - public func kind() throws(TermDecodingError) -> TermKind { - guard let kind = TermKind(rawValue: try peek()) - else { throw TermDecodingError.wrongTag } - return kind - } - - public mutating func skip() throws(TermDecodingError) { - let kind = try kind() - switch kind { - case .version: - _ = try decodeVersion() - case .binary: - _ = try decodeBinary() - case .bitBinary: - _ = try decodeBitBinary() - case .distributionHeader: - _ = try decodeDistributionHeader() - case .export: - _ = try decodeExport() - case .float: - _ = try decodeFloat() - case .function: - _ = try decodeFunction() - case .integer: - _ = try decodeInteger() - case .largeBig: - _ = try decodeLargeBig() - case .largeTuple: - let size = try decodeLargeTupleHeader() - for _ in 0.. 0 { + self = .distributionHeader( + flags: try [UInt8](parsing: &input, byteCount: Int(numberOfAtomCacheRefs / 2 + 1)), + atomCacheRefs: try Array( + parsing: &input, + count: Int(numberOfAtomCacheRefs) + ) { (input: inout ParserSpan) -> UInt8 in + guard try UInt8(parsing: &input) == 82 // atom cache ref + else { fatalError() } + return try UInt8(parsing: &input) + } + ) + } else { + self = .distributionHeader(flags: [], atomCacheRefs: []) + } + case 82: // atom cache ref + self = .atomCacheRef(index: try UInt8(parsing: &input)) + case 97: // small integer + self = .smallInteger(try UInt8(parsing: &input)) + case 98: // integer + self = .integer(try Int32(parsingBigEndian: &input)) + case 99: // float + self = .float(try String(parsingUTF8: &input, count: 31)) + + case 120: // v4 port + self = .port(Port( + node: try Term.atom(parsing: &input), + id: try UInt64(parsingBigEndian: &input), + creation: try UInt32(parsingBigEndian: &input) + )) + + case 88: // new pid + self = .pid(PID( + node: try Term.atom(parsing: &input), + id: try UInt32(parsingBigEndian: &input), + serial: try UInt32(parsingBigEndian: &input), + creation: try UInt32(parsingBigEndian: &input) + )) + + case 104: // small tuple + let arity = Int(try UInt8(parsing: &input)) + self = .smallTuple(try Array(unsafeUninitializedCapacity: arity) { buffer, initializedCount in + for i in 0..(unsafeUninitializedCapacity: arity) { buffer, initializedCount in + for i in 0..(unsafeUninitializedCapacity: length + 1) { buffer, initializedCount in + for i in 0...length { + buffer[i] = try Term(parsing: &input) + } + initializedCount = length + }) + case 109: // binary + let length = try UInt32(parsingBigEndian: &input) + self = .binary(try [UInt8](parsing: &input, byteCount: Int(length))) + + case 110: // small big + let n = try UInt8(parsing: &input) + self = .smallBig( + sign: try UInt8(parsing: &input) == 1, + try [UInt8](parsing: &input, byteCount: Int(n)) + ) + case 111: // large big + let n = try UInt32(parsingBigEndian: &input) + self = .largeBig( + sign: try UInt8(parsing: &input) == 1, + try [UInt8](parsing: &input, byteCount: Int(n)) + ) + + case 90: // newer reference + let length = try UInt16(parsingBigEndian: &input) + self = .reference(Reference( + node: try Term.atom(parsing: &input), + creation: try UInt32(parsingBigEndian: &input), + id: try [UInt8](parsing: &input, byteCount: Int(length)) + )) + + case 112: // new fun + _ = try UInt32(parsingBigEndian: &input) // size + let arity = try UInt8(parsing: &input) + let uniq = try [UInt8](parsing: &input, byteCount: 16) + let index = try Int32(parsingBigEndian: &input) + let numFree = try UInt32(parsingBigEndian: &input) + let module = try Term.atom(parsing: &input) + let oldIndex = try Term.integer(parsing: &input) + let oldUniq = try Term.integer(parsing: &input) + let pid = try PID(parsing: &input) + let freeVars = try [Term](parsing: &input, count: Int(numFree)) { + try Term(parsing: &$0) + } + self = .fun( + arity: arity, + uniq: uniq, + index: index, + module: module, + oldIndex: oldIndex, + oldUniq: oldUniq, + pid: pid, + freeVars: freeVars + ) + + case 113: // export + self = .export( + module: try Term.atom(parsing: &input), + function: try Term.atom(parsing: &input), + arity: try Term.integer(parsing: &input) + ) + + case 77: // bit binary + let length = try UInt32(parsingBigEndian: &input) + self = .bitBinary( + bits: try UInt8(parsing: &input), + try [UInt8](parsing: &input, byteCount: Int(length)) + ) + + case 70: // new float + self = .newFloat(Double(bitPattern: try UInt64(parsingBigEndian: &input))) + + case 118: // atom utf8 + let length = try UInt16(parsingBigEndian: &input) + self = .atomUTF8(try String(parsingUTF8: &input, count: Int(length))) + + case 119: // small atom utf8 + let length = try UInt8(parsing: &input) + self = .smallAtomUTF8(try String(parsingUTF8: &input, count: Int(length))) + + default: + fatalError() + } + } + + @inline(__always) + @usableFromInline + static func atom(parsing input: inout ParserSpan) throws(ThrownParsingError) -> String { + switch try UInt8(parsing: &input) { + case 118: // atom utf8 + let length = try UInt16(parsingBigEndian: &input) + return try String(parsingUTF8: &input, count: Int(length)) + case 119: // small atom utf8 + let length = try UInt8(parsing: &input) + return try String(parsingUTF8: &input, count: Int(length)) + default: + fatalError() + } + } + + @inline(__always) + @usableFromInline + static func integer(parsing input: inout ParserSpan) throws(ThrownParsingError) -> Int { + switch try UInt8(parsing: &input) { + case 97: // small integer + return Int(try UInt8(parsing: &input)) + case 98: // integer + return Int(try Int32(parsingBigEndian: &input)) + default: + fatalError() + } + } +} + +extension PID: ExpressibleByParsing { + @inlinable @inline(__always) + public init(parsing input: inout ParserSpan) throws(ThrownParsingError) { + guard try UInt8(parsing: &input) == 88 + else { fatalError() } + self.node = try Term.atom(parsing: &input) + self.id = try UInt32(parsingBigEndian: &input) + self.serial = try UInt32(parsingBigEndian: &input) + self.creation = try UInt32(parsingBigEndian: &input) + } +} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Term+CustomDebugStringConvertible.swift b/Sources/ExternalTermFormat/Term+CustomDebugStringConvertible.swift new file mode 100644 index 0000000..096be1b --- /dev/null +++ b/Sources/ExternalTermFormat/Term+CustomDebugStringConvertible.swift @@ -0,0 +1,73 @@ +extension Term: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .distributionHeader: + "" + + case .atomCacheRef: + "" + + case let .smallInteger(integer): + String(integer) + + case let .integer(integer): + String(integer) + + case let .float(float): + float + + case let .port(port): + String(describing: port) + + case let .pid(pid): + String(describing: pid) + + case let .smallTuple(tuple): + "{\(tuple.map(\.debugDescription).joined(separator: ","))}" + case let .largeTuple(tuple): + "{\(tuple.map(\.debugDescription).joined(separator: ","))}" + + case let .map(map): + "%{\(map.map { "\($0.debugDescription) => \($1.debugDescription)" }.joined(separator: ","))}" + + case .nil: + "nil" + + case let .string(string): + String(describing: string) + + case let .list(list): + "[\(list.map(\.debugDescription).joined(separator: ","))]" + + case let .binary(binary): + "<<\(binary.map(\.description).joined(separator: ","))>>" + + case .smallBig: + "" + + case .largeBig: + "" + + case let .reference(reference): + String(describing: reference) + + case let .fun(arity, uniq, index, module, oldIndex, oldUniq, pid, freeVars): + "#Function<\(module.debugDescription)/\(arity)>" + + case let .export(module, function, arity): + "\(module.debugDescription):\(function.debugDescription)/\(arity)" + + case let .bitBinary(bits, bytes): + "<<\(bytes.map(\.description).joined(separator: ",")),bits:\(bits)>>" + + case let .newFloat(value): + String(value) + + case let .atomUTF8(atom): + #":"\#(atom)""# + + case let .smallAtomUTF8(atom): + #":"\#(atom)""# + } + } +} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Term.swift b/Sources/ExternalTermFormat/Term.swift new file mode 100644 index 0000000..e803122 --- /dev/null +++ b/Sources/ExternalTermFormat/Term.swift @@ -0,0 +1,90 @@ +public enum Term: Hashable { + case distributionHeader( + flags: [UInt8], + atomCacheRefs: [UInt8] + ) + + case atomCacheRef(index: UInt8) + + /// Unsigned 8-bit integer. + case smallInteger(UInt8) + + /// Signed 32-bit integer in big-endian format. + case integer(Int32) + + /// A finite float stored in string format. + case float(String) + + case port(Port) + + case pid(PID) + + indirect case smallTuple([Term]) + indirect case largeTuple([Term]) + + indirect case map([Term:Term]) + + case `nil` + + case string([UInt8]) + + indirect case list([Term]) + + case binary([UInt8]) + + case smallBig(sign: Bool, [UInt8]) + + case largeBig(sign: Bool, [UInt8]) + + case reference(Reference) + + indirect case fun(arity: UInt8, uniq: [UInt8], index: Int32, module: String, oldIndex: Int, oldUniq: Int, pid: PID, freeVars: [Term]) + + case export(module: String, function: String, arity: Int) + + case bitBinary(bits: UInt8, [UInt8]) + + case newFloat(Double) + + case atomUTF8(String) + + case smallAtomUTF8(String) +} + +public struct PID: Hashable { + public let node: String + public let id: UInt32 + public let serial: UInt32 + public let creation: UInt32 + + public init(node: String, id: UInt32, serial: UInt32, creation: UInt32) { + self.node = node + self.id = id + self.serial = serial + self.creation = creation + } +} + +public struct Port: Hashable { + public let node: String + public let id: UInt64 + public let creation: UInt32 + + public init(node: String, id: UInt64, creation: UInt32) { + self.node = node + self.id = id + self.creation = creation + } +} + +public struct Reference: Hashable { + public let node: String + public let creation: UInt32 + public let id: [UInt8] + + public init(node: String, creation: UInt32, id: [UInt8]) { + self.node = node + self.creation = creation + self.id = id + } +} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Binary.swift b/Sources/ExternalTermFormat/Terms/Binary.swift deleted file mode 100644 index e43c854..0000000 --- a/Sources/ExternalTermFormat/Terms/Binary.swift +++ /dev/null @@ -1,22 +0,0 @@ -public extension TermBuffer { - /// Encodes a binary. - mutating func encodeBinary( - _ data: [UInt8] - ) { - append(.binary) - withUnsafeBytes(of: UInt32(data.count).bigEndian) { - append(contentsOf: $0) - } - append(contentsOf: data) - } - - mutating func decodeBinary() throws(TermDecodingError) -> [UInt8] { - guard try kind() == .binary - else { throw TermDecodingError.wrongTag } - _ = try consume() - let size = try consume(4).withUnsafeBytes { - UInt32(bigEndian: $0.load(as: UInt32.self)) - } - return try consume(Int(size)) - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/BitBinary.swift b/Sources/ExternalTermFormat/Terms/BitBinary.swift deleted file mode 100644 index 5ec99af..0000000 --- a/Sources/ExternalTermFormat/Terms/BitBinary.swift +++ /dev/null @@ -1,21 +0,0 @@ -public extension TermBuffer { - /// Encodes a bit binary term whose length in bits doesn't have to be a multiple of 8. - /// The bits field is the number of bits (1-8) that are used in the last byte in the data field. - mutating func encodeBitBinary(bits: UInt8, data: [UInt8]) { - append(.bitBinary) // 'M' - withUnsafeBytes(of: UInt32(data.count).bigEndian) { - append(contentsOf: $0) - } - append(bits) - append(contentsOf: data) - } - - mutating func decodeBitBinary() throws(TermDecodingError) -> (bits: UInt8, data: [UInt8]) { - guard try kind() == .bitBinary else { throw TermDecodingError.wrongTag } - _ = try consume() - let size = try consume(4).withUnsafeBytes { - UInt32(bigEndian: $0.load(as: UInt32.self)) - } - return (bits: try consume(), data: try consume(Int(size))) - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/DistributionHeader.swift b/Sources/ExternalTermFormat/Terms/DistributionHeader.swift deleted file mode 100644 index 1af3e52..0000000 --- a/Sources/ExternalTermFormat/Terms/DistributionHeader.swift +++ /dev/null @@ -1,13 +0,0 @@ -public extension TermBuffer { - /// The non-fragmented distribution header format - mutating func encodeDistributionHeader() { - append(.distributionHeader) // tag - append(0) // number of atom cache refs - } - - mutating func decodeDistributionHeader() throws(TermDecodingError) -> UInt8 { - guard try kind() == .distributionHeader else { throw TermDecodingError.wrongTag } - _ = try consume() - return try consume() - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Export.swift b/Sources/ExternalTermFormat/Terms/Export.swift deleted file mode 100644 index 67ecf56..0000000 --- a/Sources/ExternalTermFormat/Terms/Export.swift +++ /dev/null @@ -1,26 +0,0 @@ -public extension TermBuffer { - /// Encodes an EXPORT_EXT term. - /// | 1 | N1 | N2 | N3 | - /// | ----- | -------- | ---------- | ------- | - /// | `113` | `Module` | `Function` | `Arity` | - mutating func encodeExport( - module: String, - function: String, - arity: UInt8 - ) { - append(.export) // 'q' - encodeSmallAtomUTF8(module) - encodeSmallAtomUTF8(function) - encodeSmallInteger(arity) - } - - mutating func decodeExport() throws(TermDecodingError) -> (module: String, function: String, arity: UInt8) { - guard try kind() == .export else { throw TermDecodingError.wrongTag } - _ = try consume() - return ( - module: try decodeSmallAtomUTF8(), - function: try decodeSmallAtomUTF8(), - arity: try decodeSmallInteger(), - ) - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Float.swift b/Sources/ExternalTermFormat/Terms/Float.swift deleted file mode 100644 index d088d65..0000000 --- a/Sources/ExternalTermFormat/Terms/Float.swift +++ /dev/null @@ -1,18 +0,0 @@ -public extension TermBuffer { - /// Encodes a NEW_FLOAT_EXT term. - /// A finite float is stored as 8 bytes in big-endian IEEE format. - mutating func encodeFloat(_ value: Double) { - append(.float) // 'F' - withUnsafeBytes(of: value.bitPattern.bigEndian) { - append(contentsOf: $0) - } - } - - mutating func decodeFloat() throws(TermDecodingError) -> Double { - guard try kind() == .float else { throw TermDecodingError.wrongTag } - _ = try consume() - return try consume(8).withUnsafeBytes { - Double(bitPattern: UInt64(bigEndian: $0.load(as: UInt64.self))) - } - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Function.swift b/Sources/ExternalTermFormat/Terms/Function.swift deleted file mode 100644 index dbc632e..0000000 --- a/Sources/ExternalTermFormat/Terms/Function.swift +++ /dev/null @@ -1,113 +0,0 @@ -public extension TermBuffer { - /// Encodes a NEW_FUN_EXT term. - /// | 1 | 4 | 1 | 16 | 4 | 4 | N1 | N2 | N3 | N4 | N5 | - /// | ----- | ------ | ------- | ------ | ------- | --------- | -------- | ---------- | --------- | ----- | ----------- | - /// | `112` | `Size` | `Arity` | `Uniq` | `Index` | `NumFree` | `Module` | `OldIndex` | `OldUniq` | `Pid` | `Free Vars` | - mutating func encodeFunction( - arity: UInt8, - uniq: [UInt8], - index: UInt32, - module: String, - oldIndex: Int, - oldUniq: Int, - pid: PID, - freeVars: [[UInt8]] - ) { - append(.function) // 'p' - - let startIndex = self.index - - append(arity) - append(contentsOf: uniq.prefix(16)) // MD5 hash is 16 bytes - - withUnsafeBytes(of: index.bigEndian) { - append(contentsOf: $0) - } - - let numFree = UInt32(freeVars.count) - withUnsafeBytes(of: numFree.bigEndian) { - append(contentsOf: $0) - } - - encodeSmallAtomUTF8(module) - - // Encode oldIndex as SMALL_INTEGER_EXT or INTEGER_EXT - if (0...255).contains(oldIndex) { - encodeSmallInteger(UInt8(oldIndex)) - } else { - encodeInteger(Int32(oldIndex)) - } - - // Encode oldUniq as SMALL_INTEGER_EXT or INTEGER_EXT - if (0...255).contains(oldUniq) { - encodeSmallInteger(UInt8(oldUniq)) - } else { - encodeInteger(Int32(oldUniq)) - } - - encodePID(pid) - - // Encode free variables - for freeVar in freeVars { - append(contentsOf: freeVar) - } - - // Calculate and write the size - let size = UInt32(self.index - startIndex) - withUnsafeBytes(of: size.bigEndian) { bytes in - insert(contentsOf: bytes, at: startIndex + 1) - } - } - - mutating func decodeFunction() throws(TermDecodingError) -> ( - arity: UInt8, - uniq: [UInt8], - index: UInt32, - module: String, - oldIndex: Int, - oldUniq: Int, - pid: PID, - numFree: UInt32, - freeVars: [UInt8] - ) { - guard try kind() == .function else { throw TermDecodingError.wrongTag } - _ = try consume() - - let startIndex = index - - let size = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - - let arity = try consume() - - let uniq = try consume(16) - - let index = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - - // numFree - let numFree = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - - let module = try decodeSmallAtomUTF8() - - let oldIndex = (try? Int(decodeSmallInteger())) ?? (try? Int(decodeInteger())) - guard let oldIndex else { throw TermDecodingError.decodingError } - - let oldUniq = (try? Int(decodeSmallInteger())) ?? (try? Int(decodeInteger())) - guard let oldUniq else { throw TermDecodingError.decodingError } - - let pid = try decodePID() - - let freeVars = try consume((startIndex + Int(size)) - self.index) - - return ( - arity: arity, - uniq: Array(uniq), - index: index, - module: module, - oldIndex: oldIndex, - oldUniq: oldUniq, - pid: pid, - numFree: numFree, - freeVars: freeVars - ) - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Integer.swift b/Sources/ExternalTermFormat/Terms/Integer.swift deleted file mode 100644 index 234c7ee..0000000 --- a/Sources/ExternalTermFormat/Terms/Integer.swift +++ /dev/null @@ -1,16 +0,0 @@ -public extension TermBuffer { - /// Signed 32-bit integer in big-endian format. - mutating func encodeInteger(_ value: Int32) { - append(.integer) - withUnsafeBytes(of: value.bigEndian) { - append(contentsOf: $0) - } - } - - mutating func decodeInteger() throws(TermDecodingError) -> Int32 { - guard try kind() == .integer else { throw TermDecodingError.wrongTag } - _ = try consume() - - return try consume(4).withUnsafeBytes { Int32(bigEndian: $0.load(as: Int32.self)) } - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/LargeBig.swift b/Sources/ExternalTermFormat/Terms/LargeBig.swift deleted file mode 100644 index d1ca6ee..0000000 --- a/Sources/ExternalTermFormat/Terms/LargeBig.swift +++ /dev/null @@ -1,33 +0,0 @@ -public extension TermBuffer { - /// Encodes a LARGE_BIG_EXT term. - /// | 1 | 4 | 1 | n | - /// | ----- | --- | ------ | ------------------- | - /// | `111` | `n` | `Sign` | `d(0)` ... `d(n-1)` | - mutating func encodeLargeBig(_ value: Int) { - append(.largeBig) // 'o' - let absValue = abs(value) - let sign: UInt8 = value < 0 ? 1 : 0 - let bytes = withUnsafeBytes(of: absValue.littleEndian) { Array($0) } - withUnsafeBytes(of: UInt32(bytes.count).bigEndian) { - append(contentsOf: $0) - } - append(sign) - append(contentsOf: bytes) - } - - mutating func decodeLargeBig() throws(TermDecodingError) -> Int { - guard try kind() == .largeBig else { throw TermDecodingError.wrongTag } - _ = try consume() - let byteCount = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - let sign = try consume() - guard sign == 0 || sign == 1 else { - throw TermDecodingError.wrongTag - } - let bytes = try consume(Int(byteCount)) - var value: Int = 0 - for (i, byte) in bytes.enumerated() { - value += Int(byte) << (8 * i) - } - return sign == 1 ? -value : value - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/LargeTuple.swift b/Sources/ExternalTermFormat/Terms/LargeTuple.swift deleted file mode 100644 index 2de8174..0000000 --- a/Sources/ExternalTermFormat/Terms/LargeTuple.swift +++ /dev/null @@ -1,18 +0,0 @@ -public extension TermBuffer { - /// Encodes a large tuple header. - mutating func encodeLargeTupleHeader( - arity: UInt32 - ) { - append(.largeTuple) - withUnsafeBytes(of: arity.bigEndian) { - append(contentsOf: $0) - } - } - - mutating func decodeLargeTupleHeader() throws(TermDecodingError) -> UInt32 { - guard try kind() == .largeTuple else { throw TermDecodingError.wrongTag } - _ = try consume() - - return try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/List.swift b/Sources/ExternalTermFormat/Terms/List.swift deleted file mode 100644 index 75dd622..0000000 --- a/Sources/ExternalTermFormat/Terms/List.swift +++ /dev/null @@ -1,18 +0,0 @@ -public extension TermBuffer { - /// Encodes a list header. - mutating func encodeListHeader( - arity: UInt32 - ) { - append(.list) - withUnsafeBytes(of: arity.bigEndian) { - append(contentsOf: $0) - } - } - - mutating func decodeListHeader() throws(TermDecodingError) -> UInt32 { - guard try kind() == .list else { throw TermDecodingError.wrongTag } - _ = try consume() - - return try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Map.swift b/Sources/ExternalTermFormat/Terms/Map.swift deleted file mode 100644 index 8b80239..0000000 --- a/Sources/ExternalTermFormat/Terms/Map.swift +++ /dev/null @@ -1,18 +0,0 @@ -public extension TermBuffer { - /// Encodes a map header. - mutating func encodeMapHeader( - arity: UInt32 - ) { - append(.map) - withUnsafeBytes(of: arity.bigEndian) { - append(contentsOf: $0) - } - } - - mutating func decodeMapHeader() throws(TermDecodingError) -> UInt32 { - guard try kind() == .map else { throw TermDecodingError.wrongTag } - _ = try consume() - - return try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Nil.swift b/Sources/ExternalTermFormat/Terms/Nil.swift deleted file mode 100644 index c82267f..0000000 --- a/Sources/ExternalTermFormat/Terms/Nil.swift +++ /dev/null @@ -1,11 +0,0 @@ -public extension TermBuffer { - /// Encodes an empty list. - mutating func encodeNil() { - append(.nil) - } - - mutating func decodeNil() throws(TermDecodingError) { - guard try kind() == .nil else { throw TermDecodingError.wrongTag } - _ = try consume() - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/PIDCoding.swift b/Sources/ExternalTermFormat/Terms/PIDCoding.swift deleted file mode 100644 index d588bf5..0000000 --- a/Sources/ExternalTermFormat/Terms/PIDCoding.swift +++ /dev/null @@ -1,35 +0,0 @@ -public extension TermBuffer { - /// Encodes a process identifier. - mutating func encodePID( - _ pid: PID - ) { - append(.pid) - encodeSmallAtomUTF8(pid.node) - withUnsafeBytes(of: pid.id.bigEndian) { - append(contentsOf: $0) - } - withUnsafeBytes(of: pid.serial.bigEndian) { - append(contentsOf: $0) - } - withUnsafeBytes(of: pid.creation.bigEndian) { - append(contentsOf: $0) - } - } - - mutating func decodePID() throws(TermDecodingError) -> PID { - guard try kind() == .pid else { throw TermDecodingError.wrongTag } - _ = try consume() - - let node = try decodeSmallAtomUTF8() - let id = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - let serial = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - let creation = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - - return PID( - node: node, - id: id, - serial: serial, - creation: creation - ) - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Port.swift b/Sources/ExternalTermFormat/Terms/Port.swift deleted file mode 100644 index 2d5fb62..0000000 --- a/Sources/ExternalTermFormat/Terms/Port.swift +++ /dev/null @@ -1,28 +0,0 @@ -public extension TermBuffer { - /// Encodes a port identifier. - mutating func encodePort( - node: String, - id: UInt64, - creation: UInt32 - ) { - append(.port) - encodeSmallAtomUTF8(node) - withUnsafeBytes(of: id.bigEndian) { - append(contentsOf: $0) - } - withUnsafeBytes(of: creation.bigEndian) { - append(contentsOf: $0) - } - } - - mutating func decodePort() throws(TermDecodingError) -> (node: String, id: UInt64, creation: UInt32) { - guard try kind() == .port else { throw TermDecodingError.wrongTag } - _ = try consume() - - let node = try decodeSmallAtomUTF8() - let id = try consume(8).withUnsafeBytes { UInt64(bigEndian: $0.load(as: UInt64.self)) } - let creation = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - - return (node: node, id: id, creation: creation) - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/Reference.swift b/Sources/ExternalTermFormat/Terms/Reference.swift deleted file mode 100644 index 0339208..0000000 --- a/Sources/ExternalTermFormat/Terms/Reference.swift +++ /dev/null @@ -1,38 +0,0 @@ -public extension TermBuffer { - /// Encodes a reference. - mutating func encodeReference( - node: String, - creation: UInt32, - id: [UInt32] - ) { - append(.reference) - withUnsafeBytes(of: UInt16(id.count).bigEndian) { - append(contentsOf: $0) - } - encodeSmallAtomUTF8(node) - withUnsafeBytes(of: creation) { - append(contentsOf: $0) - } - for element in id { - withUnsafeBytes(of: element.bigEndian) { - append(contentsOf: $0) - } - } - } - - mutating func decodeReference() throws(TermDecodingError) -> (node: String, creation: UInt32, id: [UInt32]) { - guard try kind() == .reference else { throw TermDecodingError.wrongTag } - _ = try consume() - - let idCount = try consume(2).withUnsafeBytes { UInt16(bigEndian: $0.load(as: UInt16.self)) } - - let node = try decodeSmallAtomUTF8() - - let creation = try consume(4).withUnsafeBytes { UInt32(bigEndian: $0.load(as: UInt32.self)) } - - let id = (0.. String { - guard try kind() == .smallAtomUTF8 else { throw TermDecodingError.wrongTag } - _ = try consume() - - let count = Int(try consume()) - let atom = String(decoding: try consume(count), as: UTF8.self) - - return atom - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/SmallBig.swift b/Sources/ExternalTermFormat/Terms/SmallBig.swift deleted file mode 100644 index 62cf11e..0000000 --- a/Sources/ExternalTermFormat/Terms/SmallBig.swift +++ /dev/null @@ -1,30 +0,0 @@ -public extension TermBuffer { - /// Encodes a SMALL_BIG_EXT term. - /// | 1 | 1 | 1 | n | - /// | ----- | --- | ------ | ------------------- | - /// | `110` | `n` | `Sign` | `d(0)` ... `d(n-1)` | - mutating func encodeSmallBig(_ value: Int) { - append(.smallBig) // 'n' - let absValue = abs(value) - let sign: UInt8 = value < 0 ? 1 : 0 - let bytes = withUnsafeBytes(of: absValue.littleEndian) { Array($0) } - append(UInt8(bytes.count)) - append(sign) - append(contentsOf: bytes) - } - - mutating func decodeSmallBig() throws(TermDecodingError) -> Int { - guard try kind() == .smallBig else { - throw TermDecodingError.wrongTag - } - _ = try consume() - let count = Int(try consume(1)[0]) - let sign = try consume(1)[0] - let bytes = try consume(count) - var value: Int = 0 - for (i, byte) in bytes.enumerated() { - value |= Int(byte) << (i * 8) - } - return sign == 1 ? -value : value - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/SmallInteger.swift b/Sources/ExternalTermFormat/Terms/SmallInteger.swift deleted file mode 100644 index d4a2b09..0000000 --- a/Sources/ExternalTermFormat/Terms/SmallInteger.swift +++ /dev/null @@ -1,13 +0,0 @@ -public extension TermBuffer { - /// Unsigned 8-bit integer - mutating func encodeSmallInteger(_ value: UInt8) { - append(.smallInteger) - append(value) - } - - mutating func decodeSmallInteger() throws(TermDecodingError) -> UInt8 { - guard try kind() == .smallInteger else { throw TermDecodingError.wrongTag } - _ = try consume() - return try consume() - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/SmallTuple.swift b/Sources/ExternalTermFormat/Terms/SmallTuple.swift deleted file mode 100644 index de96b22..0000000 --- a/Sources/ExternalTermFormat/Terms/SmallTuple.swift +++ /dev/null @@ -1,15 +0,0 @@ -public extension TermBuffer { - /// Encodes a small tuple header. - mutating func encodeSmallTupleHeader( - arity: UInt8 - ) { - append(.smallTuple) - append(arity) - } - - mutating func decodeSmallTupleHeader() throws(TermDecodingError) -> UInt8 { - guard try kind() == .smallTuple else { throw TermDecodingError.wrongTag } - _ = try consume() - return try consume() - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormat/Terms/String.swift b/Sources/ExternalTermFormat/Terms/String.swift deleted file mode 100644 index a091ac3..0000000 --- a/Sources/ExternalTermFormat/Terms/String.swift +++ /dev/null @@ -1,20 +0,0 @@ -public extension TermBuffer { - /// Encodes a list of bytes. - mutating func encodeString( - _ characters: [UInt8] - ) { - append(.string) - withUnsafeBytes(of: UInt16(characters.count).bigEndian) { - append(contentsOf: $0) - } - append(contentsOf: characters) - } - - mutating func decodeString() throws(TermDecodingError) -> [UInt8] { - guard try kind() == .string else { throw TermDecodingError.wrongTag } - _ = try consume() - - let count = try consume(2).withUnsafeBytes { UInt16(bigEndian: $0.load(as: UInt16.self)) } - return try consume(Int(count)) - } -} \ No newline at end of file diff --git a/Sources/ExternalTermFormatBenchmarks/ExternalTermFormatBenchmarks.swift b/Sources/ExternalTermFormatBenchmarks/ExternalTermFormatBenchmarks.swift new file mode 100644 index 0000000..ad2f2c5 --- /dev/null +++ b/Sources/ExternalTermFormatBenchmarks/ExternalTermFormatBenchmarks.swift @@ -0,0 +1,48 @@ +import ExternalTermFormat + +@main +struct ExternalTermFormatBenchmarks { + static func main() throws { + try test([131, 97, 5]) // .smallInteger(5) + + let binary: [UInt8] = [131,116,0,0,0,9,119,1,99,107,0,13,48,50,102,104,50,48,51,106,102,48,50,57,51, + 119,1,102,119,5,102,97,108,115,101,119,1,98,70,64,52,124,89,108,77,151,87, + 119,1,97,97,10,119,1,107,98,49,228,9,208,119,1,100,108,0,0,0,4,97,32,98,0,0, + 165,112,107,0,4,51,53,50,51,97,234,106,119,1,101,116,0,0,0,2,119,1,114,98,0, + 0,16,249,119,1,103,107,0,5,102,119,101,102,119,119,1,110,119,4,116,114,117, + 101,119,1,122,119,3,110,105,108] + let clock = ContinuousClock() + print(try Term(parsing: binary)) + let start = clock.now + for _ in 0..<1_000_000 { + _ = try Term(parsing: binary) // .list([.smallAtomUTF8("hello"), .smallAtomUTF8("world")]) + } + + let duration = start.duration(to: .now) + print("Swift: \(duration.components.seconds * 1_000_000 + duration.components.attoseconds / 1_000_000_000_000) µs") + } + + static func test(_ binary: [UInt8]) throws { + print(try Term(parsing: binary)) + } +} + +// 2715209 µs +// 494964 µs + +// smallTuple([ +// .smallTuple([.smallAtomUTF8("ok"), .list([.smallTuple([.smallAtomUTF8("user"), .binary([67, 97, 114, 115, 111, 110])]), +// .smallTuple([.smallAtomUTF8("age"), .smallInteger(29)])])]), .nil, .map([ +// MapPair(key: .smallAtomUTF8("active"), value: .smallAtomUTF8("true")), +// MapPair(key: .smallAtomUTF8("name"), value: .binary([67, 97, 114, 115, 111, 110, 32, 75, 97, 116, 114, 105])), +// MapPair(key: .smallAtomUTF8("score"), value: .newFloat([64, 88, 172, 204, 204, 204, 204, 205])) +// ]), +// .list([.smallInteger(1), .smallInteger(2), .smallInteger(3), .smallTuple([ +// .smallAtomUTF8("nested"), +// .list([.smallAtomUTF8("tuple"), .smallAtomUTF8("with"), .smallAtomUTF8("lists")]) +// ]), +// .nil +// ]), +// .binary([0, 1, 2, 3]), +// .nil +// ]) \ No newline at end of file diff --git a/Tests/ExternalTermFormatTests/ExternalTermFormatTests.swift b/Tests/ExternalTermFormatTests/ExternalTermFormatTests.swift index 858978c..7385131 100644 --- a/Tests/ExternalTermFormatTests/ExternalTermFormatTests.swift +++ b/Tests/ExternalTermFormatTests/ExternalTermFormatTests.swift @@ -1,319 +1,325 @@ import Testing -@testable import ExternalTermFormat +import ExternalTermFormat -@Test func smallInteger() throws { +@Test func smallInteger() { let encoded: [UInt8] = [131, 97, 5] - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeSmallInteger() == 5) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeSmallInteger(5) - - #expect(buffer.buffer == encoded) -} - -@Test func binary() throws { - let encoded: [UInt8] = [131, 109, 0, 0, 0, 3, 1, 2, 3] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeBinary() == [1, 2, 3]) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeBinary([1, 2, 3]) - - #expect(buffer.buffer == encoded) -} - -@Test func bitBinary() throws { - let encoded: [UInt8] = [131, 77, 0, 0, 0, 1, 3, 160] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeBitBinary() == (bits: 3, data: [160])) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeBitBinary(bits: 3, data: [160]) - - #expect(buffer.buffer == encoded) -} - -@Test func distributionHeader() throws { - let encoded: [UInt8] = [131, 68, 0] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeDistributionHeader() == 0) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeDistributionHeader() - - #expect(buffer.buffer == encoded) -} - -@Test func export() throws { - let encoded: [UInt8] = [131, 113, 119, 11, 69, 108, 105, 120, 105, 114, 46, 69, 110, 117, 109, 119, 3, 109, 97, 112, 97, 2] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeExport() == (module: "Elixir.Enum", function: "map", arity: 2)) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeExport(module: "Elixir.Enum", function: "map", arity: 2) - - #expect(buffer.buffer == encoded) + print(Term.smallAtomUTF8("hello, world!")) } -@Test func float() throws { - let encoded: [UInt8] = [131, 70, 64, 9, 30, 184, 81, 235, 133, 31] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeFloat() == 3.14) +// @Test func smallInteger() throws { +// let encoded: [UInt8] = [131, 97, 5] - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeFloat(3.14) - - #expect(buffer.buffer == encoded) -} - -@Test func function() throws { - let encoded: [UInt8] = [131, 112, 0, 0, 1, 53, 3, 155, 150, 1, 91, 207, 233, 28, 67, 173, 38, 99, 113, - 23, 87, 162, 37, 0, 0, 0, 40, 0, 0, 0, 1, 119, 8, 101, 114, 108, 95, 101, 118, - 97, 108, 97, 40, 98, 4, 220, 176, 10, 88, 119, 13, 110, 111, 110, 111, 100, - 101, 64, 110, 111, 104, 111, 115, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 104, - 6, 97, 19, 116, 0, 0, 0, 0, 104, 2, 119, 5, 118, 97, 108, 117, 101, 113, 119, - 6, 101, 108, 105, 120, 105, 114, 119, 18, 101, 118, 97, 108, 95, 108, 111, 99, - 97, 108, 95, 104, 97, 110, 100, 108, 101, 114, 97, 2, 104, 2, 119, 5, 118, 97, - 108, 117, 101, 113, 119, 6, 101, 108, 105, 120, 105, 114, 119, 21, 101, 118, - 97, 108, 95, 101, 120, 116, 101, 114, 110, 97, 108, 95, 104, 97, 110, 100, 108, - 101, 114, 97, 3, 116, 0, 0, 0, 0, 108, 0, 0, 0, 1, 104, 5, 119, 6, 99, 108, 97, - 117, 115, 101, 97, 19, 108, 0, 0, 0, 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, - 119, 4, 95, 97, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, - 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, - 108, 0, 0, 0, 1, 104, 3, 119, 5, 116, 117, 112, 108, 101, 97, 19, 108, 0, 0, 0, - 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 97, 64, 49, 104, 3, 119, - 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, 64, 49, 104, 3, 119, 3, 118, 97, 114, - 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, 106] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - let function = try buffer.decodeFunction() - #expect(function.arity == 3) - #expect(function.module == "erl_eval") - #expect(function.pid.node == PID(node: "nonode@nohost", id: 0, serial: 0, creation: 0).node) +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeSmallInteger() == 5) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeSmallInteger(5) + +// #expect(buffer.buffer == encoded) +// } + +// @Test func binary() throws { +// let encoded: [UInt8] = [131, 109, 0, 0, 0, 3, 1, 2, 3] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeBinary() == [1, 2, 3]) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeBinary([1, 2, 3]) + +// #expect(buffer.buffer == encoded) +// } + +// @Test func bitBinary() throws { +// let encoded: [UInt8] = [131, 77, 0, 0, 0, 1, 3, 160] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeBitBinary() == (bits: 3, data: [160])) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeBitBinary(bits: 3, data: [160]) + +// #expect(buffer.buffer == encoded) +// } + +// @Test func distributionHeader() throws { +// let encoded: [UInt8] = [131, 68, 0] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeDistributionHeader() == 0) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeDistributionHeader() + +// #expect(buffer.buffer == encoded) +// } + +// @Test func export() throws { +// let encoded: [UInt8] = [131, 113, 119, 11, 69, 108, 105, 120, 105, 114, 46, 69, 110, 117, 109, 119, 3, 109, 97, 112, 97, 2] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeExport() == (module: "Elixir.Enum", function: "map", arity: 2)) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeExport(module: "Elixir.Enum", function: "map", arity: 2) + +// #expect(buffer.buffer == encoded) +// } + +// @Test func float() throws { +// let encoded: [UInt8] = [131, 70, 64, 9, 30, 184, 81, 235, 133, 31] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeFloat() == 3.14) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeFloat(3.14) + +// #expect(buffer.buffer == encoded) +// } + +// @Test func function() throws { +// let encoded: [UInt8] = [131, 112, 0, 0, 1, 53, 3, 155, 150, 1, 91, 207, 233, 28, 67, 173, 38, 99, 113, +// 23, 87, 162, 37, 0, 0, 0, 40, 0, 0, 0, 1, 119, 8, 101, 114, 108, 95, 101, 118, +// 97, 108, 97, 40, 98, 4, 220, 176, 10, 88, 119, 13, 110, 111, 110, 111, 100, +// 101, 64, 110, 111, 104, 111, 115, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 104, +// 6, 97, 19, 116, 0, 0, 0, 0, 104, 2, 119, 5, 118, 97, 108, 117, 101, 113, 119, +// 6, 101, 108, 105, 120, 105, 114, 119, 18, 101, 118, 97, 108, 95, 108, 111, 99, +// 97, 108, 95, 104, 97, 110, 100, 108, 101, 114, 97, 2, 104, 2, 119, 5, 118, 97, +// 108, 117, 101, 113, 119, 6, 101, 108, 105, 120, 105, 114, 119, 21, 101, 118, +// 97, 108, 95, 101, 120, 116, 101, 114, 110, 97, 108, 95, 104, 97, 110, 100, 108, +// 101, 114, 97, 3, 116, 0, 0, 0, 0, 108, 0, 0, 0, 1, 104, 5, 119, 6, 99, 108, 97, +// 117, 115, 101, 97, 19, 108, 0, 0, 0, 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, +// 119, 4, 95, 97, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, +// 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, +// 108, 0, 0, 0, 1, 104, 3, 119, 5, 116, 117, 112, 108, 101, 97, 19, 108, 0, 0, 0, +// 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 97, 64, 49, 104, 3, 119, +// 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, 64, 49, 104, 3, 119, 3, 118, 97, 114, +// 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, 106] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// let function = try buffer.decodeFunction() +// #expect(function.arity == 3) +// #expect(function.module == "erl_eval") +// #expect(function.pid.node == PID(node: "nonode@nohost", id: 0, serial: 0, creation: 0).node) - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeFunction(arity: 3, uniq: [155, 150, 1, 91, 207, 233, 28, 67, 173, 38, 99, 113, 23, 87, 162, 37], index: 40, module: "erl_eval", oldIndex: 40, oldUniq: 81571850, pid: PID(node: "nonode@nohost", id: 0, serial: 0, creation: 0), freeVars: [[104, 6, 97, 19, 116, 0, 0, 0, 0, 104, 2, 119, 5, 118, 97, 108, 117, 101, 113, 119, 6, 101, 108, 105, 120, 105, 114, 119, 18, 101, 118, 97, 108, 95, 108, 111, 99, 97, 108, 95, 104, 97, 110, 100, 108, 101, 114, 97, 2, 104, 2, 119, 5, 118, 97, 108, 117, 101, 113, 119, 6, 101, 108, 105, 120, 105, 114, 119, 21, 101, 118, 97, 108, 95, 101, 120, 116, 101, 114, 110, 97, 108, 95, 104, 97, 110, 100, 108, 101, 114, 97, 3, 116, 0, 0, 0, 0, 108, 0, 0, 0, 1, 104, 5, 119, 6, 99, 108, 97, 117, 115, 101, 97, 19, 108, 0, 0, 0, 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 97, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, 108, 0, 0, 0, 1, 104, 3, 119, 5, 116, 117, 112, 108, 101, 97, 19, 108, 0, 0, 0, 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 97, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, 106]]) +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeFunction(arity: 3, uniq: [155, 150, 1, 91, 207, 233, 28, 67, 173, 38, 99, 113, 23, 87, 162, 37], index: 40, module: "erl_eval", oldIndex: 40, oldUniq: 81571850, pid: PID(node: "nonode@nohost", id: 0, serial: 0, creation: 0), freeVars: [[104, 6, 97, 19, 116, 0, 0, 0, 0, 104, 2, 119, 5, 118, 97, 108, 117, 101, 113, 119, 6, 101, 108, 105, 120, 105, 114, 119, 18, 101, 118, 97, 108, 95, 108, 111, 99, 97, 108, 95, 104, 97, 110, 100, 108, 101, 114, 97, 2, 104, 2, 119, 5, 118, 97, 108, 117, 101, 113, 119, 6, 101, 108, 105, 120, 105, 114, 119, 21, 101, 118, 97, 108, 95, 101, 120, 116, 101, 114, 110, 97, 108, 95, 104, 97, 110, 100, 108, 101, 114, 97, 3, 116, 0, 0, 0, 0, 108, 0, 0, 0, 1, 104, 5, 119, 6, 99, 108, 97, 117, 115, 101, 97, 19, 108, 0, 0, 0, 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 97, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, 108, 0, 0, 0, 1, 104, 3, 119, 5, 116, 117, 112, 108, 101, 97, 19, 108, 0, 0, 0, 3, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 97, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 98, 64, 49, 104, 3, 119, 3, 118, 97, 114, 97, 19, 119, 4, 95, 99, 64, 49, 106, 106, 106]]) - #expect(buffer.buffer == encoded) -} - -@Test func integer() throws { - let encoded: [UInt8] = [131, 98, 255, 255, 252, 0] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeInteger() == -1024) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeInteger(-1024) - - #expect(buffer.buffer == encoded) -} - -@Test func big() throws { - let encoded: [UInt8] = [131, 110, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeSmallBig() == 576460752303423488) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeSmallBig(576460752303423488) - - #expect(buffer.buffer == encoded) -} - -@Test func tuple() throws { - let encoded: [UInt8] = [131, 104, 3, 97, 1, 97, 2, 97, 3] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeSmallTupleHeader() == 3) - #expect(try buffer.decodeSmallInteger() == 1) - #expect(try buffer.decodeSmallInteger() == 2) - #expect(try buffer.decodeSmallInteger() == 3) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeSmallTupleHeader(arity: 3) - buffer.encodeSmallInteger(1) - buffer.encodeSmallInteger(2) - buffer.encodeSmallInteger(3) - - #expect(buffer.buffer == encoded) -} - -@Test func list() throws { - let encoded: [UInt8] = [131, 108, 0, 0, 0, 3, 119, 1, 97, 119, 1, 98, 119, 1, 99, 106] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeListHeader() == 3) - #expect(try buffer.decodeSmallAtomUTF8() == "a") - #expect(try buffer.decodeSmallAtomUTF8() == "b") - #expect(try buffer.decodeSmallAtomUTF8() == "c") - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeListHeader(arity: 3) - buffer.encodeSmallAtomUTF8("a") - buffer.encodeSmallAtomUTF8("b") - buffer.encodeSmallAtomUTF8("c") - buffer.encodeNil() - - #expect(buffer.buffer == encoded) -} - -@Test func map() throws { - let encoded: [UInt8] = [131, 116, 0, 0, 0, 2, 97, 1, 97, 2, 97, 3, 97, 4] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeMapHeader() == 2) - #expect(try buffer.decodeSmallInteger() == 1) - #expect(try buffer.decodeSmallInteger() == 2) - #expect(try buffer.decodeSmallInteger() == 3) - #expect(try buffer.decodeSmallInteger() == 4) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeMapHeader(arity: 2) - buffer.encodeSmallInteger(1) - buffer.encodeSmallInteger(2) - buffer.encodeSmallInteger(3) - buffer.encodeSmallInteger(4) - - #expect(buffer.buffer == encoded) -} - -@Test func nilCoding() throws { - let encoded: [UInt8] = [131, 106] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - try buffer.decodeNil() - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeNil() - - #expect(buffer.buffer == encoded) -} - -@Test func pid() throws { - let encoded: [UInt8] = [131, 88, 119, 13, 110, 111, 110, 111, 100, 101, 64, 110, 111, 104, 111, 115, 116, 0, 0, 0, 104, 0, 0, 0, 0, 0, 0, 0, 0] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodePID().node == PID(node: "nonode@nohost", id: 104, serial: 0, creation: 0).node) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodePID(PID(node: "nonode@nohost", id: 104, serial: 0, creation: 0)) +// #expect(buffer.buffer == encoded) +// } + +// @Test func integer() throws { +// let encoded: [UInt8] = [131, 98, 255, 255, 252, 0] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeInteger() == -1024) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeInteger(-1024) + +// #expect(buffer.buffer == encoded) +// } + +// @Test func big() throws { +// let encoded: [UInt8] = [131, 110, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeSmallBig() == 576460752303423488) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeSmallBig(576460752303423488) + +// #expect(buffer.buffer == encoded) +// } + +// @Test func tuple() throws { +// let encoded: [UInt8] = [131, 104, 3, 97, 1, 97, 2, 97, 3] + +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeSmallTupleHeader() == 3) +// #expect(try buffer.decodeSmallInteger() == 1) +// #expect(try buffer.decodeSmallInteger() == 2) +// #expect(try buffer.decodeSmallInteger() == 3) + +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeSmallTupleHeader(arity: 3) +// buffer.encodeSmallInteger(1) +// buffer.encodeSmallInteger(2) +// buffer.encodeSmallInteger(3) + +// #expect(buffer.buffer == encoded) +// } - #expect(buffer.buffer == encoded) -} - -@Test func port() throws { - let encoded: [UInt8] = [131, 120, 119, 13, 110, 111, 110, 111, 100, 101, 64, 110, 111, 104, 111, 115, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodePort() == (node: "nonode@nohost", id: 0, creation: 0)) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodePort(node: "nonode@nohost", id: 0, creation: 0) - - #expect(buffer.buffer == encoded) -} - -@Test func reference() throws { - let encoded: [UInt8] = [131, 90, 0, 3, 119, 13, 110, 111, 110, 111, 100, 101, 64, 110, 111, 104, 111, 115, 116, 0, 0, 0, 0, 0, 1, 22, 124, 218, 72, 0, 5, 196, 59, 199, 159] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeReference() == (node: "nonode@nohost", creation: 0, id: [71292, 3662151685, 3292252063])) - - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeReference(node: "nonode@nohost", creation: 0, id: [71292, 3662151685, 3292252063]) +// @Test func list() throws { +// let encoded: [UInt8] = [131, 108, 0, 0, 0, 3, 119, 1, 97, 119, 1, 98, 119, 1, 99, 106] - #expect(buffer.buffer == encoded) -} - -@Test func smallAtomUTF8() throws { - let encoded: [UInt8] = [131, 119, 5, 104, 101, 108, 108, 111] - - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeSmallAtomUTF8() == "hello") +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeListHeader() == 3) +// #expect(try buffer.decodeSmallAtomUTF8() == "a") +// #expect(try buffer.decodeSmallAtomUTF8() == "b") +// #expect(try buffer.decodeSmallAtomUTF8() == "c") - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeSmallAtomUTF8("hello") - - #expect(buffer.buffer == encoded) -} +// buffer = TermBuffer() +// buffer.encodeVersion() +// buffer.encodeListHeader(arity: 3) +// buffer.encodeSmallAtomUTF8("a") +// buffer.encodeSmallAtomUTF8("b") +// buffer.encodeSmallAtomUTF8("c") +// buffer.encodeNil() -@Test func string() throws { - let encoded: [UInt8] = [131, 107, 0, 3, 97, 98, 99] +// #expect(buffer.buffer == encoded) +// } - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - #expect(try buffer.decodeString() == ["a" as Character, "b" as Character, "c" as Character].map(\.asciiValue)) +// @Test func map() throws { +// let encoded: [UInt8] = [131, 116, 0, 0, 0, 2, 97, 1, 97, 2, 97, 3, 97, 4] - buffer = TermBuffer() - buffer.encodeVersion() - buffer.encodeString(["a" as Character, "b" as Character, "c" as Character].compactMap(\.asciiValue)) - - #expect(buffer.buffer == encoded) -} - -@Test func messageTest() throws { - let encoded: [UInt8] = [131, 68, 0, 104, 4, 97, 6, 88, 119, 13, 119, 101, 98, 64, 49, 50, 55, 46, 48, 46, 48, 46, 49, 0, 0, 0, 0, 0, 0, 0, 0, 104, 54, 130, 92, 119, 0, 119, 3, 102, 111, 111, 97, 5] - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - print(try buffer.decodeDistributionHeader()) - print(try buffer.decodeSmallTupleHeader()) - print(try buffer.decodeSmallInteger()) - print(try buffer.decodePID()) - print(try buffer.decodeSmallAtomUTF8() == "") - print(try buffer.decodeSmallAtomUTF8()) - print(try buffer.decodeSmallInteger()) -} +// var buffer = TermBuffer(encoded) +// _ = try buffer.decodeVersion() +// #expect(try buffer.decodeMapHeader() == 2) +// #expect(try buffer.decodeSmallInteger() == 1) +// #expect(try buffer.decodeSmallInteger() == 2) +// #expect(try buffer.decodeSmallInteger() == 3) +// #expect(try buffer.decodeSmallInteger() == 4) -@Test func skip() throws { - let encoded: [UInt8] = [131, 68, 0, 104, 4, 97, 6, 88, 119, 13, 119, 101, 98, 64, 49, 50, 55, 46, 48, 46, 48, 46, 49, 0, 0, 0, 0, 0, 0, 0, 0, 104, 54, 130, 92, 119, 0, 119, 3, 102, 111, 111, 97, 5] - var buffer = TermBuffer(encoded) - _ = try buffer.decodeVersion() - _ = try buffer.decodeDistributionHeader() - let controlMessageStart = buffer.index - try buffer.skip() // skip the control message tuple - let controlMessage = buffer.buffer[controlMessageStart..