1 /**
2 This module implements ScriptObject, the base class for builtin Mildew objects.
3 
4 ────────────────────────────────────────────────────────────────────────────────
5 
6 Copyright (C) 2021 pillager86.rf.gd
7 
8 This program is free software: you can redistribute it and/or modify it under 
9 the terms of the GNU General Public License as published by the Free Software 
10 Foundation, either version 3 of the License, or (at your option) any later 
11 version.
12 
13 This program is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
15 PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 
17 You should have received a copy of the GNU General Public License along with 
18 this program.  If not, see <https://www.gnu.org/licenses/>.
19 */
20 module mildew.types.object;
21 
22 /**
23  * General Object class. Similar to JavaScript, this class works as a dictionary but 
24  * the keys must be strings. Native D objects can be stored in any ScriptObject or derived 
25  * class by assigning it to its nativeObject field. This is also the base class for
26  * arrays, strings, and functions so that those script values can have dictionary entries
27  * assigned to them as well.
28  */
29 class ScriptObject
30 {
31     import mildew.types.any: ScriptAny;
32     import mildew.types.func: ScriptFunction;
33 public:
34     /**
35      * Constructs a new ScriptObject that can be stored inside ScriptValue.
36      * Params:
37      *  typename = This does not have to be set to a meaningful value but constructors (calling script functions
38      *             with the new keyword) set this value to the name of the function.
39      *  proto = The object's __proto__ property. If a value is not found inside the current object's table, a chain
40      *          of prototypes is searched until reaching a null prototype. If this parameter is null, the value is
41      *          set to Object.prototype
42      *  native = A ScriptObject can contain a native D object that can be accessed later. This is used for binding
43      *           D classes.
44      */
45     this(in string typename, ScriptObject proto, Object native = null)
46     {
47         import mildew.types.bindings: getObjectPrototype;
48         _name = typename;
49         if(proto !is null)
50             _prototype = proto;
51         else
52             _prototype = getObjectPrototype;
53         _nativeObject = native;
54     }
55 
56     /**
57      * Empty constructor that leaves prototype, and nativeObject as null.
58      */
59     this(in string typename)
60     {
61         _name = typename;
62     }
63 
64     /// name property
65     string name() const { return _name; }
66 
67 	/// getters property
68 	auto getters() { return _getters; }
69 	/// setters property
70 	auto setters() { return _setters; }
71 
72     /// prototype property
73     auto prototype() { return _prototype; }
74 
75     /// prototype property (setter)
76     auto prototype(ScriptObject proto) { return _prototype = proto; }
77 
78     /// This property provides direct access to the dictionary
79     auto dictionary() { return _dictionary; }
80 
81     /**
82      * Add a getter. Getters should be added to a constructor function's "prototype" field
83      */
84     void addGetterProperty(in string propName, ScriptFunction getter)
85     {
86         _getters[propName] = getter;
87     }
88 
89     /**
90      * Add a setter. Setters should be added to a constructor function's "prototype" field
91      */
92     void addSetterProperty(in string propName, ScriptFunction setter)
93     {
94         _setters[propName] = setter;
95     }
96 
97     /**
98      * Looks up a field through the prototype chain. Note that this does not call any getters because
99      * it is not possible to pass an Environment to opIndex.
100      */
101     ScriptAny lookupField(in string name)
102     {
103         if(name == "__proto__")
104             return ScriptAny(_prototype);
105         if(name == "__super__")
106         {
107             //the super non-constructor expression should translate to "this.__proto__.constructor.__proto__.prototype"
108             if(_prototype && _prototype["constructor"])
109             {
110                 // .__proto__.constructor
111                 auto protoCtor = _prototype["constructor"].toValue!ScriptObject;
112                 if(protoCtor && protoCtor._prototype)
113                 {
114                     return protoCtor._prototype["prototype"];
115                 }
116             }
117         }
118         if(name in _dictionary)
119             return _dictionary[name];
120         if(_prototype !is null)
121             return _prototype.lookupField(name);
122         return ScriptAny.UNDEFINED;
123     }
124 
125     /**
126      * Shorthand for lookupField.
127      */
128     ScriptAny opIndex(in string index)
129     {
130         return lookupField(index);
131     }
132 
133     /**
134      * Comparison operator
135      */
136     int opCmp(const ScriptObject other) const
137     {
138         if(other is null)
139             return 1;
140         
141         if(_dictionary == other._dictionary)
142             return 0;
143         
144         if(_dictionary.keys < other._dictionary.keys)
145             return -1;
146         else if(_dictionary.keys > other._dictionary.keys)
147             return 1;
148         else if(_dictionary.values < other._dictionary.values)
149             return -1;
150         else if(_dictionary.values > other._dictionary.values)
151             return 1;
152         else
153         {
154             if(_prototype is null && other._prototype is null)
155                 return 0;
156             else if(_prototype is null && other._prototype !is null)
157                 return -1;
158             else if(_prototype !is null && other._prototype is null)
159                 return 1;
160             else
161                 return _prototype.opCmp(other._prototype);
162         }
163     }
164 
165     /**
166      * opEquals
167      */
168     bool opEquals(const ScriptObject other) const
169     {
170         // TODO rework this to account for __proto__
171         return opCmp(other) == 0;
172     }
173 
174     /// toHash
175     override size_t toHash() const @safe nothrow
176     {
177         return typeid(_dictionary).getHash(&_dictionary);
178     }
179 
180     /**
181      * Assigns a field to the current object. This does not call any setters.
182      */
183     ScriptAny assignField(in string name, ScriptAny value)
184     {
185         if(name == "__proto__")
186         {
187             _prototype = value.toValue!ScriptObject;
188         }
189         else if(name == "__super__")
190         {
191             return value; // this can't be assigned directly
192         }
193         else
194         {
195             _dictionary[name] = value;
196         }
197         return value;
198     }
199 
200     /**
201      * Determines if there is a getter for a given property
202      */
203     bool hasGetter(in string propName)
204     {
205         auto objectToSearch = this;
206         while(objectToSearch !is null)
207         {
208             if(propName in objectToSearch._getters)
209                 return true;
210             objectToSearch = objectToSearch._prototype;
211         }
212         return false;
213     }
214 
215     /**
216      * Determines if there is a setter for a given property
217      */
218     bool hasSetter(in string propName)
219     {
220         auto objectToSearch = this;
221         while(objectToSearch !is null)
222         {
223             if(propName in objectToSearch._setters)
224                 return true;
225             objectToSearch = objectToSearch._prototype;
226         }
227         return false;
228     }
229 
230     /**
231      * Find a getter in the prototype chain
232      */
233     ScriptFunction findGetter(in string propName)
234     {
235         auto objectToSearch = this;
236         while(objectToSearch !is null)
237         {
238             if(propName in objectToSearch._getters)
239                 return objectToSearch._getters[propName];
240             objectToSearch = objectToSearch._prototype;
241         }
242         return null;
243     }
244 
245     /**
246      * Find a setter in the prototype chain
247      */
248      ScriptFunction findSetter(in string propName)
249      {
250         auto objectToSearch = this;
251         while(objectToSearch !is null)
252         {
253             if(propName in objectToSearch._setters)
254                 return objectToSearch._setters[propName];
255             objectToSearch = objectToSearch._prototype;
256         }
257         return null;
258      }
259 
260     /**
261      * Shorthand for assignField
262      */
263     ScriptAny opIndexAssign(T)(T value, in string index)
264     {
265         static if(is(T==ScriptAny))
266             return assignField(index, value);
267         else
268         {
269             ScriptAny any = value;
270             return assignField(index, any);
271         }
272     }
273 
274     /**
275      * Returns a property descriptor without searching the prototype chain. The object returned is
276      * an object possibly containing get, set, or value fields.
277      * Returns:
278      *  A ScriptObject whose dictionary contains possible "get", "set", and "value" fields.
279      */
280     ScriptObject getOwnPropertyOrFieldDescriptor(in string propName)
281     {
282         ScriptObject property = new ScriptObject("property", null);
283         // find the getter
284         auto objectToSearch = this;
285         if(propName in objectToSearch._getters)
286             property["get"] = objectToSearch._getters[propName];
287         if(propName in objectToSearch._setters)
288             property["set"] = objectToSearch._setters[propName];
289         if(propName in objectToSearch._dictionary)
290             property["value"] = _dictionary[propName];
291         objectToSearch = objectToSearch._prototype;
292         return property;
293     }
294 
295     /**
296      * Get all fields and properties for this object without searching the prototype chain.
297      * Returns:
298      *  A ScriptObject whose dictionary entry keys are names of properties and fields, and the value
299      *  of which is a ScriptObject containing possible "get", "set", and "value" fields.
300      */
301     ScriptObject getOwnFieldOrPropertyDescriptors()
302     {
303         auto property = new ScriptObject("descriptors", null);
304         foreach(k,v ; _dictionary)
305         {
306             auto descriptor = new ScriptObject("descriptor", null);
307             descriptor["value"] = v;
308             property[k] = descriptor;
309         }
310         foreach(k,v ; _getters)
311         {
312             auto descriptor = new ScriptObject("descriptor", null);
313             descriptor["get"] = v;
314             property[k] = descriptor;
315         }
316         foreach(k, v ; _setters)
317         {
318             auto descriptor = property[k].toValue!ScriptObject;
319             if(descriptor is null)
320                 descriptor = new ScriptObject("descriptor", null);
321             descriptor["set"] = v;
322             property[k] = descriptor;
323         }
324         return property;
325     }
326 
327     /**
328      * Tests whether or not a property or field exists in this object without searching the
329      * __proto__ chain.
330      */
331     bool hasOwnFieldOrProperty(in string propOrFieldName)
332     {
333         if(propOrFieldName in _dictionary)
334             return true;
335         if(propOrFieldName in _getters)
336             return true;
337         if(propOrFieldName in _setters)
338             return true;
339         return false;
340     }
341 
342     /**
343      * If a native object was stored inside this ScriptObject, it can be retrieved with this function.
344      * Note that one must always check that the return value isn't null because all functions can be
345      * called with invalid "this" objects using Function.prototype.call.
346      */
347     T nativeObject(T)() const
348     {
349         static if(is(T == class) || is(T == interface))
350             return cast(T)_nativeObject;
351         else
352             static assert(false, "This method can only be used with D classes and interfaces");
353     }
354 
355     /**
356      * Native object can also be written because this is how binding works. Constructors
357      * receive a premade ScriptObject as the "this" with the name and prototype already set.
358      * Native D constructor functions have to set this property.
359      */
360     T nativeObject(T)(T obj)
361     {
362         static if(is(T == class) || is(T == interface))
363             return cast(T)(_nativeObject = obj);
364         else
365             static assert(false, "This method can only be used with D classes and interfaces");
366     }
367 
368     /**
369      * Returns a string with JSON like formatting representing the object's key-value pairs as well as
370      * any nested objects. In the future this will be replaced and an explicit function call will be
371      * required to print this detailed information.
372      */
373     override string toString() const
374     {
375         if(nativeObject!Object !is null)
376             return nativeObject!Object.toString();
377         return _name ~ " " ~ formattedString();
378     }
379 protected:
380 
381     /// The dictionary of key-value pairs
382     ScriptAny[string] _dictionary;
383 
384     /// The lookup table for getters
385     ScriptFunction[string] _getters;
386 
387     /// The lookup table for setters
388     ScriptFunction[string] _setters;
389 
390 private:
391 
392     // TODO complete rewrite
393     string formattedString(int indent = 0) const
394     {
395         immutable indentation = "    ";
396         auto result = "{";
397         size_t counter = 0;
398         immutable keyLength = _dictionary.keys.length;
399         foreach(k, v ; _dictionary)
400         {
401             for(int i = 0; i < indent; ++i)
402                 result ~= indentation;
403             result ~= k ~ ": ";
404             if(v.type == ScriptAny.Type.OBJECT)
405             {
406                 if(!v.isNull)
407                     result ~= v.toValue!ScriptObject().formattedString(indent+1);
408                 else
409                     result ~= "<null object>";
410             }
411             else if(v.type == ScriptAny.Type.STRING)
412             {
413                 result ~= "\"" ~ v.toString() ~ "\"";
414             }
415             else
416                 result ~= v.toString();
417             if(counter < keyLength - 1)
418                 result ~= ", ";
419             ++counter;
420         }
421         // for(int i = 0; i < indent; ++i)
422         //    result ~= indentation;
423         result ~= "}";
424         return result;
425     }
426 
427     /// type name (Function or whatever)
428     string _name;
429     /// it can also hold a native object
430     Object _nativeObject;
431     /// prototype 
432     ScriptObject _prototype = null;
433 }