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 }