From 53197b51e5edbb1b195070523dbfd8d9ba05d847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Thu, 15 Jun 2023 09:42:06 +0200 Subject: [PATCH] Add a Swift backend for LibertyXDR --- CMakeLists.txt | 10 +- README.adoc | 3 + tools/lxdrgen-go.awk | 15 +-- tools/lxdrgen-mjs.awk | 6 +- tools/lxdrgen-swift.awk | 276 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 298 insertions(+), 12 deletions(-) create mode 100644 tools/lxdrgen-swift.awk diff --git a/CMakeLists.txt b/CMakeLists.txt index c521568..42590a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,7 +72,7 @@ add_test (test-cmake-parser # Test protocol code generation set (lxdrgen_outputs) set (lxdrgen_base "${PROJECT_BINARY_DIR}/lxdrgen.lxdr") -foreach (backend c go mjs) +foreach (backend c go mjs swift) list (APPEND lxdrgen_outputs ${lxdrgen_base}.${backend}) add_custom_command (OUTPUT ${lxdrgen_base}.${backend} COMMAND env LC_ALL=C awk @@ -108,3 +108,11 @@ if (NODE_EXECUTABLE) else () message (WARNING "Cannot test generated protocol code for Javascript") endif () + +find_program (SWIFTC_EXECUTABLE swiftc) +if (SWIFTC_EXECUTABLE) + add_test (test-lxdrgen-swift + ${SWIFTC_EXECUTABLE} -typecheck ${lxdrgen_base}.swift) +else () + message (WARNING "Cannot test generated protocol code for Swift") +endif () diff --git a/README.adoc b/README.adoc index 3c500dc..f392f7f 100644 --- a/README.adoc +++ b/README.adoc @@ -54,6 +54,9 @@ lxdrgen-mjs.awk:: LibertyXDR backend for Javascript, currently for decoding only. It cuts a corner by not using BigInts, on par with `JSON.parse()`. +lxdrgen-swift.awk:: + LibertyXDR backend for the Swift programming language. + Contributing and Support ------------------------ Use https://git.janouch.name/p/liberty to report any bugs, request features, diff --git a/tools/lxdrgen-go.awk b/tools/lxdrgen-go.awk index 6aa0d04..f9163bb 100644 --- a/tools/lxdrgen-go.awk +++ b/tools/lxdrgen-go.awk @@ -441,7 +441,7 @@ function codegen_struct(name, cg, gotype) { function codegen_union_tag(d, cg) { cg["tagtype"] = d["type"] - cg["tagname"] = d["name"] + cg["tagname"] = snaketocamel(d["name"]) # The tag is implied from the type of struct stored in the interface. } @@ -450,8 +450,8 @@ function codegen_union_struct(name, casename, cg, scg, structname, init) { structname = name snaketocamel(casename) codegen_struct(structname, scg) - init = CodegenGoType[structname] "{" snaketocamel(cg["tagname"]) \ - ": " decapitalize(snaketocamel(cg["tagname"])) "}" + init = CodegenGoType[structname] "{" cg["tagname"] \ + ": " decapitalize(cg["tagname"]) "}" append(cg, "unmarshal", "\tcase " CodegenGoType[cg["tagtype"]] snaketocamel(casename) ":\n" \ "\t\ts := " init "\n" \ @@ -467,7 +467,7 @@ function codegen_union_struct(name, casename, cg, scg, structname, init) { "\t\tu.Interface = &s\n") } -function codegen_union(name, cg, gotype, tagfield, tagvar) { +function codegen_union(name, cg, gotype, tagvar) { gotype = PrefixCamel name print "type " gotype " struct {" print "\tInterface any" @@ -481,18 +481,17 @@ function codegen_union(name, cg, gotype, tagfield, tagvar) { print "}" print "" - tagfield = snaketocamel(cg["tagname"]) - tagvar = decapitalize(tagfield) + tagvar = decapitalize(cg["tagname"]) print "func (u *" gotype ") UnmarshalJSON(data []byte) (err error) {" print "\tvar t struct {" - print "\t\t" tagfield " " CodegenGoType[cg["tagtype"]] \ + print "\t\t" cg["tagname"] " " CodegenGoType[cg["tagtype"]] \ " `json:\"" tagvar "\"`" print "\t}" print "\tif err := json.Unmarshal(data, &t); err != nil {" print "\t\treturn err" print "\t}" print "" - print "\tswitch " tagvar " := t." tagfield "; " tagvar " {" + print "\tswitch " tagvar " := t." cg["tagname"] "; " tagvar " {" print cg["unmarshal"] "\tdefault:" print "\t\terr = errors.New(`unsupported value: ` + " tagvar ".String())" print "\t}" diff --git a/tools/lxdrgen-mjs.awk b/tools/lxdrgen-mjs.awk index a9a81f7..01ee97d 100644 --- a/tools/lxdrgen-mjs.awk +++ b/tools/lxdrgen-mjs.awk @@ -185,14 +185,14 @@ function codegen_struct(name, cg) { function codegen_union_tag(d, cg) { cg["tagtype"] = d["type"] - cg["tagname"] = d["name"] + cg["tagname"] = decapitalize(snaketocamel(d["name"])) } function codegen_union_struct(name, casename, cg, scg, structname) { append(scg, "methods", "\n" \ "\tconstructor() {\n" \ - "\t\tthis." decapitalize(snaketocamel(cg["tagname"])) \ + "\t\tthis." cg["tagname"] \ " = " cg["tagtype"] "." snaketocamel(casename) "\n" \ "\t}\n") @@ -209,7 +209,7 @@ function codegen_union_struct(name, casename, cg, scg, structname) { } function codegen_union(name, cg, tagvar) { - tagvar = decapitalize(snaketocamel(cg["tagname"])) + tagvar = cg["tagname"] print "" print "export function deserialize" name "(r) {" diff --git a/tools/lxdrgen-swift.awk b/tools/lxdrgen-swift.awk new file mode 100644 index 0000000..28554c9 --- /dev/null +++ b/tools/lxdrgen-swift.awk @@ -0,0 +1,276 @@ +# lxdrgen-swift.awk: Swift backend for lxdrgen.awk. +# +# Copyright (c) 2023, Přemysl Eric Janouch +# SPDX-License-Identifier: 0BSD + +function define_internal(name, swifttype) { + Types[name] = "internal" + CodegenSwiftType[name] = swifttype + CodegenDeserialize[name] = "%s.read()" +} + +function define_sint(size, shortname, swifttype) { + shortname = "i" size + swifttype = "Int" size + define_internal(shortname, swifttype) +} + +function define_uint(size, shortname, swifttype) { + shortname = "u" size + swifttype = "UInt" size + define_internal(shortname, swifttype) +} + +function codegen_begin() { + define_sint("8") + define_sint("16") + define_sint("32") + define_sint("64") + define_uint("8") + define_uint("16") + define_uint("32") + define_uint("64") + define_internal("bool", "Bool") + define_internal("string", "String") + + print "// Code generated from " FILENAME ". DO NOT EDIT." + print "import Foundation" + print "" + print "public struct RelayReader {" + print "\tpublic var data: Data" + print "" + print "\tpublic enum ReadError: Error {" + print "\t\tcase unexpectedEOF" + print "\t\tcase invalidEncoding" + print "\t\tcase overflow" + print "\t\tcase unexpectedValue" + print "\t}" + print "" + print "\tpublic mutating func read() throws -> T {" + print "\t\tlet size = MemoryLayout.size" + print "\t\tguard data.count >= size else {" + print "\t\t\tthrow ReadError.unexpectedEOF" + print "\t\t}" + print "\t\tvar acc: T = 0" + print "\t\tdata.prefix(size).forEach { acc = acc << 8 | T($0) }" + print "\t\tdata = data.dropFirst(size)" + print "\t\treturn acc" + print "\t}" + print "" + print "\tpublic mutating func read() throws -> Bool {" + print "\t\ttry read() != UInt8(0)" + print "\t}" + print "" + print "\tpublic mutating func read() throws -> String {" + print "\t\tlet size: UInt32 = try self.read()" + print "\t\tguard let count = Int(exactly: size) else {" + print "\t\t\tthrow ReadError.overflow" + print "\t\t}" + print "\t\tguard data.count >= count else {" + print "\t\t\tthrow ReadError.unexpectedEOF" + print "\t\t}" + print "\t\tdefer {" + print "\t\t\tdata = data.dropFirst(count)" + print "\t\t}" + print "\t\tif let s = String(data: data.prefix(count), encoding: .utf8) {" + print "\t\t\treturn s" + print "\t\t} else {" + print "\t\t\tthrow ReadError.invalidEncoding" + print "\t\t}" + print "\t}" + print "" + print "\tpublic mutating func read<" \ + "T: RawRepresentable>() throws -> T {" + print "\t\tguard let value = T(rawValue: try read()) else {" + print "\t\t\tthrow ReadError.unexpectedValue" + print "\t\t}" + print "\t\treturn value" + print "\t}" + print "" + print "\tpublic mutating func read(" + print "\t\t\t_ read: (inout Self) throws -> T) throws -> [T] {" + print "\t\tlet size: UInt32 = try self.read()" + print "\t\tguard let count = Int(exactly: size) else {" + print "\t\t\tthrow ReadError.overflow" + print "\t\t}" + print "\t\tvar array = [T]()" + print "\t\tarray.reserveCapacity(count)" + print "\t\tfor _ in 0..(_ number: T) {" + print "\t\tvar n = number.byteSwapped" + print "\t\tfor _ in 0...size {" + print "\t\t\tdata.append(UInt8(truncatingIfNeeded: n))" + print "\t\t\tn >>= 8" + print "\t\t}" + print "\t}" + print "" + print "\tpublic mutating func append(_ bool: Bool) {" + print "\t\tappend(UInt8(bool ? 1 : 0))" + print "\t}" + print "" + print "\tpublic mutating func append(_ string: String) {" + print "\t\tlet bytes = string.data(using: .utf8)!" + print "\t\tappend(UInt32(bytes.count))" + print "\t\tdata.append(bytes)" + print "\t}" + print "" + print "\tpublic mutating func append<" \ + "T: RawRepresentable>(_ value: T) {" + print "\t\tappend(value.rawValue)" + print "\t}" + print "" + print "\tpublic mutating func append(" + print "\t\t\t_ array: Array, _ write: (inout Self, T) -> ()) {" + print "\t\tappend(UInt32(array.count))" + print "\t\tfor i in 0..(_ value: T) {" + print "\t\tvalue.encode(to: &self)" + print "\t}" + print "}" + print "" + print "public protocol RelayEncodable { " \ + "func encode(to: inout RelayWriter) }" +} + +function codegen_constant(name, value) { + print "" + print "public let " decapitalize(PrefixCamel snaketocamel(name)) " = " value +} + +function codegen_enum_value(name, subname, value, cg) { + append(cg, "fields", + "\tcase " decapitalize(snaketocamel(subname)) " = " value "\n") +} + +function codegen_enum(name, cg, swifttype) { + swifttype = PrefixCamel name + print "" + print "public enum " swifttype ": Int8 {" + print cg["fields"] "}" + + CodegenSwiftType[name] = swifttype + CodegenDeserialize[name] = "%s.read()" + for (i in cg) + delete cg[i] +} + +function codegen_struct_field(d, cg, camel) { + camel = decapitalize(snaketocamel(d["name"])) + if (!d["isarray"]) { + append(cg, "fields", + "\tpublic var " camel ": " CodegenSwiftType[d["type"]] "\n") + append(cg, "deserialize", + "\t\tself." camel " = try " \ + sprintf(CodegenDeserialize[d["type"]], "from") "\n") + append(cg, "serialize", + "\t\tto.append(self." camel ")\n") + return + } + + append(cg, "fields", + "\tpublic var " camel ": [" CodegenSwiftType[d["type"]] "]\n") + append(cg, "deserialize", + "\t\tself." camel " = try from.read() { r in try " \ + sprintf(CodegenDeserialize[d["type"]], "r") " }\n") + append(cg, "serialize", + "\t\tto.append(self." camel ") { (w, value) in w.append(value) }\n") +} + +function codegen_struct_tag(d, cg, camel) { + camel = decapitalize(snaketocamel(d["name"])) + append(cg, "serialize", + "\t\tto.append(self." camel ")\n") +} + +function codegen_struct(name, cg, swifttype) { + swifttype = PrefixCamel name + print "" + print "public struct " swifttype " {\n" cg["fields"] "}" + print "" + print "extension " swifttype ": " PrefixCamel "Encodable {" + print "\tpublic init(from: inout RelayReader) throws {" + print cg["deserialize"] "\t}" + print "" + print "\tpublic func encode(to: inout RelayWriter) {" + print cg["serialize"] "\t}" + print "}" + + CodegenSwiftType[name] = swifttype + CodegenDeserialize[name] = "%s.read()" + for (i in cg) + delete cg[i] +} + +function codegen_union_tag(d, cg) { + cg["tagtype"] = d["type"] + cg["tagname"] = decapitalize(snaketocamel(d["name"])) +} + +function codegen_union_struct(name, casename, cg, scg, swifttype) { + # And thus not all generated structs are present in Types. + swifttype = PrefixCamel name snaketocamel(casename) + casename = decapitalize(snaketocamel(casename)) + print "" + print "public struct " swifttype ": " PrefixCamel name " {" + print "\tpublic var " cg["tagname"] \ + ": " CodegenSwiftType[cg["tagtype"]] " { ." casename " }" + print scg["fields"] "}" + print "" + print "extension " swifttype ": " PrefixCamel "Encodable {" + print "\tfileprivate init(from: inout RelayReader) throws {" + print scg["deserialize"] "\t}" + print "" + print "\tpublic func encode(to: inout RelayWriter) {" + print scg["serialize"] "\t}" + print "}" + + append(cg, "cases", "\tcase ." casename ":\n" \ + "\t\treturn try " swifttype "(from: &from)\n") + + CodegenSwiftType[name] = swifttype + CodegenDeserialize[name] = "%s.read()" + for (i in scg) + delete scg[i] +} + +function codegen_union(name, cg, swifttype, init) { + # Classes don't have automatic member-wise initializers, + # thus using structs and protocols. + swifttype = PrefixCamel name + print "" + print "public protocol " swifttype ": " PrefixCamel "Encodable {" + print "\tvar " cg["tagname"] ": " CodegenSwiftType[cg["tagtype"]] " { get }" + print "}" + + init = decapitalize(swifttype) + print "" + print "public func " init \ + "(from: inout RelayReader) throws -> " swifttype " {" + print "\tlet " cg["tagname"] ": " CodegenSwiftType[cg["tagtype"]] \ + " = try from.read()" + print "\tswitch " cg["tagname"] " {" + # TODO: Only generate the default if there are remaining values, + # so that swiftc doesn't produce warnings. + print cg["cases"] "\tdefault:" + print "\t\tthrow RelayReader.ReadError.unexpectedValue" + print"\t}" + print "}" + + CodegenSwiftType[name] = swifttype + CodegenDeserialize[name] = init "(from: &%s)" + for (i in cg) + delete cg[i] +}