1 /// Code generation module.
2 module pbd.codegen;
3 
4 import std.meta : AliasSeq;
5 
6 import pegged.grammar : ParseTree;
7 
8 
9 /// string dict converts Proto types to D types
10 /// See also: https://developers.google.com/protocol-buffers/docs/proto#scalar
11 enum Proto2DTypes = [
12     "double": "double",
13     "float": "float",
14     "int32": "int",
15     "int64": "long",
16     "uint32": "uint",
17     "uint64": "ulong",
18     "sint32": "@ZigZag int",
19     "sint64": "@ZigZag long",
20     "fixed32": "uint",
21     "fixed64": "ulong",
22     "sfixed32": "int",
23     "sfixed64": "long",
24     "bool": "bool",
25     "string": "ProtoArray!char",
26     "bytes": "ProtoArray!byte",
27 ];
28 
29 struct ProtoTag
30 {
31   /// Tag number for encoding/decoding
32   int tag = 0;
33 }
34 
35 /// Returns tag of a given member or 0 if not found.
36 int protoTagOf(T, string member)()
37 {
38   import std.traits : getUDAs;
39   enum udas = getUDAs!(__traits(getMember, T, member), ProtoTag);
40   static assert(udas.length == 1);
41   return udas[0].tag;
42 }
43 
44 struct ZigZag {};
45 struct Unpacked {};
46 
47 /// Returns true if member is zigzag encoded.
48 bool isZigZag(T, string member)()
49 {
50   import std.traits : hasUDA;
51   return hasUDA!(__traits(getMember, T, member), ZigZag);
52 }
53 
54 /// Returns a tagged member name corresponding to a given tag.
55 string protoMemberOf(T)(int tag)
56 {
57   foreach (name; __traits(allMembers, T))
58   {
59     alias m = __traits(getMember, T, name);
60     foreach (attr; __traits(getAttributes, m))
61     {
62       static if (is(typeof(attr) == ProtoTag))
63       {
64         if (attr.tag == tag)
65         {
66           return name;
67         }
68       }
69     }
70   }
71   assert(false, "@ProtoTag not found.");
72 }
73 
74 ///
75 version (pbd_test)
76 unittest
77 {
78   struct Test
79   {
80     @ProtoTag(2)
81     int i;
82     @ZigZag
83     @ProtoTag(1)
84     int j;
85   }
86 
87   static assert(protoTagOf!(Test, "i") == 2);
88   static assert(!isZigZag!(Test, "i"));
89   static assert(protoMemberOf!Test(2) == "i");
90   static assert(isZigZag!(Test, "j"));
91 }
92 
93 /// Generates D code from Protobuf IDL ParseTree (Proto result).
94 string toD(ParseTree p, int numIndent = 0, string indent = "  ")
95 {
96   import std.range : repeat, join;
97 
98   auto spaces = indent.repeat(numIndent).join;
99   switch (p.name)
100   {
101     case "Proto":
102       assert(p.children.length == 1, "Proto should have only one child.");
103       return "import pbd.codegen;\n" ~ toD(p.children[0], numIndent, indent);
104     case "Proto.Root":
105       string code;
106       foreach (child; p.children)
107       {
108         code ~= toD(child, numIndent, indent);
109       }
110       return code;
111     case "Proto.Syntax":
112       assert(p.matches[0] == "proto3", `only syntax = "proto3" is supported.`);
113       return "";
114     case "Proto.Package":
115       // TODO(karita): support package?
116       return "";
117     case "Proto.Option":
118       // TODO(karita): support option?
119       return "";
120     case "Proto.Message":
121       string code = spaces ~ "struct " ~ p.matches[0] ~ " {\n";
122       foreach (child; p.children)
123       {
124         code ~= toD(child, numIndent + 1, indent);
125       }
126       code ~= spaces ~ "}\n";
127       return code;
128     case "Proto.SingleField":
129       // e.g., int32 a = 1;
130       auto type = Proto2DTypes[p.matches[0]];
131       auto name = p.matches[1];
132       auto tag = "@ProtoTag(" ~ p.matches[2] ~ ") ";
133       // [packed = true];
134       auto packed = p.matches.length == 3 ||
135                     (p.matches[3] == "packed" && p.matches[4] == "true")
136                     ? "" : "@Unpacked ";
137       return spaces ~ packed ~ tag ~ type ~ " " ~ name ~ ";\n";
138     default:
139       assert(false, p.name ~ " unsupported");
140   }
141 }
142 
143 ///
144 version (pbd_test)
145 @nogc nothrow pure @safe
146 unittest
147 {
148   import std.stdio;
149   import pbd.parse : Proto;
150 
151   enum exampleProto = `
152 syntax = "proto3";
153 
154 package tensorflow;
155 
156 message Foo {
157   int32 aa = 1;
158   sint32 bb = 2;
159 }
160 `;
161 
162   // example of generated code
163   import pbd.codegen;
164   struct ExpectedFoo {
165     @ProtoTag(1) int aa;
166     @ZigZag @ProtoTag(2) int bb;
167   }
168 
169   enum tree = Proto(exampleProto);
170   enum code = tree.toD(0, "  ");
171   mixin(code);
172 
173   static assert(is(typeof(Foo.aa) == int));
174   static assert(protoTagOf!(Foo, "aa") == 1);
175   static assert(!isZigZag!(Foo, "aa"));
176 
177   static assert(is(typeof(Foo.bb) == int));
178   static assert(protoTagOf!(Foo, "bb") == 2);
179   static assert(isZigZag!(Foo, "bb"));
180   // writeln("generated:\n", code);
181 }
182 
183 
184 /// Custom array type for pb decoding
185 struct ProtoArray(T)
186 {
187   import std.container.array : Array;
188 
189   Array!T base;
190   alias base this;
191 
192   /// Converts to string for debugging
193   string toString() const
194   {
195     import std.conv : text;
196     return this.base[].text;
197   }
198 
199   static if (is(T == char))
200   {
201     /// Constructs from string
202     this(string rhs)
203     {
204       this.base.clear();
205       this.base.reserve(rhs.length);
206       foreach (x; rhs)
207       {
208         this.base.insertBack(x);
209       }
210     }
211 
212     /// Compares to non-string types
213     bool opEquals(Rhs)(auto ref Rhs rhs)
214     {
215       return this.base == rhs;
216     }
217 
218     /// Compares to string
219     @nogc nothrow pure bool opEquals(Rhs : string)(Rhs rhs)
220     {
221       // if (this.length != rhs.length) return false;
222       // foreach (i; 0 .. this.base.length)
223       // {
224       //   if (this.base[i] != rhs[i]) return false;
225       // }
226       // return true;
227       return (&this[0])[0 .. this.length] == rhs;
228     }
229   }
230 }
231 
232 alias pstring = ProtoArray!char;
233 
234 version (pbd_test)
235 @nogc nothrow pure
236 unittest
237 {
238     pstring cs = "abc";
239     assert(cs == "abc");
240 }
241 
242 auto ProtoToD(string path)()
243 {
244   import pbd.parse : Proto;
245   return Proto(import(path)).toD;
246 }