1 module lumars.table;
2 
3 import bindbc.lua, lumars;
4 import std.traits : isNumeric;
5 
6 enum LuaIterateOption
7 {
8     none = 0,
9     dontDupeStrings = 1 << 0,
10 }
11 
12 template ipairs(alias Func, LuaIterateOption Options = LuaIterateOption.none)
13 {
14     void ipairs(LuaTableT)(LuaTableT table)
15     {
16         import std.format : format;
17 
18         const index = table.push();
19         scope(exit) table.pop();
20 
21         const expectedTop = table.lua.top + 1;
22         foreach(i; 1..int.max)
23         {
24             lua_rawgeti(table.lua.handle, index, i);
25             scope(failure) table.lua.pop(1);
26             if(lua_isnil(table.lua.handle, -1))
27             {
28                 table.lua.pop(1);
29                 break;
30             }
31 
32             LuaValue value;
33             const valueType = table.lua.type(-1);
34             if(valueType == LuaValue.Kind.text)
35             {
36                 static if((Options & LuaIterateOption.dontDupeStrings) > 0)
37                     value = LuaValue(table.lua.get!(const(char)[])(-1));
38                 else
39                     value = LuaValue(table.lua.get!string(-1));
40             }
41             else
42                 value = table.lua.get!LuaValue(-1);
43 
44             Func(i, value);
45             if(table.lua.top != expectedTop)
46             {
47                 table.lua.printStack();
48                 assert(false, 
49                     "Expected stack top to be %s after call to user function, but it is %s."
50                     .format(expectedTop, table.lua.top)
51                 );
52             }
53 
54             table.lua.pop(1);
55         }
56     }
57 }
58 
59 template ipairs(ValueT, alias Func)
60 {
61     void ipairs(LuaTableT)(LuaTableT table)
62     {
63         table.ipairs!((k, _)
64         {
65             Func(k, table.lua.get!ValueT(-1));
66         });
67     }
68 }
69 
70 template pairs(alias Func, LuaIterateOption Options = LuaIterateOption.none)
71 {
72     void pairs(LuaTableT)(LuaTableT table)
73     {
74         auto index = table.push();
75         scope(exit) table.pop();
76 
77         index = index < 0 ? index - 1 : index;
78 
79         table.lua.push(null);
80         while(table.lua.next(index))
81         {
82             scope(failure) table.lua.pop(2);
83 
84             LuaValue key;
85             const keyType = table.lua.type(-2);
86             if(keyType == LuaValue.Kind.text)
87             {
88                 static if((Options & LuaIterateOption.dontDupeStrings) > 0)
89                     key = LuaValue(table.lua.get!(const(char)[])(-2));
90                 else
91                     key = LuaValue(table.lua.get!string(-2));
92             }
93             else
94                 key = table.lua.get!LuaValue(-2);
95 
96             LuaValue value;
97             const valueType = table.lua.type(-1);
98             if(valueType == LuaValue.Kind.text)
99             {
100                 static if((Options & LuaIterateOption.dontDupeStrings) > 0)
101                     value = LuaValue(table.lua.get!(const(char)[])(-1));
102                 else
103                     value = LuaValue(table.lua.get!string(-1));
104             }
105             else
106                 value = table.lua.get!LuaValue(-1);
107 
108             Func(key, value);
109             table.lua.pop(1);
110         }
111     }
112 }
113 
114 template pairs(KeyT, ValueT, alias Func)
115 {
116     void pairs(LuaTableT)(LuaTableT table)
117     {
118         table.pairs!((k, v)
119         {
120             static if(is(KeyT == LuaValue) && is(ValueT == LuaValue))
121                 Func(k, v);
122             else static if(is(KeyT == LuaValue))
123                 Func(k, table.lua.get!ValueT(-1));
124             else static if(is(ValueT == LuaValue))
125                 Func(table.lua.get!KeyT(-2), v);
126             else
127                 Func(table.lua.get!KeyT(-2), table.lua.get!ValueT(-1));
128         });
129     }
130 }
131 
132 private mixin template LuaTableFuncs(bool isPseudo)
133 {
134     T tryGet(T, IndexT)(IndexT index, out bool result)
135     if(isNumeric!IndexT || is(IndexT == string))
136     {
137         static assert(
138             !is(IndexT == LuaTableWeak)
139         &&  !is(IndexT == LuaFuncWeak)
140         &&  !is(IndexT == const(char)[]),
141             "Can't use weak references with `get` as this function does not keep the value alive on the stack."
142         );
143 
144         static if(!isPseudo)
145         {
146             const meIndex = this.push();
147             scope(exit) this.pop();
148             const tableIndex = meIndex < 0 ? meIndex - 1 : meIndex;
149         }
150         else
151             const tableIndex = this._index;
152         
153         this.lua.push(index);
154         lua_gettable(this.lua.handle, tableIndex);
155         result = this.lua.isType!T(-1);
156 
157         if(!result)
158         {
159             this.lua.pop(1);
160             return T.init;
161         }
162         else
163         {
164             auto value = this.lua.get!T(-1);
165             this.lua.pop(1);
166             return value;
167         }
168     }
169     
170     T get(T, IndexT)(IndexT index)
171     if(isNumeric!IndexT || is(IndexT == string))
172     {
173         static assert(
174             !is(IndexT == LuaTableWeak)
175         &&  !is(IndexT == LuaFuncWeak)
176         &&  !is(IndexT == const(char)[]),
177             "Can't use weak references with `get` as this function does not keep the value alive on the stack."
178         );
179 
180         static if(!isPseudo)
181         {
182             const meIndex = this.push();
183             scope(exit) this.pop();
184             const tableIndex = meIndex < 0 ? meIndex - 1 : meIndex;
185         }
186         else
187             const tableIndex = this._index;
188         
189         this.lua.push(index);
190         lua_gettable(this.lua.handle, tableIndex);
191         auto value = this.lua.get!T(-1);
192         this.lua.pop(1);
193         return value;
194     }
195 
196     void set(T, IndexT)(IndexT index, T value)
197     if(isNumeric!IndexT || is(IndexT == string))
198     {
199         static if(!isPseudo)
200         {
201             const meIndex = this.push();
202             scope(exit) this.pop();
203             const tableIndex = meIndex < 0 ? meIndex - 2 : meIndex;
204         }
205         else
206             const tableIndex = this._index;
207         
208         this.lua.push(index);
209         this.lua.push(value);
210         lua_settable(this.lua.handle, tableIndex);
211     }
212 
213     void setMetatable(LuaTable metatable)
214     {
215         static if(!isPseudo)
216         {
217             const meIndex = this.push();
218             scope(exit) this.pop();
219             const tableIndex = meIndex < 0 ? meIndex - 1 : meIndex;
220         }
221         else
222             const tableIndex = this._index;
223 
224         metatable.push();
225         lua_setmetatable(this.lua.handle, tableIndex);
226     }
227 
228     void opIndexAssign(T, IndexT)(T value, IndexT index)
229     {
230         this.set(index, value);
231     }
232 
233     size_t length()
234     {
235         static if(!isPseudo)
236         {
237             const index = this.push();
238             scope(exit) this.pop();
239         }
240         else
241             const index = this._index;
242 
243         return lua_objlen(this.lua.handle, index);
244     }
245 }
246 
247 struct LuaTablePseudo
248 {
249     mixin LuaTableFuncs!true;
250 
251     private
252     {
253         LuaState* _lua;
254         int _index;
255     }
256 
257     @safe @nogc
258     this(LuaState* lua, int index) nothrow
259     {
260         this._index = index;
261         this._lua = lua;
262     }
263 
264     void pushElement(IndexT)(IndexT index)
265     if(isNumeric!IndexT || is(IndexT == string))
266     {
267         this.lua.push(index);
268         lua_gettable(this.lua.handle, this._index);
269     }
270 
271     @property @safe @nogc
272     LuaState* lua() nothrow pure
273     {
274         return this._lua;
275     }
276 }
277 
278 struct LuaTableWeak 
279 {
280     mixin LuaTableFuncs!false;
281 
282     private
283     {
284         LuaState* _lua;
285         int _index;
286     }
287 
288     this(LuaState* lua, int index)
289     {
290         lua.enforceType(LuaValue.Kind.table, index);
291         this._index = index;
292         this._lua = lua;
293     }    
294     
295     void pushElement(IndexT)(IndexT index)
296     if(isNumeric!IndexT || is(IndexT == string))
297     {
298         this.lua.push(index);
299         lua_gettable(this.lua.handle, this._index < 0 ? this._index - 1 : this._index);
300     }
301 
302     @safe @nogc 
303     int push() nothrow pure const
304     {
305         return this._index;
306     }
307 
308     void pop()
309     {
310         this.lua.enforceType(LuaValue.Kind.table, this._index);
311     }
312 
313     @property @safe @nogc
314     LuaState* lua() nothrow pure
315     {
316         return this._lua;
317     }
318 }
319 
320 struct LuaTable 
321 {
322     import std.range : isInputRange;
323     import std.traits : isAssociativeArray;
324     import std.typecons : RefCounted;
325     mixin LuaTableFuncs!false;
326 
327     private
328     {
329         static struct State
330         {
331             LuaState* lua;
332             int ref_;
333             bool isWrapper;
334             
335             ~this()
336             {
337                 if(this.lua && !this.isWrapper)
338                     luaL_unref(this.lua.handle, LUA_REGISTRYINDEX, this.ref_);
339             }
340         }
341         RefCounted!State _state;
342     }
343 
344     void pushElement(IndexT)(IndexT index)
345     if(isNumeric!IndexT || is(IndexT == string))
346     {
347         this.push();
348         scope(exit) this.lua.remove(-2);
349 
350         this.lua.push(index);
351         lua_gettable(this.lua.handle, -2);
352     }
353 
354     static LuaTable makeRef(LuaState* lua)
355     {
356         lua.enforceType(LuaValue.Kind.table, -1);
357         RefCounted!State state;
358         state.lua = lua;
359         state.ref_ = luaL_ref(lua.handle, LUA_REGISTRYINDEX);
360         state.isWrapper = lua._isWrapper;
361 
362         return LuaTable(state);
363     }
364 
365     static LuaTable makeNew(LuaState* lua, int arrayCapacity = 0, int recordCapacity = 0)
366     {
367         lua_createtable(lua.handle, arrayCapacity, recordCapacity);
368         return LuaTable.makeRef(lua);
369     }
370 
371     static LuaTable makeNew(Range)(LuaState* lua, Range range)
372     if(isInputRange!Range)
373     {
374         import std.range : ElementType;
375         alias Element = ElementType!Range;
376 
377         lua_newtable(lua.handle);
378         static if(is(Element == struct) && __traits(hasMember, Element, "key") && __traits(hasMember, Element, "value"))
379         {
380             foreach(kvp; range)
381             {
382                 lua.push(kvp.key);
383                 lua.push(kvp.value);
384                 lua.rawSet(-3);
385             }
386         }
387         else
388         {
389             int i = 1;
390             foreach(v; range)
391             {
392                 lua.push(v);
393                 lua.rawSet(-2, i++);
394             }
395         }
396 
397         return LuaTable.makeRef(lua);
398     }
399 
400     static LuaTable makeNew(AA)(LuaState* lua, AA aa)
401     if(isAssociativeArray!AA)
402     {
403         return makeNew(lua, aa.byKeyValue);
404     }
405 
406     @nogc
407     int push() nothrow
408     {
409         lua_rawgeti(this._state.lua.handle, LUA_REGISTRYINDEX, this._state.ref_);
410         return this._state.lua.top;
411     }
412 
413     void pop()
414     {
415         this._state.lua.enforceType(LuaValue.Kind.table, -1);
416         this._state.lua.pop(1);
417     }
418 
419     LuaState* lua()
420     {
421         return this._state.lua;
422     }
423 }
424 
425 unittest
426 {
427     import std;
428     auto l = LuaState(null);
429 
430     l.push(["Henlo, ", "Warld."]);
431     auto t = l.get!LuaTableWeak(-1);
432     int i = 0;
433     t.ipairs!((k, v)
434     {
435         i++;
436         assert(
437             (k == 1 && v.textValue == "Henlo, ")
438          || (k == 2 && v.textValue == "Warld."),
439          format("%s, %s", k, v)
440         );
441     });
442     assert(t.get!string(1) == "Henlo, ");
443     assert(t.get!string(2) == "Warld.");
444     assert(i == 2);
445     t.ipairs!(string, (k, v)
446     {
447         if(k == 1)
448             assert(v == "Henlo, ");
449     });
450     l.pop(1);
451 }
452 
453 unittest
454 {
455     import std;
456     auto l = LuaState(null);
457 
458     l.push(
459         [
460             "a": "bc",
461             "1": "23"
462         ]
463     );
464     auto t = l.get!LuaTable(-1);
465     int i = 0;
466     t.pairs!((k, v)
467     {
468         i++;
469         assert(
470             (k.textValue == "a" && v.textValue == "bc")
471          || (k.textValue == "1" && v.textValue == "23"),
472          format("%s, %s", k, v)
473         );
474     });
475     assert(t.get!string("a") == "bc");
476     assert(t.get!string("1") == "23");
477     assert(i == 2);
478     l.pop(1);
479 }
480 
481 unittest
482 {
483     auto l = LuaState(null);
484     auto t = LuaTable.makeNew(&l);
485 
486     t["test"] = "icles";
487     t[4] = 20;
488     assert(t.get!string("test") == "icles");
489     assert(t.get!LuaNumber(4) == 20);
490 }
491 
492 unittest
493 {
494     import std;
495     auto l = LuaState(null);
496     auto t = LuaTable.makeNew(&l, iota(1, 11));
497     assert(t.length == 10);
498     t.ipairs!(LuaNumber, (i, v)
499     {
500         assert(i == v);
501     });
502 }
503 
504 unittest
505 {
506     auto l = LuaState(null);
507     auto t = LuaTable.makeNew(&l, ["a": "bc", "1": "23"]);
508 
509     int count;
510     t.pairs!(string, string, (k, v)
511     {
512         if(k == "a")
513             assert(v == "bc");
514         else if(k == "1")
515             assert(v == "23");
516         else
517             assert(false);
518         count++;
519     });
520     assert(count == 2);
521 }
522 
523 unittest
524 {
525     auto l = LuaState(null);
526     auto t = LuaTable.makeNew(&l);
527     t["__call"] = &luaCWrapperSmart!((LuaValue _) => 2);
528     auto t2 = LuaTable.makeNew(&l);
529     t2.setMetatable(t);
530 
531     l.globalTable["test"] = t2;
532     l.doString("assert(test() == 2)");
533 }
534 
535 unittest
536 {
537     auto l = LuaState(null);
538     auto t = LuaTable.makeNew(&l);
539     t["hello"] = () => "world";
540     auto f = t.get!LuaFunc("hello").bind!(string);
541     assert(f() == "world");
542 }