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         return opCmp(other) == 0;
171     }
172 
173     /// toHash
174     override size_t toHash() const @safe nothrow
175     {
176         return typeid(_dictionary).getHash(&_dictionary);
177     }
178 
179     /**
180      * Assigns a field to the current object. This does not call any setters.
181      */
182     ScriptAny assignField(in string name, ScriptAny value)
183     {
184         if(name == "__proto__")
185         {
186             _prototype = value.toValue!ScriptObject;
187         }
188         else if(name == "__super__")
189         {
190             return value; // this can't be assigned directly
191         }
192         else
193         {
194             _dictionary[name] = value;
195         }
196         return value;
197     }
198 
199     /**
200      * Determines if there is a getter for a given property
201      */
202     bool hasGetter(in string propName)
203     {
204         auto objectToSearch = this;
205         while(objectToSearch !is null)
206         {
207             if(propName in objectToSearch._getters)
208                 return true;
209             objectToSearch = objectToSearch._prototype;
210         }
211         return false;
212     }
213 
214     /**
215      * Determines if there is a setter for a given property
216      */
217     bool hasSetter(in string propName)
218     {
219         auto objectToSearch = this;
220         while(objectToSearch !is null)
221         {
222             if(propName in objectToSearch._setters)
223                 return true;
224             objectToSearch = objectToSearch._prototype;
225         }
226         return false;
227     }
228 
229     /**
230      * Find a getter in the prototype chain
231      */
232     ScriptFunction findGetter(in string propName)
233     {
234         auto objectToSearch = this;
235         while(objectToSearch !is null)
236         {
237             if(propName in objectToSearch._getters)
238                 return objectToSearch._getters[propName];
239             objectToSearch = objectToSearch._prototype;
240         }
241         return null;
242     }
243 
244     /**
245      * Find a setter in the prototype chain
246      */
247      ScriptFunction findSetter(in string propName)
248      {
249         auto objectToSearch = this;
250         while(objectToSearch !is null)
251         {
252             if(propName in objectToSearch._setters)
253                 return objectToSearch._setters[propName];
254             objectToSearch = objectToSearch._prototype;
255         }
256         return null;
257      }
258 
259     /**
260      * Shorthand for assignField
261      */
262     ScriptAny opIndexAssign(T)(T value, in string index)
263     {
264         static if(is(T==ScriptAny))
265             return assignField(index, value);
266         else
267         {
268             ScriptAny any = value;
269             return assignField(index, any);
270         }
271     }
272 
273     /**
274      * Returns a property descriptor without searching the prototype chain. The object returned is
275      * an object possibly containing get, set, or value fields.
276      */
277     ScriptObject getOwnPropertyOrFieldDescriptor(in string propName)
278     {
279         ScriptObject property = new ScriptObject("property", null);
280         // find the getter
281         auto objectToSearch = this;
282         if(propName in objectToSearch._getters)
283             property["get"] = objectToSearch._getters[propName];
284         if(propName in objectToSearch._setters)
285             property["set"] = objectToSearch._setters[propName];
286         if(propName in objectToSearch._dictionary)
287             property["value"] = _dictionary[propName];
288         objectToSearch = objectToSearch._prototype;
289         return property;
290     }
291 
292     ScriptObject getOwnFieldOrPropertyDescriptors()
293     {
294         auto property = new ScriptObject("descriptors", null);
295         foreach(k,v ; _dictionary)
296         {
297             auto descriptor = new ScriptObject("descriptor", null);
298             descriptor["value"] = v;
299             property[k] = descriptor;
300         }
301         foreach(k,v ; _getters)
302         {
303             auto descriptor = new ScriptObject("descriptor", null);
304             descriptor["get"] = v;
305             property[k] = descriptor;
306         }
307         foreach(k, v ; _setters)
308         {
309             auto descriptor = property[k].toValue!ScriptObject;
310             if(descriptor is null)
311                 descriptor = new ScriptObject("descriptor", null);
312             descriptor["set"] = v;
313             property[k] = descriptor;
314         }
315         return property;
316     }
317 
318     /**
319      * Tests whether or not a property or field exists in this object without searching the
320      * __proto__ chain.
321      */
322     bool hasOwnFieldOrProperty(in string propOrFieldName)
323     {
324         if(propOrFieldName in _dictionary)
325             return true;
326         if(propOrFieldName in _getters)
327             return true;
328         if(propOrFieldName in _setters)
329             return true;
330         return false;
331     }
332 
333     /**
334      * If a native object was stored inside this ScriptObject, it can be retrieved with this function.
335      * Note that one must always check that the return value isn't null because all functions can be
336      * called with invalid "this" objects using functionName.call.
337      */
338     T nativeObject(T)() const
339     {
340         static if(is(T == class) || is(T == interface))
341             return cast(T)_nativeObject;
342         else
343             static assert(false, "This method can only be used with D classes and interfaces");
344     }
345 
346     /**
347      * Native object can also be written because this is how binding works. Constructors
348      * receive a premade ScriptObject as the "this" with the name and prototype already set.
349      * Native D constructor functions have to set this property.
350      */
351     T nativeObject(T)(T obj)
352     {
353         static if(is(T == class) || is(T == interface))
354             return cast(T)(_nativeObject = obj);
355         else
356             static assert(false, "This method can only be used with D classes and interfaces");
357     }
358 
359     /**
360      * Returns a string with JSON like formatting representing the object's key-value pairs as well as
361      * any nested objects. In the future this will be replaced and an explicit function call will be
362      * required to print this detailed information.
363      */
364     override string toString() const
365     {
366         if(nativeObject!Object !is null)
367             return nativeObject!Object.toString();
368         return _name ~ " " ~ formattedString();
369     }
370 protected:
371 
372     /// The dictionary of key-value pairs
373     ScriptAny[string] _dictionary;
374 
375     /// The lookup table for getters
376     ScriptFunction[string] _getters;
377 
378     /// The lookup table for setters
379     ScriptFunction[string] _setters;
380 
381 private:
382 
383     // TODO complete rewrite
384     string formattedString(int indent = 0) const
385     {
386         immutable indentation = "    ";
387         auto result = "{";
388         size_t counter = 0;
389         immutable keyLength = _dictionary.keys.length;
390         foreach(k, v ; _dictionary)
391         {
392             for(int i = 0; i < indent; ++i)
393                 result ~= indentation;
394             result ~= k ~ ": ";
395             if(v.type == ScriptAny.Type.OBJECT)
396             {
397                 if(!v.isNull)
398                     result ~= v.toValue!ScriptObject().formattedString(indent+1);
399                 else
400                     result ~= "<null object>";
401             }
402             else if(v.type == ScriptAny.Type.STRING)
403             {
404                 result ~= "\"" ~ v.toString() ~ "\"";
405             }
406             else
407                 result ~= v.toString();
408             if(counter < keyLength - 1)
409                 result ~= ", ";
410             ++counter;
411         }
412         // for(int i = 0; i < indent; ++i)
413         //    result ~= indentation;
414         result ~= "}";
415         return result;
416     }
417 
418     /// type name (Function or whatever)
419     string _name;
420     /// it can also hold a native object
421     Object _nativeObject;
422     /// prototype 
423     ScriptObject _prototype = null;
424 }