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 }