1 module lumars.emmylua;
2 
3 import lumars;
4 
5 struct EmmyLuaBuilder
6 {
7     import std.array : Appender;
8 
9     private Appender!(char[]) _output;
10     private string[] _tables;
11     private string[] _typeNames;
12 
13     private void addTable(string name)
14     {
15         import std.algorithm : canFind;
16 
17         if(!name.length)
18             return;
19 
20         if(!this._tables.canFind(name))
21         {
22             auto start = 0;
23             auto cursor = 0;
24             while(cursor < name.length)
25             {
26                 if(name[cursor] == '.')
27                 {
28                     auto slice = name[start..cursor];
29                     if(!this._tables.canFind(slice))
30                         this._tables ~= slice;
31                     start = cursor+1;
32                 }
33                 cursor++;
34             }
35 
36             this._tables ~= name;
37         }
38     }
39 
40     private void putDeclaration(string table, string name, string def)
41     {
42         if(table.length)
43         {
44             this._output.put(table);
45             this._output.put('.');
46         }
47         this._output.put(name);
48         this._output.put(" = ");
49         if(table.length)
50         {
51             this._output.put(table);
52             this._output.put('.');
53         }
54         this._output.put(name);
55         this._output.put(" or ");
56         this._output.put(def ? def : "nil");
57         this._output.put('\n');
58     }
59 
60     private void putDescription(string description)
61     {
62         if(description.length)
63         {
64             this._output.put("---");
65             this._output.put(description);
66             this._output.put('\n');
67         }
68     }
69 
70     private void putType(T)()
71     {
72         import std.algorithm : canFind;
73         import std.traits : fullyQualifiedName, isDynamicArray, isSomeFunction;
74 
75         const fqn = fullyQualifiedName!T;
76         if(this._typeNames.canFind(fqn) || is(T == LuaValue) || is(T == LuaNumber))
77             return;
78         this._typeNames ~= fqn;
79 
80         static if(is(T == struct))
81         {
82             Appender!(char[]) suboutput;
83 
84             suboutput.put("---@class ");
85             suboutput.put(getTypeName!T);
86             suboutput.put('\n');
87 
88             static foreach(member; __traits(allMembers, T))
89             {{
90                 alias Member = __traits(getMember, T, member);
91 
92                 static if(__traits(compiles, mixin("T.init."~member~" = T.init."~member))) // Is it public and a variable?
93                 {
94                     alias MemberT = typeof(mixin("T.init."~member));
95                     static if(!isSomeFunction!MemberT) // Handles a weird edge case: T func(T)
96                     {
97                         suboutput.put("---@field public ");
98                         suboutput.put(member);
99                         suboutput.put(' ');
100                         suboutput.put(getTypeName!MemberT);
101                         suboutput.put('\n');
102                     }
103                 }
104             }}
105 
106             suboutput.put("local ");
107             suboutput.put(getTypeName!T);
108             suboutput.put('\n');
109             this._output.put(suboutput.data);
110         }
111     }
112 
113     void addArray(T)(string table, string name, string description = "")
114     {
115         this.addTable(table);
116         this.putType!T();
117         this.putDescription(description);
118         this._output.put("---@type ");
119         this._output.put(getTypeName!T);
120         this._output.put("[]\n");
121         this.putDeclaration(table, name, "{}");
122     }
123 
124     void addTable(Key, Value)(string table, string name, string description = "")
125     {
126         this.addTable(table);
127         this.putType!Key();
128         this.putType!Value();
129         this.putDescription(description);
130         this._output.put("---@type table<");
131         this._output.put(getTypeName!Key);
132         this._output.put(", ");
133         this._output.put(getTypeName!Value);
134         this._output.put(">\n");
135         this.putDeclaration(table, name, "{}");
136     }
137 
138     string toString() const
139     {
140         Appender!(char[]) tables;
141         foreach(t; this._tables)
142         {
143             tables.put(t);
144             tables.put(" = ");
145             tables.put(t);
146             tables.put(" or {}\n");
147         }
148         tables.put(this._output.data);
149         return tables.data.idup;
150     }
151 }
152 ///
153 unittest
154 {
155     static struct S
156     {
157         LuaNumber a;
158         LuaValue b;
159         string c;
160     }
161 
162     EmmyLuaBuilder b;
163     b.addArray!LuaNumber(null, "globalTable", "Some description");
164     b.addArray!LuaValue("t1", "field", "Some field");
165     b.addArray!string("t2.t", "field");
166     b.addTable!(string, LuaValue)(null, "map");
167     b.addArray!S(null, "s");
168     b.addFunction!(
169         (string _, LuaValue __, S ____)
170         {
171             return 200;
172         }
173     )(null, "test", "icles");
174     alias f = LuaTable.makeNew;
175     b.addFunction!f(null, "readText");
176     b.addFunctions!(
177         "writeln", (string[] s) {},
178         "readln", () { return ""; }
179     )("sh");
180 
181     // writeln(b.toString());
182 }
183 
184 template addFunction(alias Func) // Have to do this otherwise I get the fucking crappy-ass "dual-context" error.
185 {
186     void addFunction(ref EmmyLuaBuilder b, string table, string name, string description = "")
187     {
188         import std.array : Appender;
189         import std.traits : ParameterIdentifierTuple, Parameters, ReturnType;
190 
191         b.addTable(table);
192 
193         Appender!(char[]) suboutput;
194 
195         suboutput.put("--@type fun(");
196         alias idents = ParameterIdentifierTuple!Func;
197         static foreach(i, param; Parameters!Func)
198         {{
199             static if(!is(param == LuaState*)) // Wrapped functions can ask for the LuaState. This is not a parameter passed by Lua code.
200             {
201                 b.putType!param();
202 
203                 const ident = idents[i].length ? idents[i] : "_";
204                 suboutput.put(ident);
205                 suboutput.put(':');
206                 suboutput.put(getTypeName!param);
207 
208                 static if(i != Parameters!Func.length - 1)
209                     suboutput.put(", ");
210             }
211         }}
212         b.putType!(ReturnType!Func)();
213         suboutput.put("):");
214         suboutput.put(getTypeName!(ReturnType!Func));
215         suboutput.put('\n');
216 
217         b.putDescription(description);
218         b._output.put(suboutput.data);
219         b.putDeclaration(table, name, "function() assert(false, 'not implemented') end");
220     }
221 }
222 
223 template addFunctions(Funcs...)
224 {
225     void addFunctions(ref EmmyLuaBuilder b, string table)
226     {
227         static foreach(i; 0..Funcs.length/2)
228             addFunction!(Funcs[i*2+1])(b, table, Funcs[i*2]);
229     }
230 }
231 
232 private:
233 
234 string getTypeName(T)()
235 {
236     import std.range : ElementType;
237     import std.traits : isNumeric, isDynamicArray;
238 
239     static if(is(T == LuaValue))
240         return "any";
241     else static if(is(T == LuaNumber) || isNumeric!T)
242         return "number";
243     else static if(is(T == void) || is(T == typeof(null)))
244         return "nil";
245     else static if(isDynamicArray!T && !is(T == string))
246         return getTypeName!(ElementType!T)~"[]";
247     else
248         return T.stringof;
249 }