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 }