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 }