1 /** 2 This module implements the ScriptAny struct, which can hold any value type usable in the scripting language. 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.any; 21 22 import std.conv: to; 23 import std.traits; 24 25 /** 26 * This variant holds primitive values as well as ScriptObject derived complex reference types. 27 */ 28 struct ScriptAny 29 { 30 import mildew.types.object: ScriptObject; 31 import mildew.types.func: ScriptFunction; 32 import mildew.vm.program: Program; 33 import mildew.vm.consttable: ConstTable; 34 35 public: 36 /** 37 * Enumeration of what type is held by a ScriptAny. Note that a function, array, or string can be used 38 * as an object, so the best way to check if a ScriptAny is a ScriptObject is to use isObject. 39 */ 40 enum Type 41 { 42 /// primitives 43 NULL=0, UNDEFINED, BOOLEAN, INTEGER, DOUBLE, 44 /// objects 45 OBJECT, ARRAY, FUNCTION, STRING 46 } 47 48 /** 49 * Constructs a new ScriptAny based on the value given. 50 * Params: 51 * value = This can be any valid type such as null, bool, signed or unsigned ints or longs, floats, 52 * or doubles, strings, even primitive D arrays, as well as ScriptObject or ScriptFunction 53 */ 54 this(T)(T value) 55 { 56 setValue!T(value); 57 } 58 59 /** 60 * opCast will now use toValue 61 */ 62 T opCast(T)() const 63 { 64 return toValue!T(); 65 } 66 67 /** 68 * Assigns a value. 69 */ 70 auto opAssign(T)(T value) 71 { 72 setValue(value); 73 return this; 74 } 75 76 /** 77 * Implements binary math operations between two ScriptAnys and returns a ScriptAny. For 78 * certain operations that make no sense the result will be NaN or UNDEFINED. Addition 79 * involving any number of ScriptStrings will always coerce the values to string and 80 * perform a concatenation, as per Mildew language semantics. Bitwise operations always 81 * coerce the result to long (Type.INTEGER). 82 */ 83 auto opBinary(string op)(auto ref const ScriptAny rhs) const 84 { 85 static if(op == "+") 86 { 87 // if either is string convert both to string and concatenate 88 if(_type == Type.STRING || rhs._type == Type.STRING) 89 { 90 return ScriptAny(toString() ~ rhs.toString()); 91 } 92 else if(_type == Type.UNDEFINED || rhs._type == Type.UNDEFINED) 93 { 94 return ScriptAny.UNDEFINED; 95 } 96 // if they are both numerical 97 else if(this.isNumber && rhs.isNumber) 98 { 99 // if either is floating point convert both to floating point and add 100 if(_type == Type.DOUBLE || rhs._type == Type.DOUBLE) 101 return ScriptAny(toValue!double + rhs.toValue!double); 102 // else an integer addition is fine 103 else 104 return ScriptAny(toValue!long + rhs.toValue!long); 105 } 106 // else this makes no sense so concatenate strings 107 else return ScriptAny(toString() ~ rhs.toString()); 108 } 109 else static if(op == "-" || op == "*" || op == "%") // values can stay int here as well 110 { 111 // only makes sense for numbers 112 if(!(this.isNumber && rhs.isNumber)) 113 return ScriptAny(double.nan); 114 // if either is floating point convert both to float 115 if(_type == Type.DOUBLE || rhs._type == Type.DOUBLE) 116 mixin("return ScriptAny(toValue!double" ~ op ~ "rhs.toValue!double);"); 117 else // int is fine 118 mixin("return ScriptAny(toValue!long" ~ op ~ "rhs.toValue!long);"); 119 } 120 else static if(op == "/" || op == "^^") // must be doubles 121 { 122 // both must be casted to double 123 if(!(this.isNumber && rhs.isNumber)) 124 return ScriptAny(double.nan); 125 mixin("return ScriptAny(toValue!double" ~ op ~ "rhs.toValue!double);"); 126 } 127 // bitwise operations even coerce non-numbers to 0, as in JavaScript 128 else static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") 129 { 130 if(!(this.isNumber && rhs.isNumber)) 131 return ScriptAny(0); 132 mixin("return ScriptAny(toValue!long" ~ op ~ "rhs.toValue!long);"); 133 } 134 else 135 static assert(false, "The binary operation " ~ op ~ " is not supported for this type"); 136 } 137 138 /** 139 * Depending on the type of index, if it is a string it accesses a field of the object, otherwise 140 * if it is numerical, attempts to access an index of an array object. 141 */ 142 ScriptAny lookupField(T)(T index, out bool success) 143 { 144 import mildew.types..string: ScriptString; 145 import mildew.types.array: ScriptArray; 146 147 success = false; 148 149 // number index only makes sense for strings and arrays 150 static if(isIntegral!T) 151 { 152 if(!(_type == Type.ARRAY || _type == Type.STRING)) 153 { 154 return UNDEFINED; 155 } 156 else if(_type == Type.ARRAY) 157 { 158 auto arr = cast(ScriptArray)_asObject; 159 auto found = arr[index]; 160 if(found == null) 161 return UNDEFINED; 162 success = true; 163 return *found; 164 } 165 else if(_type == Type.STRING) 166 { 167 auto str = (cast(ScriptString)_asObject).toString(); 168 if(index < 0 || index >= str.length) 169 return UNDEFINED; 170 success = true; 171 return ScriptAny([str[index]]); 172 } 173 return ScriptAny.UNDEFINED; 174 } 175 // else it is a string so we are accessing an object property 176 else static if(isSomeString!T) 177 { 178 if(!isObject) 179 return UNDEFINED; 180 success = true; 181 return _asObject.lookupField(index.to!string); 182 } 183 else 184 static assert(false, "Invalid index type"); 185 } 186 187 /** 188 * Overload to ignore the bool 189 */ 190 ScriptAny lookupField(T)(T index) 191 { 192 bool ignore; // @suppress(dscanner.suspicious.unmodified) 193 return lookupField(index, ignore); 194 } 195 196 /** 197 * Attempt to assign a field of a complex object. This can be used to assign array indexes if 198 * the index is a number. 199 */ 200 ScriptAny assignField(T)(T index, ScriptAny value, out bool success) 201 { 202 import mildew.types..string: ScriptString; 203 import mildew.types.array: ScriptArray; 204 205 success = false; 206 207 // number index only makes sense for arrays 208 static if(isIntegral!T) 209 { 210 if(_type != Type.ARRAY) 211 { 212 return UNDEFINED; 213 } 214 else 215 { 216 auto arr = cast(ScriptArray)_asObject; 217 auto found = (arr[index] = value); 218 if(found == null) 219 return UNDEFINED; 220 success = true; 221 return *found; 222 } 223 } 224 // else it is a string so we are accessing an object property 225 else static if(isSomeString!T) 226 { 227 if(!isObject) 228 return UNDEFINED; 229 success = true; 230 return _asObject.assignField(index.to!string, value); 231 } 232 else 233 static assert(false, "Invalid index type"); 234 } 235 236 /** 237 * Overload to ignore the bool 238 */ 239 ScriptAny assignField(T)(T index, ScriptAny value) 240 { 241 bool ignore; // @suppress(dscanner.suspicious.unmodified) 242 return assignField(index, ignore); 243 } 244 245 /** 246 * Add a get method to an object. Throws exception if this is not a ScriptObject 247 */ 248 void addGetterProperty(in string name, ScriptFunction func) 249 { 250 if(!isObject) 251 throw new ScriptAnyException("Cannot add getter " ~ name ~ " to non-object", this); 252 _asObject.addGetterProperty(name, func); 253 } 254 255 /** 256 * Add a set method to an object. Throws exception if this is not a ScriptObject 257 */ 258 void addSetterProperty(in string name, ScriptFunction func) 259 { 260 if(!isObject) 261 throw new ScriptAnyException("Cannot add setter " ~ name ~ " to non-object", this); 262 _asObject.addSetterProperty(name, func); 263 } 264 265 /** 266 * Defines unary math operations for a ScriptAny. 267 */ 268 auto opUnary(string op)() 269 { 270 // plus and minus can work on doubles or longs 271 static if(op == "-") 272 { 273 if(!isNumber) 274 return ScriptAny(-double.nan); 275 276 if(_type == Type.DOUBLE) 277 mixin("return ScriptAny(" ~ op ~ " toValue!double);"); 278 else 279 mixin("return ScriptAny(" ~ op ~ " toValue!long);"); 280 } 281 else static if(op == "+") 282 { 283 return this; // no effect 284 } 285 // bit not 286 else static if(op == "~") 287 { 288 if(!isNumber) 289 return ScriptAny(0); 290 return ScriptAny(~toValue!long); 291 } 292 else // increment and decrement have to be handled by the scripting environment 293 static assert(false, "Unary operator " ~ op ~ " is not implemented for this type"); 294 } 295 296 /** 297 * Tests for equality by casting similar types to the same type and comparing values. If the types 298 * are too different to do this, the result is false. 299 */ 300 bool opEquals(const ScriptAny other) const 301 { 302 import mildew.types.array : ScriptArray; 303 304 // if both are undefined then return true 305 if(_type == Type.UNDEFINED && other._type == Type.UNDEFINED) 306 return true; 307 // but if both are either null or undefined return true 308 else if((_type == Type.UNDEFINED || _type == Type.NULL) 309 && (other._type == Type.UNDEFINED || other._type == Type.NULL)) 310 return true; 311 // but if only one is undefined return false 312 else if(_type == Type.UNDEFINED || other._type == Type.UNDEFINED) 313 return false; 314 315 // if both are null return true else false if only one is null 316 if(_type == Type.NULL || other._type == Type.NULL) 317 return false; 318 319 // if either are strings, convert to string and compare 320 if(_type == Type.STRING || other._type == Type.STRING) 321 return toString() == other.toString(); 322 323 // if either is numeric 324 if(this.isNumber() && other.isNumber()) 325 { 326 // if one is double convert both to double and compare 327 if(_type == Type.DOUBLE || other._type == Type.DOUBLE) 328 return toValue!double == other.toValue!double; 329 // otherwise return the integer comparison 330 return toValue!long == other.toValue!long; 331 } 332 333 // if both are arrays do an array comparison which should recursively call opEquals on each item 334 if(_type == Type.ARRAY && other._type == Type.ARRAY) 335 { 336 auto arr1 = cast(ScriptArray)_asObject; 337 auto arr2 = cast(ScriptArray)(other._asObject); 338 return arr1.array == arr2.array; 339 } 340 341 if(_type == Type.FUNCTION && other._type == Type.FUNCTION) 342 { 343 return (cast(ScriptFunction)_asObject).opEquals(cast(ScriptFunction)other._asObject); 344 } 345 346 // different types should return false by now 347 if(_type != other._type) 348 return false; 349 350 // else compare the objects for now 351 return _asObject.opEquals(other._asObject); 352 } 353 354 /** 355 * The comparison operations. 356 */ 357 int opCmp(const ScriptAny other) const 358 { 359 import mildew.types.array: ScriptArray; 360 361 // if either are strings, convert and compare 362 if(_type == Type.STRING || other._type == Type.STRING) 363 { 364 immutable str1 = toString(); 365 immutable str2 = other.toString(); 366 if(str1 < str2) 367 return -1; 368 else if(str1 > str2) 369 return 1; 370 else 371 return 0; 372 } 373 374 // if both are numeric convert to double or long as needed and compare 375 if(this.isNumber && other.isNumber) 376 { 377 if(_type == Type.DOUBLE || other._type == Type.DOUBLE) 378 { 379 immutable num1 = toValue!double, num2 = other.toValue!double; 380 if(num1 < num2) 381 return -1; 382 else if(num1 > num2) 383 return 1; 384 else 385 return 0; 386 } 387 else 388 { 389 immutable num1 = toValue!long, num2 = other.toValue!long; 390 if(num1 < num2) 391 return -1; 392 else if(num1 > num2) 393 return 1; 394 else 395 return 0; 396 } 397 } 398 399 // if both are arrays they can be compared 400 if(_type == Type.ARRAY && other._type == Type.ARRAY) 401 { 402 auto arr1 = cast(ScriptArray)_asObject; 403 auto arr2 = cast(ScriptArray)(other._asObject); 404 if(arr1 < arr2) 405 return -1; 406 else if(arr1 > arr2) 407 return 1; 408 else 409 return 0; 410 } 411 412 // if both are functions they can be compared 413 if(_type == Type.FUNCTION && other._type == Type.FUNCTION) 414 { 415 return (cast(ScriptFunction)_asObject).opCmp(cast(ScriptFunction)other._asObject); 416 } 417 418 // if different types by this point return the enum difference 419 if(_type != other._type) 420 return cast(int)_type - cast(int)other._type; 421 422 if(isObject) 423 { 424 return _asObject.opCmp(other._asObject); 425 } 426 427 return -1; 428 } 429 430 /** 431 * This allows ScriptAny to be used as a key index in a table. The Map class uses this feature. 432 */ 433 size_t toHash() const @trusted nothrow 434 { 435 import mildew.types.array: ScriptArray; 436 try 437 { 438 final switch(_type) 439 { 440 case Type.UNDEFINED: 441 return -1; // not sure what else to do 442 case Type.NULL: 443 return 0; // i don't know what to do for those 444 case Type.BOOLEAN: 445 return typeid(_asBoolean).getHash(&_asBoolean); 446 case Type.INTEGER: 447 return typeid(_asInteger).getHash(&_asInteger); 448 case Type.DOUBLE: 449 return typeid(_asDouble).getHash(&_asDouble); 450 case Type.STRING: 451 { 452 auto str = _asObject.toString(); 453 return typeid(str).getHash(&str); 454 } 455 case Type.ARRAY: 456 { 457 auto arr = (cast(ScriptArray)_asObject).array; 458 return typeid(arr).getHash(&arr); 459 } 460 case Type.FUNCTION: 461 return (cast(ScriptFunction)_asObject).toHash(); 462 case Type.OBJECT: 463 return _asObject.toHash(); 464 } 465 } 466 catch(Exception ex) 467 { 468 return 0; 469 } 470 } 471 472 /** 473 * This implements the '===' and '!==' operators. Objects must be exactly the same in type and value. 474 * This operator should not be used on numerical primitives because true === 1 will return false. 475 * Same with 1.0 === 1. 476 */ 477 bool strictEquals(const ScriptAny other) 478 { 479 import mildew.types.array: ScriptArray; 480 if(_type != other._type) 481 return false; 482 483 final switch(_type) 484 { 485 case Type.UNDEFINED: 486 case Type.NULL: 487 return true; 488 case Type.BOOLEAN: 489 return _asBoolean == other._asBoolean; 490 case Type.INTEGER: 491 return _asInteger == other._asInteger; 492 case Type.DOUBLE: 493 return _asDouble == other._asDouble; 494 case Type.STRING: 495 return toString() == other.toString(); 496 case Type.ARRAY: 497 return (cast(ScriptArray)_asObject) == (cast(ScriptArray)other._asObject); 498 case Type.FUNCTION: 499 return (cast(ScriptFunction)_asObject) == (cast(ScriptFunction)other._asObject); 500 case Type.OBJECT: 501 return _asObject == other._asObject; 502 } 503 } 504 505 /** 506 * Returns the read-only type property. This should always be checked before using the 507 * toValue or checkValue template to retrieve the stored D value. Unless one is casting 508 * to primitives and does not care about the value being 0 or false. 509 */ 510 auto type() const nothrow @nogc { return _type; } 511 512 /** 513 * Returns true if the type is UNDEFINED. 514 */ 515 auto isUndefined() const nothrow @nogc { return _type == Type.UNDEFINED; } 516 517 /** 518 * Returns true if the type is NULL or if it an object or function whose stored value is null. Note 519 * that the second condition should be impossible as receiving a null object value sets the type 520 * to NULL anyway. 521 */ 522 auto isNull() const nothrow @nogc 523 { 524 if(_type == Type.NULL) 525 return true; 526 if(isObject) 527 return _asObject is null; 528 return false; 529 } 530 531 /** 532 * Returns true if the value stored is a numerical type or anything that can be converted into a 533 * valid number such as boolean, or even null, which gets converted to 0. 534 */ 535 auto isNumber() const nothrow @nogc 536 { 537 return _type == Type.NULL || _type == Type.BOOLEAN || _type == Type.INTEGER || _type == Type.DOUBLE; 538 } 539 540 /** 541 * Returns true if the value stored is a valid integer, but not a floating point number. 542 */ 543 auto isInteger() const nothrow @nogc 544 { 545 return _type == Type.NULL || _type == Type.BOOLEAN || _type == Type.INTEGER; 546 } 547 548 /** 549 * This should always be used instead of checking type==OBJECT because ScriptFunction, ScriptArray, 550 * and ScriptString are valid subclasses of ScriptObject. 551 */ 552 auto isObject() const nothrow @nogc 553 { 554 return _type == Type.ARRAY || _type == Type.STRING || _type == Type.OBJECT || _type == Type.FUNCTION; 555 } 556 557 /** 558 * Converts a stored value back into a D value if it is valid, otherwise throws a ScriptAnyException. 559 * This is not ideal to use in native bindings because such an exception cannot be caught by the script runtime. 560 */ 561 T checkValue(T)() const 562 { 563 return convertValue!T(true); 564 } 565 566 /** 567 * Similar to checkValue except if the type is invalid and doesn't match the template type, a sane 568 * default value such as 0 or null is returned instead of throwing an exception. 569 */ 570 T toValue(T)() const 571 { 572 return convertValue!T(false); 573 } 574 575 /** 576 * Shorthand for returning nativeObject from casting this to ScriptObject 577 */ 578 T toNativeObject(T)() const 579 { 580 if(!isObject) 581 return cast(T)null; 582 return _asObject.nativeObject!T; 583 } 584 585 /** 586 * Shorthand for testing if this is a ScriptObject containing a native D object of a specific type. 587 */ 588 bool isNativeObjectType(T)() const 589 { 590 if(!isObject) 591 return false; 592 return _asObject.nativeObject!T !is null; 593 } 594 595 /// For use with the scripting language's typeof operator 596 string typeToString() const 597 { 598 final switch(_type) 599 { 600 case Type.NULL: return "null"; 601 case Type.UNDEFINED: return "undefined"; 602 case Type.BOOLEAN: return "boolean"; 603 case Type.INTEGER: return "integer"; 604 case Type.DOUBLE: return "double"; 605 case Type.STRING: return "string"; 606 case Type.ARRAY: return "array"; 607 case Type.FUNCTION: return "function"; 608 case Type.OBJECT: return "object"; 609 } 610 } 611 612 /** 613 * Shorthand to access fields of the complex object types 614 */ 615 ScriptAny opIndex(in string index) 616 { 617 if(!isObject) 618 return UNDEFINED; 619 return _asObject.lookupField(index); 620 } 621 622 /** 623 * Shorthand to assign fields of the complex object types 624 */ 625 ScriptAny opIndexAssign(T)(T value, string index) 626 { 627 if(!isObject) 628 return UNDEFINED; 629 auto any = ScriptAny(value); 630 _asObject.assignField(index, any); 631 return any; 632 } 633 634 /// Returns a string representation of the stored value 635 auto toString() const 636 { 637 import std.format: format; 638 639 final switch(_type) 640 { 641 case Type.NULL: 642 return "null"; 643 case Type.UNDEFINED: 644 return "undefined"; 645 case Type.BOOLEAN: 646 return _asBoolean.to!string; 647 case Type.INTEGER: 648 return _asInteger.to!string; 649 case Type.DOUBLE: 650 return format("%.15g", _asDouble); 651 case Type.STRING: 652 case Type.ARRAY: 653 case Type.FUNCTION: 654 case Type.OBJECT: 655 if(_asObject !is null) 656 return _asObject.toString(); 657 return "null"; 658 } 659 } 660 661 /// convert a value to raw bytes. Only certain values can be converted. Throws exception if not possible 662 ubyte[] serialize() const 663 { 664 import mildew.util.encode: encode; 665 666 ubyte[] data = encode!uint(_type); 667 668 final switch(_type) 669 { 670 case Type.NULL: 671 case Type.UNDEFINED: 672 break; 673 case Type.BOOLEAN: 674 data ~= _asBoolean ? 1 : 0; 675 break; 676 case Type.INTEGER: 677 data ~= encode(_asInteger); 678 break; 679 case Type.DOUBLE: 680 data ~= encode(_asDouble); 681 break; 682 case Type.STRING: { 683 // save as utf-8 684 immutable str = toValue!string; 685 data ~= encode!(ubyte[])(cast(ubyte[])str); 686 break; 687 } 688 case Type.ARRAY: { 689 auto arr = toValue!(ScriptAny[]); 690 data ~= encode!size_t(arr.length); 691 foreach(item ; arr) 692 { 693 data ~= item.serialize(); 694 } 695 break; 696 } 697 case Type.FUNCTION: { 698 auto func = cast(ScriptFunction)_asObject; 699 if(func.type != ScriptFunction.Type.SCRIPT_FUNCTION) 700 throw new ScriptAnyException("Native functions cannot be serialized", this); 701 data ~= encode(func.functionName); 702 data ~= encode!size_t(func.argNames.length); 703 foreach(arg ; func.argNames) 704 data ~= encode(arg); 705 data ~= func.isClass ? 1 : 0; 706 data ~= func.isGenerator ? 1 : 0; 707 data ~= encode(func.compiled); 708 break; 709 } 710 case Type.OBJECT: 711 throw new ScriptAnyException("Objects cannot be encoded yet", this); 712 } 713 return data; 714 } 715 716 /// read a ScriptAny from a stream of bytes. if invalid data, throws exception 717 static ScriptAny deserialize(ref ubyte[] stream) 718 { 719 // debug import std.stdio; 720 import mildew.util.encode: decode; 721 import mildew.types.array: ScriptArray; 722 import mildew.types..string: ScriptString; 723 724 ScriptAny value = ScriptAny.UNDEFINED; 725 value._type = cast(Type)decode!int(stream); 726 stream = stream[int.sizeof..$]; 727 switch(value._type) 728 { 729 case Type.NULL: 730 // debug write("Decoding a null: "); 731 break; 732 case Type.UNDEFINED: 733 // debug write("Decoding undefined: "); 734 break; 735 case Type.BOOLEAN: 736 // debug write("Decoding a boolean: "); 737 value._asBoolean = cast(bool)decode!ubyte(stream); 738 stream = stream[ubyte.sizeof..$]; 739 break; 740 case Type.INTEGER: 741 // debug write("Decoding a long: "); 742 value._asInteger = decode!long(stream); 743 stream = stream[long.sizeof..$]; 744 break; 745 case Type.DOUBLE: 746 // debug write("Decoding a double: "); 747 value._asDouble = decode!double(stream); 748 stream = stream[double.sizeof..$]; 749 break; 750 case Type.STRING: { 751 // debug write("Decoding a string: "); 752 auto str = cast(string)decode!(ubyte[])(stream); 753 stream = stream[size_t.sizeof..$]; 754 stream = stream[str.length*char.sizeof..$]; 755 value._asObject = new ScriptString(str); 756 break; 757 } 758 case Type.ARRAY: { 759 // debug write("Decoding an array: "); 760 immutable len = decode!size_t(stream); 761 stream = stream[size_t.sizeof..$]; 762 auto array = new ScriptAny[len]; 763 for(auto i = 0; i < len; ++i) 764 { 765 array[i] = ScriptAny.deserialize(stream); 766 } 767 value._asObject = new ScriptArray(array); 768 break; 769 } 770 case Type.FUNCTION: { 771 // debug write("Decoding a function: "); 772 auto fnname = cast(string)decode!(ubyte[])(stream); 773 stream = stream[size_t.sizeof..$]; 774 stream = stream[fnname.length*char.sizeof..$]; 775 string[] args; 776 immutable argLen = decode!size_t(stream); 777 stream = stream[size_t.sizeof..$]; 778 args = new string[argLen]; 779 for(auto i = 0; i < argLen; ++i) 780 { 781 args[i] = cast(string)(decode!(ubyte[])(stream)); 782 stream = stream[size_t.sizeof..$]; 783 stream = stream[args[i].length * char.sizeof .. $]; 784 } 785 bool isClass = cast(bool)stream[0]; 786 stream = stream[1..$]; 787 bool isGenerator = cast(bool)stream[0]; 788 stream = stream[1..$]; 789 auto compiled = decode!(ubyte[])(stream); 790 stream = stream[size_t.sizeof..$]; 791 stream = stream[compiled.length..$]; 792 value._asObject = new ScriptFunction(fnname.to!string, args, compiled, isClass, isGenerator); 793 break; 794 } 795 case Type.OBJECT: 796 throw new ScriptAnyException("Objects cannot be decoded yet", value); 797 default: 798 throw new ScriptAnyException("Decoded value is not a ScriptAny (" 799 ~ to!string(cast(int)value._type) ~ ")", ScriptAny.UNDEFINED); 800 } 801 802 // writeln(value.toString()); 803 return value; 804 } 805 806 /** 807 * This should always be used to return an undefined value. 808 */ 809 static immutable UNDEFINED = ScriptAny(); 810 811 private: 812 813 void setValue(T)(T value) 814 { 815 import mildew.types.array: ScriptArray; 816 import mildew.types.func: ScriptFunction; 817 import mildew.types.object: ScriptObject; 818 import mildew.types..string: ScriptString; 819 820 static if(isBoolean!T) 821 { 822 _type = Type.BOOLEAN; 823 _asBoolean = value; 824 } 825 else static if(isIntegral!T) 826 { 827 _type = Type.INTEGER; 828 _asInteger = cast(long)value; 829 } 830 else static if(isFloatingPoint!T) 831 { 832 _type = Type.DOUBLE; 833 _asDouble = cast(double)value; 834 } 835 else static if(isSomeString!T) 836 { 837 _type = Type.STRING; 838 _asObject = new ScriptString(value.to!string); 839 } 840 else static if(is(T == ScriptAny[])) 841 { 842 _type = Type.ARRAY; 843 _asObject = new ScriptArray(value); 844 } 845 else static if(isArray!T) 846 { 847 _type = Type.ARRAY; 848 ScriptAny[] arr; 849 foreach(item; value) 850 { 851 arr ~= ScriptAny(item); 852 } 853 _asObject = new ScriptArray(arr); 854 } 855 else static if(is(T == ScriptFunction)) 856 { 857 _type = Type.FUNCTION; 858 _asObject = value; 859 if(_asObject is null) 860 _type = Type.NULL; 861 } 862 else static if(is(T == ScriptObject)) 863 { 864 _type = Type.OBJECT; 865 _asObject = value; 866 if(_asObject is null) 867 _type = Type.NULL; 868 } 869 else static if(is(T == ScriptAny) || is(T == immutable(ScriptAny)) || is(T == const(ScriptAny))) 870 { 871 this._type = value._type; 872 final switch(value._type) 873 { 874 case Type.UNDEFINED: 875 case Type.NULL: 876 break; 877 case Type.BOOLEAN: 878 this._asBoolean = value._asBoolean; 879 break; 880 case Type.INTEGER: 881 this._asInteger = value._asInteger; 882 break; 883 case Type.DOUBLE: 884 this._asDouble = value._asDouble; 885 break; 886 case Type.STRING: 887 case Type.ARRAY: 888 case Type.FUNCTION: 889 case Type.OBJECT: 890 this._asObject = cast(ScriptObject)(value._asObject); 891 break; 892 } 893 } 894 else static if(is(T == typeof(null))) 895 { 896 _type = Type.NULL; 897 _asObject = null; 898 } 899 else // can't directly set D objects because ScriptAny must be verified as a ScriptObject first! 900 static assert(false, "This type is not supported: " ~ T.stringof); 901 } 902 903 T convertValue(T)(bool throwing) const 904 { 905 import mildew.types..string: ScriptString; 906 import mildew.types.array: ScriptArray; 907 import mildew.types.func: ScriptFunction; 908 909 static if(isBoolean!T) 910 { 911 if(_type == Type.NULL || _type == Type.UNDEFINED) 912 return false; 913 else if (_type == Type.BOOLEAN) 914 return _asBoolean; 915 else if (this.isNumber()) 916 return convertValue!double(throwing) != 0.0; 917 else if(_type == Type.STRING) 918 { 919 auto s = cast(ScriptString)_asObject; 920 return s.toString() != ""; 921 } 922 else if(_type == Type.ARRAY) 923 { 924 auto arr = cast(ScriptArray)_asObject; 925 // return arr.array.length != 0; 926 return true; 927 } 928 else 929 return _asObject !is null; 930 } 931 else static if(isIntegral!T || isFloatingPoint!T) 932 { 933 if(!this.isNumber()) 934 { 935 if(throwing) 936 throw new ScriptAnyException("Unable to convert value " ~ toString ~ " to number", this); 937 else 938 return cast(T)0; 939 } 940 else if(_type == Type.BOOLEAN) 941 return cast(T)_asBoolean; 942 else if(_type == Type.INTEGER) 943 return cast(T)_asInteger; 944 else if(_type == Type.DOUBLE) 945 return cast(T)_asDouble; 946 else // if null 947 return 0; 948 } 949 else static if(isSomeString!T) 950 { 951 return to!T(toString()); 952 } 953 else static if(is(T == ScriptAny[])) 954 { 955 if(_type != Type.ARRAY) 956 { 957 if(throwing) 958 throw new ScriptAnyException("ScriptAny " ~ toString ~ " is not an array", this); 959 else 960 return cast(T)null; 961 } 962 else 963 { 964 auto arr = cast(ScriptArray)_asObject; 965 return cast(ScriptAny[])arr.array; 966 } 967 } 968 else static if(is(T : E[], E)) 969 { 970 if(_type != Type.ARRAY) 971 { 972 if(throwing) 973 throw new ScriptAnyException("ScriptAny " ~ toString ~ " is not an array", this); 974 else 975 return cast(T)null; 976 } 977 T arrayToFill = []; 978 auto scriptArray = cast(ScriptArray)_asObject; 979 foreach(item ; scriptArray.array) 980 { 981 arrayToFill ~= item.convertValue!E(throwing); 982 } 983 return arrayToFill; 984 } 985 else static if(is(T == ScriptArray)) 986 { 987 if(_type != Type.ARRAY) 988 { 989 if(throwing) 990 throw new ScriptAnyException("ScriptAny " ~ toString ~ " is not a ScriptArray", this); 991 else 992 return cast(T)null; 993 } 994 return cast(T)_asObject; 995 } 996 else static if(is(T == ScriptString)) 997 { 998 if(_type != Type.STRING) 999 { 1000 if(throwing) 1001 throw new ScriptAnyException("ScriptAny " ~ toString ~ " is not a ScriptString", this); 1002 else 1003 return cast(T)null; 1004 } 1005 return cast(T)_asObject; 1006 } 1007 else static if(is(T == ScriptFunction)) 1008 { 1009 if(_type != Type.FUNCTION) 1010 { 1011 if(throwing) 1012 throw new ScriptAnyException("ScriptAny " ~ toString ~ " is not a ScriptFunction", this); 1013 else 1014 return cast(T)null; 1015 } 1016 else 1017 { 1018 return cast(T)_asObject; 1019 } 1020 } 1021 else static if(is(T == ScriptObject)) 1022 { 1023 if(!isObject) 1024 { 1025 if(throwing) 1026 throw new ScriptAnyException("ScriptAny " ~ toString ~ " is not an object", this); 1027 else 1028 return cast(T)null; 1029 } 1030 else 1031 { 1032 return cast(T)_asObject; 1033 } 1034 } 1035 else static if(is(T == class) || is(T == interface)) 1036 { 1037 if(!isObject) 1038 { 1039 if(throwing) 1040 throw new ScriptAnyException("ScriptAny " ~ toString ~ " cannot store a D object", this); 1041 else 1042 return cast(T)null; 1043 } 1044 else 1045 { 1046 return _asObject.nativeObject!T; 1047 } 1048 } 1049 else 1050 static assert(false, "This type is not supported: " ~ T.stringof); 1051 } 1052 1053 Type _type = Type.UNDEFINED; 1054 1055 union 1056 { 1057 bool _asBoolean; 1058 long _asInteger; 1059 double _asDouble; 1060 /// this includes array, string, function, or object 1061 ScriptObject _asObject; 1062 } 1063 } 1064 1065 /** 1066 * This exception is only thrown when using ScriptAny.checkValue. 1067 */ 1068 class ScriptAnyException : Exception 1069 { 1070 /// ctor 1071 this(string msg, ScriptAny val, string file = __FILE__, size_t line = __LINE__) 1072 { 1073 super(msg, file, line); 1074 value = val; 1075 } 1076 /// the offending value 1077 ScriptAny value; 1078 } 1079 1080 unittest 1081 { 1082 import std.stdio: writeln, writefln, writef; 1083 ScriptAny foo = [1, 5, 10]; 1084 auto data = foo.serialize(); 1085 foreach( b ; data ) 1086 { 1087 writef("%02x ", b); 1088 } 1089 writeln(); 1090 1091 ScriptAny des = ScriptAny.deserialize(data); 1092 writeln(des); 1093 }