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