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