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