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