1 /**
2  * This module implements the Interpreter class, the main class used by host applications to run scripts
3  */
4 module mildew.interpreter;
5 
6 import std.variant;
7 
8 import mildew.context;
9 import mildew.exceptions: ScriptRuntimeException;
10 import mildew.lexer: Token, Lexer;
11 import mildew.nodes;
12 import mildew.parser;
13 import mildew.types;
14 import mildew.visitors;
15 
16 /**
17  * This is the main interface for the host application to interact with scripts.
18  */
19 class Interpreter : INodeVisitor
20 {
21 public:
22 
23     /**
24      * Constructs a new Interpreter with a global context. Note that all calls to evaluate
25      * run in a new context below the global context. This allows keywords such as let and const
26      * to not pollute the global namespace. However, scripts can use var to declare variables that
27      * are global.
28      */
29     this()
30     {
31         _globalContext = new Context(this);
32         _currentContext = _globalContext;
33     }
34 
35     /**
36      * Initializes the Mildew standard library, such as Object, Math, and console namespaces. This
37      * is optional and is not called by the constructor. For a script to use these methods this
38      * must be called first.
39      */
40     void initializeStdlib()
41     {
42         import mildew.types.bindings: initializeTypesLibrary;
43         import mildew.stdlib.global: initializeGlobalLibrary;
44         import mildew.stdlib.console: initializeConsoleLibrary;
45         import mildew.stdlib.date: initializeDateLibrary;
46         import mildew.stdlib.math: initializeMathLibrary;
47         initializeTypesLibrary(this);
48         initializeGlobalLibrary(this);
49         initializeConsoleLibrary(this);
50         initializeDateLibrary(this);
51         initializeMathLibrary(this);
52     }
53 
54     /**
55      * Calls a script function. Can throw ScriptRuntimeException.
56      */
57     ScriptAny callFunction(ScriptFunction func, ScriptAny thisObj, ScriptAny[] args)
58     {
59         auto vr = callFn(func, thisObj, args, false);
60         if(vr.exception)
61             throw vr.exception;
62         return vr.result;
63     }
64 
65     /**
66      * This is the main entry point for evaluating a script program.
67      * Params:
68      *  code = This is the code of a script to be executed.
69      * Returns:
70      *  If the script has a return statement with an expression, this value will be the result of that expression
71      *  otherwise it will be ScriptAny.UNDEFINED
72      */
73     ScriptAny evaluate(in string code)
74     {
75         debug import std.stdio: writeln;
76 
77         auto lexer = Lexer(code);
78         auto tokens = lexer.tokenize();
79         auto parser = Parser(tokens);
80         // debug writeln(tokens);
81         auto programBlock = parser.parseProgram();
82         auto vr = programBlock.accept(this).get!VisitResult;
83         if(vr.exception !is null)
84             throw vr.exception;
85         if(vr.returnFlag)
86             return vr.result;
87         return ScriptAny.UNDEFINED;
88     }
89 
90     // TODO: Read script from file
91 
92     // TODO: Create an evaluate function with default exception handling with file name info
93 
94     /**
95      * Sets a global variable or constant without checking whether or not the variable or const was already
96      * declared. This is used by host applications to define custom functions or objects.
97      * Params:
98      *  name = The name of the variable.
99      *  value = The value the variable should be set to.
100      *  isConst = Whether or not the script can overwrite the global.
101      */
102     void forceSetGlobal(T)(in string name, T value, bool isConst=false)
103     {
104         _globalContext.forceSetVarOrConst(name, ScriptAny(value), isConst);
105     }
106 
107     /**
108      * Unsets a variable or constant in the global context. Used by host applications to remove
109      * items that were loaded by the standard library load functions.
110      */
111     void forceUnsetGlobal(in string name)
112     {
113         _globalContext.forceRemoveVarOrConst(name);
114     }
115 	
116 	/// extract a VisitResult from a LiteralNode
117 	Variant visitLiteralNode(LiteralNode lnode)
118 	{
119 		import mildew.exceptions: ScriptCompileException;
120 		import mildew.parser: Parser;
121 		import mildew.lexer: Lexer;
122 
123 		if(lnode.literalToken.literalFlag == Token.LiteralFlag.TEMPLATE_STRING)
124 		{
125 			size_t currentStart = 0;
126             size_t endLast;
127             bool addToParseString = false;
128             string result;
129             string stringToParse;
130             for(size_t index = 0; index < lnode.literalToken.text.length; ++index)
131             {
132                 if(lnode.literalToken.text[index] == '$')
133                 {
134                     if(index < lnode.literalToken.text.length - 1) // @suppress(dscanner.suspicious.length_subtraction)
135                     {
136                         if(lnode.literalToken.text[index+1] == '{')
137                         {
138                             addToParseString = true;
139                             index += 1;
140                         }
141                         else
142                         {
143                             if(addToParseString)
144                                 stringToParse ~= lnode.literalToken.text[index];
145                             else
146                                 endLast = index + 1;
147                         }
148                     }
149                     else 
150                     {
151                         if(addToParseString)
152                             stringToParse ~= lnode.literalToken.text[index];
153                         else
154                             endLast = index + 1; 
155                     }
156                 }
157                 else if(lnode.literalToken.text[index] == '}' && addToParseString)
158                 {
159                     result ~= lnode.literalToken.text[currentStart .. endLast];
160                     auto lexer = Lexer(stringToParse);
161                     auto parser = Parser(lexer.tokenize());
162                     ExpressionNode expressionNode;
163                     VisitResult vr;
164                     try 
165                     {
166                         expressionNode = parser.parseExpression();
167                     }
168                     catch(ScriptCompileException ex)
169                     {
170                         vr.exception = new ScriptRuntimeException(ex.msg);
171                         return Variant(vr);
172                     }
173                     vr = expressionNode.accept(this).get!VisitResult;
174                     if(vr.exception !is null)
175                         return Variant(vr);
176                     result ~= vr.result.toString();
177                     addToParseString = false;
178                     currentStart = index+1;
179                     stringToParse = "";
180                 }
181                 else
182                 {
183                     if(addToParseString)
184                         stringToParse ~= lnode.literalToken.text[index];
185                     else
186                         endLast = index + 1;
187                 }
188             }
189             if(addToParseString)
190             {
191                 VisitResult vr;
192                 vr.exception = new ScriptRuntimeException("Unclosed template string expression");
193                 return Variant(vr);
194             }
195             if(currentStart < lnode.literalToken.text.length)
196                 result ~= lnode.literalToken.text[currentStart .. endLast];
197             return Variant(VisitResult(result));
198 		}
199 		else
200 		{
201 			return Variant(VisitResult(lnode.value));
202 		}
203 	}
204 	
205     /// handles function literals
206     Variant visitFunctionLiteralNode(FunctionLiteralNode flnode)
207     {
208         auto func = new ScriptFunction("<anonymous function>", flnode.argList, flnode.statements, _currentContext);
209         return Variant(VisitResult(ScriptAny(func)));
210     }
211 
212 	/// return an array from an array literal node
213 	Variant visitArrayLiteralNode(ArrayLiteralNode alnode)
214 	{
215 		VisitResult vr;
216         ScriptAny[] values = [];
217         foreach(expression ; alnode.valueNodes)
218         {
219             vr = expression.accept(this).get!VisitResult;
220             if(vr.exception !is null)
221                 return Variant(vr);
222             values ~= vr.result;
223         }
224         vr.result = values;
225         return Variant(vr);     
226 	}
227 	
228 	/// generates object from object literal node
229 	Variant visitObjectLiteralNode(ObjectLiteralNode olnode)
230 	{
231 		if(olnode.keys.length != olnode.valueNodes.length)
232             throw new Exception("Error with object literal node");
233         ScriptAny[] vals = [];
234         VisitResult vr;
235         foreach(valueNode ; olnode.valueNodes)
236         {
237             vr = valueNode.accept(this).get!VisitResult;
238             if(vr.exception !is null)
239                 return Variant(vr);
240             vals ~= vr.result;
241         }
242         auto obj = new ScriptObject("", null, null);
243         for(size_t i = 0; i < olnode.keys.length; ++i)
244         {
245             obj.assignField(olnode.keys[i], vals[i]);
246         }
247         vr.result = obj;
248         return Variant(vr);
249 	}
250 	
251     /// handle class literals
252 	Variant visitClassLiteralNode(ClassLiteralNode clnode)
253 	{
254 		VisitResult vr;
255 
256         try 
257         {
258             vr.result = clnode.classDefinition.create(_currentContext);
259         }
260         catch(ScriptRuntimeException ex)
261         {
262             vr.exception = ex;
263         }
264 		        
265         return Variant(vr);
266 	}
267 	
268 	/// processes binary operations including assignment
269 	Variant visitBinaryOpNode(BinaryOpNode bonode)
270 	{
271 		import std.conv: to;
272 
273         auto lhsResult = bonode.leftNode.accept(this).get!VisitResult;
274         auto rhsResult = bonode.rightNode.accept(this).get!VisitResult;
275 
276         if(lhsResult.exception !is null)
277             return Variant(lhsResult);
278         if(rhsResult.exception !is null)
279             return Variant(rhsResult);
280 
281         VisitResult finalResult;
282 
283         if(bonode.opToken.isAssignmentOperator)
284         {
285             // if an anonymous class or function is being assigned we need to update its name
286             if(rhsResult.result.type == ScriptAny.Type.FUNCTION)
287             {
288                 auto func = rhsResult.result.toValue!ScriptFunction;
289                 if(func.functionName == "<anonymous function>" || func.functionName == "<anonymous class>")
290                     func.functionName = bonode.leftNode.toString;
291             }
292             final switch(lhsResult.accessType)
293             {
294             case VisitResult.AccessType.NO_ACCESS:
295                 finalResult.exception = new ScriptRuntimeException("Invalid left hand assignment");
296                 return Variant(finalResult);
297             case VisitResult.AccessType.VAR_ACCESS:
298                 return Variant(handleVarReassignment(bonode.opToken, lhsResult.memberOrVarToAccess, rhsResult.result));
299             case VisitResult.AccessType.ARRAY_ACCESS:
300                 return Variant(handleArrayReassignment(bonode.opToken, lhsResult.objectToAccess, 
301                         lhsResult.indexToAccess, rhsResult.result));
302             case VisitResult.AccessType.OBJECT_ACCESS:
303                 return Variant(handleObjectReassignment(bonode.opToken, lhsResult.objectToAccess, 
304 				        lhsResult.memberOrVarToAccess, rhsResult.result));
305             }
306         }
307 
308         auto lhs = lhsResult.result;
309         auto rhs = rhsResult.result;
310 
311         switch(bonode.opToken.type)
312         {
313         case Token.Type.POW:
314             return Variant(VisitResult(lhs ^^ rhs));
315         case Token.Type.STAR:
316             return Variant(VisitResult(lhs * rhs));
317         case Token.Type.FSLASH:
318             return Variant(VisitResult(lhs / rhs));
319         case Token.Type.PERCENT:
320             return Variant(VisitResult(lhs % rhs));
321         case Token.Type.PLUS:
322             return Variant(VisitResult(lhs + rhs));
323         case Token.Type.DASH:
324             return Variant(VisitResult(lhs - rhs));
325         case Token.Type.BIT_LSHIFT:
326             return Variant(VisitResult(lhs << rhs));
327         case Token.Type.BIT_RSHIFT:
328             return Variant(VisitResult(lhs >> rhs));
329         case Token.Type.BIT_URSHIFT:
330             return Variant(VisitResult(lhs >>> rhs));
331         case Token.Type.GT:
332             return Variant(VisitResult(lhs > rhs));
333         case Token.Type.GE:
334             return Variant(VisitResult(lhs >= rhs));
335         case Token.Type.LT:
336             return Variant(VisitResult(lhs < rhs));
337         case Token.Type.LE:
338             return Variant(VisitResult(lhs <= rhs));
339         case Token.Type.EQUALS:
340             return Variant(VisitResult(lhs == rhs));
341         case Token.Type.NEQUALS:
342             return Variant(VisitResult(lhs != rhs));
343         case Token.Type.STRICT_EQUALS:
344             return Variant(VisitResult(lhs.strictEquals(rhs)));
345         case Token.Type.STRICT_NEQUALS:
346             return Variant(VisitResult(!lhs.strictEquals(rhs)));
347         case Token.Type.BIT_AND:
348             return Variant(VisitResult(lhs & rhs));
349         case Token.Type.BIT_XOR:
350             return Variant(VisitResult(lhs ^ rhs));
351         case Token.Type.BIT_OR:
352             return Variant(VisitResult(lhs | rhs));
353         case Token.Type.AND:
354             return Variant(VisitResult(lhs && rhs));
355         case Token.Type.OR:
356             return Variant(VisitResult(lhs.orOp(rhs)));
357         default:
358             if(bonode.opToken.isKeyword("instanceof"))
359             {
360                 if(!lhs.isObject)
361                     return Variant(VisitResult(false));
362                 if(rhs.type != ScriptAny.Type.FUNCTION)
363                     return Variant(VisitResult(false));
364                 auto lhsObj = lhs.toValue!ScriptObject; // @suppress(dscanner.suspicious.unmodified)
365                 auto rhsFunc = rhs.toValue!ScriptFunction; // @suppress(dscanner.suspicious.unmodified)
366                 auto proto = lhsObj.prototype;
367                 while(proto !is null)
368                 {
369                     if(proto["constructor"].toValue!ScriptFunction is rhsFunc)
370                         return Variant(VisitResult(true));
371                     proto = proto.prototype;
372                 }
373                 return Variant(VisitResult(false));
374             }
375             else
376                 throw new Exception("Forgot to implement missing binary operator " 
377                     ~ bonode.opToken.type.to!string ~ " for " ~ this.toString());
378         }
379 	}
380 	
381     /// returns a value from a unary operation
382 	Variant visitUnaryOpNode(UnaryOpNode uonode)
383 	{
384 		auto vr = uonode.operandNode.accept(this).get!VisitResult;
385         if(vr.exception !is null)
386             return Variant(vr);
387         auto value = vr.result;
388         int incOrDec = 0;
389         switch(uonode.opToken.type)
390         {
391         case Token.Type.BIT_NOT:
392             return Variant(VisitResult(~value));
393         case Token.Type.NOT:
394             return Variant(VisitResult(!value));
395         case Token.Type.PLUS:
396             return Variant(VisitResult(value));
397         case Token.Type.DASH:
398             return Variant(VisitResult(-value));
399         case Token.Type.DEC:
400             incOrDec = -1;
401             break;
402         case Token.Type.INC:
403             incOrDec = 1;
404             break;
405         default:
406             if(uonode.opToken.isKeyword("typeof"))
407                 return Variant(VisitResult(value.typeToString()));
408             return Variant(VisitResult(ScriptAny.UNDEFINED));
409         }
410 
411         if(incOrDec != 0)
412         {
413             // TODO: fix this to allow constructs such as ++foo++
414             if(vr.accessType == VisitResult.AccessType.VAR_ACCESS)
415                 return Variant(handleVarReassignment(Token.createFakeToken(Token.Type.PLUS_ASSIGN,""), 
416                         vr.memberOrVarToAccess, ScriptAny(incOrDec)));
417             else if(vr.accessType == VisitResult.AccessType.ARRAY_ACCESS)
418                 return Variant(handleArrayReassignment(Token.createFakeToken(Token.Type.PLUS_ASSIGN,""), 
419                         vr.objectToAccess, vr.indexToAccess, ScriptAny(incOrDec)));
420             else if(vr.accessType == VisitResult.AccessType.OBJECT_ACCESS)
421                 return Variant(handleObjectReassignment(Token.createFakeToken(Token.Type.PLUS_ASSIGN,""), 
422                         vr.objectToAccess, vr.memberOrVarToAccess, ScriptAny(incOrDec)));
423             else
424                 vr.exception = new ScriptRuntimeException("Invalid operand for " ~ uonode.opToken.symbol);
425         }
426         return Variant(vr);
427 	}
428 	
429     /// handle constructs such as i++ and i--
430 	Variant visitPostfixOpNode(PostfixOpNode ponode)
431 	{
432 		// first get the operand's original value that will be returned
433         VisitResult vr = ponode.operandNode.accept(this).get!VisitResult;
434         if(vr.exception !is null)
435             return Variant(vr);
436         auto incOrDec = 0;
437         if(ponode.opToken.type == Token.Type.INC)
438             incOrDec = 1;
439         else if(ponode.opToken.type == Token.Type.DEC)
440             incOrDec = -1;
441         else
442             throw new Exception("Impossible parse state: invalid postfix operator");
443         // now perform an increment or decrement assignment based on object access type
444         VisitResult errVR;
445         if(vr.accessType == VisitResult.AccessType.VAR_ACCESS)
446             errVR = handleVarReassignment(Token.createFakeToken(Token.Type.PLUS_ASSIGN,""), 
447                 vr.memberOrVarToAccess, ScriptAny(incOrDec));
448         else if(vr.accessType == VisitResult.AccessType.ARRAY_ACCESS)
449             errVR = handleArrayReassignment(Token.createFakeToken(Token.Type.PLUS_ASSIGN,""), 
450                 vr.objectToAccess, vr.indexToAccess, ScriptAny(incOrDec));
451         else if(vr.accessType == VisitResult.AccessType.OBJECT_ACCESS)
452             errVR = handleObjectReassignment(Token.createFakeToken(Token.Type.PLUS_ASSIGN,""), 
453                 vr.objectToAccess, vr.memberOrVarToAccess, ScriptAny(incOrDec));
454         else
455             vr.exception = new ScriptRuntimeException("Invalid post operand for " ~ ponode.opToken.symbol);
456         if(errVR.exception !is null)
457             return Variant(errVR);
458         return Variant(vr);
459 	}
460 	
461     /// handles : ? operator
462 	Variant visitTerniaryOpNode(TerniaryOpNode tonode)
463 	{
464 		// first evaluate the condition
465         auto vr = tonode.conditionNode.accept(this).get!VisitResult;
466         if(vr.exception !is null)
467             return Variant(vr);
468         if(vr.result)
469             vr = tonode.onTrueNode.accept(this).get!VisitResult;
470         else
471             vr = tonode.onFalseNode.accept(this).get!VisitResult;
472         return Variant(vr);
473 	}
474 	
475     /// handles variable access
476 	Variant visitVarAccessNode(VarAccessNode vanode)
477 	{
478 		VisitResult vr;
479         vr.accessType = VisitResult.AccessType.VAR_ACCESS;
480         vr.memberOrVarToAccess = vanode.varToken.text;
481         bool _; // @suppress(dscanner.suspicious.unmodified)
482         immutable ptr = cast(immutable)_currentContext.lookupVariableOrConst(vanode.varToken.text, _);
483         if(ptr == null)
484             vr.exception = new ScriptRuntimeException("Undefined variable lookup " ~ vanode.varToken.text);
485         else
486             vr.result = *ptr;
487         return Variant(vr);
488 	}
489 	
490     /// handles function calls
491 	Variant visitFunctionCallNode(FunctionCallNode fcnode)
492 	{
493         ScriptAny thisObj;
494         auto vr = fcnode.functionToCall.accept(this).get!VisitResult;
495 
496         if(vr.exception !is null)
497             return Variant(vr);
498 
499 		// the "this" is the left hand of dot operation
500         if(vr.accessType == VisitResult.AccessType.OBJECT_ACCESS 
501             || vr.accessType == VisitResult.AccessType.ARRAY_ACCESS)
502         {
503             thisObj = vr.objectToAccess;
504         }
505 		// or it is local "this" if exists
506 		else if(_currentContext.variableOrConstExists("this"))
507 		{
508 			bool _; // @suppress(dscanner.suspicious.unmodified)
509 			thisObj = *(_currentContext.lookupVariableOrConst("this", _));
510 		}
511 
512         auto fnToCall = vr.result;
513         if(fnToCall.type == ScriptAny.Type.FUNCTION)
514         {
515             ScriptAny[] args;
516             vr = convertExpressionsToArgs(fcnode.expressionArgs, args);
517             if(vr.exception !is null)
518                 return Variant(vr);
519             auto fn = fnToCall.toValue!ScriptFunction;
520             vr = callFn(fn, thisObj, args, fcnode.returnThis);
521             return Variant(vr);
522         }
523         else 
524         {
525             vr.result = ScriptAny.UNDEFINED;
526             vr.exception = new ScriptRuntimeException("Unable to call non function " ~ fnToCall.toString);
527             return Variant(vr);
528         }
529 	}
530 	
531     /// handle array index
532 	Variant visitArrayIndexNode(ArrayIndexNode ainode)
533 	{
534 		VisitResult vr = ainode.indexValueNode.accept(this).get!VisitResult;
535         if(vr.exception !is null)
536             return Variant(vr);
537         auto index = vr.result;
538         auto objVR = ainode.objectNode.accept(this).get!VisitResult;
539         if(objVR.exception !is null)
540             return Variant(objVR);
541 
542         // also need to validate that the object can be accessed
543         if(!objVR.result.isObject)
544         {
545             vr.exception = new ScriptRuntimeException("Cannot index value " ~ objVR.result.toString);
546             return Variant(vr);
547         }
548 
549         if(index.type == ScriptAny.Type.STRING)
550         {
551             // we have to be accessing an object or trying to
552             auto indexAsStr = index.toString();
553             vr.accessType = VisitResult.AccessType.OBJECT_ACCESS;
554             vr.memberOrVarToAccess = index.toString();
555             vr.objectToAccess = objVR.result;
556             vr.result = vr.objectToAccess.lookupField(indexAsStr);
557         }
558         else if(index.isNumber)
559         {
560             auto indexAsNum = index.toValue!size_t;
561             vr.accessType = VisitResult.AccessType.ARRAY_ACCESS;
562             vr.indexToAccess = indexAsNum;
563             vr.objectToAccess = objVR.result;
564             if(auto asString = objVR.result.toValue!ScriptString)
565             {
566                 // TODO catch the UTFException and just return whatever
567                 auto wstr = asString.getWString();
568                 if(indexAsNum >= wstr.length)
569                     vr.result = ScriptAny.UNDEFINED;
570                 else
571                     vr.result = ScriptAny(cast(wstring)([ wstr[indexAsNum] ]));
572             }
573             else if(auto asArray = objVR.result.toValue!ScriptArray)
574             {
575                 if(indexAsNum >= asArray.array.length)
576                     vr.result = ScriptAny.UNDEFINED;
577                 else
578                     vr.result = asArray.array[indexAsNum];
579             }
580             else
581             {
582                 vr.exception = new ScriptRuntimeException("Attempt to index a non-string or non-array");
583             }
584         }
585         else
586         {
587             vr.exception = new ScriptRuntimeException("Invalid index type for array or object access");
588         }
589         return Variant(vr);
590 	}
591 	
592     /// handle dot operator
593 	Variant visitMemberAccessNode(MemberAccessNode manode)
594 	{
595         VisitResult vr;
596         string memberName = "";
597         if(auto van = cast(VarAccessNode)manode.memberNode)
598         {
599             memberName = van.varToken.text;
600         }
601         else
602         {
603             vr.exception = new ScriptRuntimeException("Invalid operand for object member access");
604             return Variant(vr);
605         }
606 
607         auto objVR = manode.objectNode.accept(this).get!VisitResult;
608         if(objVR.exception !is null)
609             return Variant(objVR);
610         // validate that objVR.result is of type object so that it can even be accessed
611         if(!objVR.result.isObject)
612         {
613             vr.exception = new ScriptRuntimeException("Cannot access non-object " 
614                 ~ objVR.result.toString() ~ ": " ~ this.toString());
615             return Variant(vr);
616         }
617 
618         // set the fields
619         vr.accessType = VisitResult.AccessType.OBJECT_ACCESS;
620         vr.objectToAccess = objVR.result;
621         vr.memberOrVarToAccess = memberName;
622         // if this is a get property we need to use the getter otherwise we lookup field
623         auto obj = vr.objectToAccess.toValue!ScriptObject;
624         if(obj.hasGetter(memberName))
625         {
626             auto gvr = getObjectProperty(obj, memberName);
627             if(gvr.exception !is null)
628                 return Variant(gvr);
629             vr.result = gvr.result;
630         }
631         else
632             vr.result = objVR.result.lookupField(memberName);
633         return Variant(vr);
634 	}
635 	
636     /// handles new expression
637 	Variant visitNewExpressionNode(NewExpressionNode nenode)
638 	{
639 		// fce should be a valid function call with its returnThis flag already set by the parser
640         auto vr = nenode.functionCallExpression.accept(this);
641         return vr; // caller will check for any exceptions.
642 	}
643 	
644     /// handles var, let, and const declarations
645 	Variant visitVarDeclarationStatementNode(VarDeclarationStatementNode vdsnode)
646 	{
647 		VisitResult visitResult;
648         foreach(varNode; vdsnode.varAccessOrAssignmentNodes)
649         {
650             if(auto v = cast(VarAccessNode)varNode)
651             {
652                 auto varName = v.varToken.text;
653                 visitResult = handleVarDeclaration(vdsnode.qualifier.text, varName, ScriptAny.UNDEFINED);
654                 if(visitResult.exception !is null)
655                     return Variant(visitResult);
656             }
657             else if(auto binNode = cast(BinaryOpNode)varNode)
658             {
659                 // auto binNode = cast(BinaryOpNode)varNode;
660                 visitResult = binNode.rightNode.accept(this).get!VisitResult;
661                 if(visitResult.exception !is null)
662                     return Variant(visitResult);
663                 auto valueToAssign = visitResult.result;
664                 // we checked this before so should be safe
665                 if(auto van = cast(VarAccessNode)(binNode.leftNode))
666                 {
667                     auto varName = van.varToken.text;
668                     visitResult = handleVarDeclaration(vdsnode.qualifier.text, varName, valueToAssign);
669                     if(visitResult.exception !is null)
670                         return Variant(visitResult);
671                     // success so make sure anon function name matches
672                     if(valueToAssign.type == ScriptAny.Type.FUNCTION)
673                     {
674                         auto func = valueToAssign.toValue!ScriptFunction;
675                         if(func.functionName == "<anonymous function>" || func.functionName == "<anonymous class>")
676                             func.functionName = varName;
677                     }
678                 }
679             }
680             else 
681                 throw new Exception("Invalid declaration got past the parser");
682         }
683         return Variant(VisitResult(ScriptAny.UNDEFINED));
684 	}
685 	
686     /// handles {block} statement
687 	Variant visitBlockStatementNode(BlockStatementNode bsnode)
688 	{
689         Context oldContext = _currentContext; // @suppress(dscanner.suspicious.unmodified)
690 		_currentContext = new Context(_currentContext, "<scope>");
691         auto result = VisitResult(ScriptAny.UNDEFINED);
692         foreach(statement ; bsnode.statementNodes)
693         {
694             result = statement.accept(this).get!VisitResult;
695             if(result.returnFlag || result.breakFlag || result.continueFlag || result.exception !is null)
696             {
697                 if(result.exception)
698                     result.exception.scriptTraceback ~= statement;
699                 break;
700             }
701         }   
702         _currentContext = oldContext;
703         return Variant(result);
704 	}
705 	
706     /// handles if statements
707 	Variant visitIfStatementNode(IfStatementNode isnode)
708 	{
709 		auto vr = isnode.conditionNode.accept(this).get!VisitResult;
710         if(vr.exception !is null)
711             return Variant(vr);
712 
713         if(vr.result)
714         {
715             vr = isnode.onTrueStatement.accept(this).get!VisitResult;
716         }
717         else 
718         {
719             if(isnode.onFalseStatement !is null)
720                 vr = isnode.onFalseStatement.accept(this).get!VisitResult;
721         }
722         return Variant(vr);
723 	}
724 	
725     /// handles switch case statements
726 	Variant visitSwitchStatementNode(SwitchStatementNode ssnode)
727 	{
728 		auto vr = ssnode.expressionNode.accept(this).get!VisitResult;
729         if(vr.exception !is null)
730             return Variant(vr);
731         size_t jumpStatement = ssnode.switchBody.defaultStatementID;
732         if(vr.result in ssnode.switchBody.jumpTable)
733         {
734             jumpStatement = ssnode.switchBody.jumpTable[vr.result];
735         }
736         if(jumpStatement < ssnode.switchBody.statementNodes.length)
737         {
738             for(size_t i = jumpStatement; i < ssnode.switchBody.statementNodes.length; ++i)
739             {
740                 vr = ssnode.switchBody.statementNodes[i].accept(this).get!VisitResult;
741                 if(vr.returnFlag || vr.continueFlag || vr.breakFlag || vr.exception !is null)
742                     return Variant(vr);
743             }
744         }
745         return Variant(vr);
746 	}
747 	
748     /// handles while statements
749 	Variant visitWhileStatementNode(WhileStatementNode wsnode)
750 	{
751 		if(wsnode.label != "")
752             _currentContext.insertLabel(wsnode.label);
753         auto vr = wsnode.conditionNode.accept(this).get!VisitResult;
754         while(vr.result && vr.exception is null)
755         {
756             vr = wsnode.bodyNode.accept(this).get!VisitResult;
757             if(vr.breakFlag)
758             {
759                 if(vr.labelName == "")
760                     vr.breakFlag = false;
761                 else
762                 {
763                     if(_currentContext.labelExists(vr.labelName))
764                     {
765                         if(wsnode.label == vr.labelName)
766                             vr.breakFlag = false;
767                     }
768                     else 
769                         vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
770                 }
771                 break;
772             }
773             if(vr.continueFlag)
774             {
775                 if(vr.labelName == "")
776                     vr.continueFlag = false;
777                 else
778                 {
779                     if(_currentContext.labelExists(vr.labelName))
780                     {
781                         if(wsnode.label == vr.labelName)
782                             vr.continueFlag = false;
783                         else
784                             break;
785                     }
786                     else
787                     {
788                         vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
789                         break;
790                     }
791                 }
792             }
793             if(vr.exception !is null || vr.returnFlag)
794                 break;
795             vr = wsnode.conditionNode.accept(this).get!VisitResult;
796         }
797         if(wsnode.label != "")
798             _currentContext.removeLabelFromCurrent(wsnode.label);
799         return Variant(vr);
800 	}
801 	
802     /// handles do-while statement
803 	Variant visitDoWhileStatementNode(DoWhileStatementNode dwsnode)
804 	{
805 		auto vr = VisitResult(ScriptAny.UNDEFINED);
806         if(dwsnode.label != "")
807             _currentContext.insertLabel(dwsnode.label);
808         do 
809         {
810             vr = dwsnode.bodyNode.accept(this).get!VisitResult;
811             if(vr.breakFlag)
812             {
813                 if(vr.labelName == "")
814                     vr.breakFlag = false;
815                 else
816                 {
817                     if(_currentContext.labelExists(vr.labelName))
818                     {
819                         if(dwsnode.label == vr.labelName)
820                             vr.breakFlag = false;
821                     }
822                     else 
823                         vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
824                 }
825                 break;
826             }
827             if(vr.continueFlag)
828             {
829                 if(vr.labelName == "")
830                     vr.continueFlag = false;
831                 else
832                 {
833                     if(_currentContext.labelExists(vr.labelName))
834                     {
835                         if(dwsnode.label == vr.labelName)
836                             vr.continueFlag = false;
837                         else
838                             break;
839                     }
840                     else
841                     {
842                         vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
843                         break;
844                     }
845                 }
846             }
847             if(vr.exception !is null || vr.returnFlag)
848                 break; 
849             vr = dwsnode.conditionNode.accept(this).get!VisitResult;
850         }
851         while(vr.result && vr.exception is null);
852         if(dwsnode.label != "")
853             _currentContext.removeLabelFromCurrent(dwsnode.label);
854         return Variant(vr);
855 	}
856 	
857     /// handles for(;;) statements
858 	Variant visitForStatementNode(ForStatementNode fsnode)
859 	{
860         Context oldContext = _currentContext; // @suppress(dscanner.suspicious.unmodified)
861 		_currentContext = new Context(_currentContext, "<outer_for_loop>");
862         if(fsnode.label != "")
863             _currentContext.insertLabel(fsnode.label);
864         auto vr = VisitResult(ScriptAny.UNDEFINED);
865         if(fsnode.varDeclarationStatement !is null)
866             vr = fsnode.varDeclarationStatement.accept(this).get!VisitResult;
867         if(vr.exception is null)
868         {
869             vr = fsnode.conditionNode.accept(this).get!VisitResult;
870             while(vr.result && vr.exception is null)
871             {
872                 vr = fsnode.bodyNode.accept(this).get!VisitResult;
873                 if(vr.breakFlag)
874                 {
875                     if(vr.labelName == "")
876                         vr.breakFlag = false;
877                     else
878                     {
879                         if(_currentContext.labelExists(vr.labelName))
880                         {
881                             if(fsnode.label == vr.labelName)
882                                 vr.breakFlag = false;
883                         }
884                         else 
885                             vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
886                     }
887                     break;
888                 }
889                 if(vr.continueFlag)
890                 {
891                     if(vr.labelName == "")
892                         vr.continueFlag = false;
893                     else
894                     {
895                         if(_currentContext.labelExists(vr.labelName))
896                         {
897                             if(fsnode.label == vr.labelName)
898                                 vr.continueFlag = false;
899                             else
900                                 break;
901                         }
902                         else
903                         {
904                             vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
905                             break;
906                         }
907                     }
908                 }
909                 if(vr.exception !is null || vr.returnFlag)
910                     break; 
911                 vr = fsnode.incrementNode.accept(this).get!VisitResult;
912                 if(vr.exception !is null)
913                     break;
914                 vr = fsnode.conditionNode.accept(this).get!VisitResult;
915             }
916         }
917         if(fsnode.label != "")
918             _currentContext.removeLabelFromCurrent(fsnode.label);
919         _currentContext = oldContext;
920         return Variant(vr);
921 	}
922 	
923     /// handles for-of (and for-in) loops
924 	Variant visitForOfStatementNode(ForOfStatementNode fosnode)
925 	{
926 		auto vr = fosnode.objectToIterateNode.accept(this).get!VisitResult;
927         // make sure this is iterable
928         if(vr.exception !is null)
929             return Variant(vr);
930 
931         if(fosnode.label != "")
932             _currentContext.insertLabel(fosnode.label);
933 
934         if(vr.result.type == ScriptAny.Type.ARRAY)
935         {
936             auto arr = vr.result.toValue!(ScriptAny[]);
937             for(size_t i = 0; i < arr.length; ++i)
938             {
939                 // TODO optimize this to reassign variables instead of creating new contexts each iteration
940                 auto oldContext = _currentContext; // @suppress(dscanner.suspicious.unmodified)
941                 _currentContext = new Context(_currentContext, "<for_of_loop>");
942                 // if one var access node, then value, otherwise index then value
943                 if(fosnode.varAccessNodes.length == 1)
944                 {
945                     _currentContext.declareVariableOrConst(fosnode.varAccessNodes[0].varToken.text,
946                         arr[i], fosnode.qualifierToken.text == "const"? true: false);
947                 }
948                 else 
949                 {
950                     _currentContext.declareVariableOrConst(fosnode.varAccessNodes[0].varToken.text,
951                         ScriptAny(i), fosnode.qualifierToken.text == "const"? true: false);
952                     _currentContext.declareVariableOrConst(fosnode.varAccessNodes[1].varToken.text,
953                         arr[i], fosnode.qualifierToken.text == "const"? true: false);
954                 }
955                 vr = fosnode.bodyNode.accept(this).get!VisitResult;
956                 _currentContext = oldContext;
957                 if(vr.breakFlag)
958                 {
959                     if(vr.labelName == "")
960                         vr.breakFlag = false;
961                     else
962                     {
963                         if(_currentContext.labelExists(vr.labelName))
964                         {
965                             if(fosnode.label == vr.labelName)
966                                 vr.breakFlag = false;
967                         }
968                         else 
969                             vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
970                     }
971                     break;
972                 }
973                 if(vr.continueFlag)
974                 {
975                     if(vr.labelName == "")
976                         vr.continueFlag = false;
977                     else
978                     {
979                         if(_currentContext.labelExists(vr.labelName))
980                         {
981                             if(fosnode.label == vr.labelName)
982                                 vr.continueFlag = false;
983                             else
984                                 break;
985                         }
986                         else
987                         {
988                             vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
989                             break;
990                         }
991                     }
992                 }
993                 if(vr.exception !is null || vr.returnFlag)
994                     break;
995             }
996         }
997         else if(vr.result.isObject)
998         {
999             auto obj = vr.result.toValue!ScriptObject;
1000             // first value is key, second value is value if there
1001             foreach(key, val; obj.dictionary)
1002             {
1003                 // TODO optimize this to reassign variables instead of creating new ones each iteration
1004                 auto oldContext = _currentContext; // @suppress(dscanner.suspicious.unmodified)
1005                 _currentContext = new Context(_currentContext, "<for_of_loop>");
1006                 _currentContext.declareVariableOrConst(fosnode.varAccessNodes[0].varToken.text,
1007                     ScriptAny(key), fosnode.qualifierToken.text == "const" ? true: false);
1008                 if(fosnode.varAccessNodes.length > 1)
1009                     _currentContext.declareVariableOrConst(fosnode.varAccessNodes[1].varToken.text,
1010                         ScriptAny(val), fosnode.qualifierToken.text == "const" ? true: false);
1011                 vr = fosnode.bodyNode.accept(this).get!VisitResult;              
1012                 _currentContext = oldContext;
1013                 if(vr.breakFlag)
1014                 {
1015                     if(vr.labelName == "")
1016                         vr.breakFlag = false;
1017                     else
1018                     {
1019                         if(_currentContext.labelExists(vr.labelName))
1020                         {
1021                             if(fosnode.label == vr.labelName)
1022                                 vr.breakFlag = false;
1023                         }
1024                         else 
1025                             vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
1026                     }
1027                     break;
1028                 }
1029                 if(vr.continueFlag)
1030                 {
1031                     if(vr.labelName == "")
1032                         vr.continueFlag = false;
1033                     else
1034                     {
1035                         if(_currentContext.labelExists(vr.labelName))
1036                         {
1037                             if(fosnode.label == vr.labelName)
1038                                 vr.continueFlag = false;
1039                             else
1040                                 break;
1041                         }
1042                         else
1043                         {
1044                             vr.exception = new ScriptRuntimeException("Label " ~ vr.labelName ~ " doesn't exist");
1045                             break;
1046                         }
1047                     }
1048                 }
1049                 if(vr.exception !is null || vr.returnFlag)
1050                     break; 
1051                 if(vr.exception !is null)
1052                     break;
1053             }
1054         }
1055 
1056         else 
1057         {
1058             vr.exception = new ScriptRuntimeException("Cannot iterate over " ~ fosnode.objectToIterateNode.toString);
1059         }
1060 
1061         if(fosnode.label != "")
1062             _currentContext.removeLabelFromCurrent(fosnode.label);
1063 
1064         return Variant(vr);
1065 	}
1066 	
1067     /// handle break statements
1068 	Variant visitBreakStatementNode(BreakStatementNode bsnode)
1069 	{
1070 		auto vr = VisitResult(ScriptAny.UNDEFINED);
1071         vr.breakFlag = true;
1072         vr.labelName = bsnode.label;
1073         return Variant(vr);
1074 	}
1075 	
1076     /// handle continue statements
1077 	Variant visitContinueStatementNode(ContinueStatementNode csnode)
1078 	{
1079         auto vr = VisitResult(ScriptAny.UNDEFINED);
1080         vr.continueFlag = true;
1081         vr.labelName = csnode.label;
1082         return Variant(vr);
1083 	}
1084 	
1085     /// handles return statements
1086 	Variant visitReturnStatementNode(ReturnStatementNode rsnode)
1087 	{
1088 		VisitResult vr = VisitResult(ScriptAny.UNDEFINED);
1089         if(rsnode.expressionNode !is null)
1090         {
1091             vr = rsnode.expressionNode.accept(this).get!VisitResult;
1092             if(vr.exception !is null)
1093             {
1094                 return Variant(vr);
1095             }
1096         }
1097         vr.returnFlag = true;
1098         return Variant(vr);
1099 	}
1100 	
1101     /// handle function declarations
1102 	Variant visitFunctionDeclarationStatementNode(FunctionDeclarationStatementNode fdsnode)
1103 	{
1104 		auto func = new ScriptFunction(fdsnode.name, fdsnode.argNames, fdsnode.statementNodes, _currentContext);
1105         immutable okToDeclare = _currentContext.declareVariableOrConst(fdsnode.name, ScriptAny(func), false);
1106         VisitResult vr = VisitResult(ScriptAny.UNDEFINED);
1107         if(!okToDeclare)
1108         {
1109             vr.exception = new ScriptRuntimeException("Cannot redeclare variable or const " ~ fdsnode.name 
1110                 ~ " with a function declaration");
1111         }
1112         return Variant(vr);
1113 	}
1114 	
1115     /// handles throw statements
1116 	Variant visitThrowStatementNode(ThrowStatementNode tsnode)
1117 	{
1118 		auto vr = tsnode.expressionNode.accept(this).get!VisitResult;
1119         if(vr.exception !is null)
1120         {
1121             return Variant(vr);
1122         }
1123         vr.exception = new ScriptRuntimeException("Uncaught script exception");
1124         vr.exception.thrownValue = vr.result;
1125         vr.result = ScriptAny.UNDEFINED;
1126         return Variant(vr);
1127 	}
1128 	
1129     /// handle try catch block statements
1130 	Variant visitTryCatchBlockStatementNode(TryCatchBlockStatementNode tcbsnode)
1131 	{
1132 		auto vr = tcbsnode.tryBlockNode.accept(this).get!VisitResult;
1133         // if there was an exception we need to start a new context and set it as a local variable
1134         if(vr.exception !is null)
1135         {
1136             auto oldContext = _currentContext; // @suppress(dscanner.suspicious.unmodified)
1137             _currentContext = new Context(_currentContext, "<catch>");
1138             if(vr.exception.thrownValue != ScriptAny.UNDEFINED)
1139                 _currentContext.forceSetVarOrConst(tcbsnode.exceptionName, vr.exception.thrownValue, false);
1140             else 
1141                 _currentContext.forceSetVarOrConst(tcbsnode.exceptionName, ScriptAny(vr.exception.message), false);
1142             vr.exception = null;
1143             // if another exception is thrown in the catch block, it will propagate through this return value
1144             vr = tcbsnode.catchBlockNode.accept(this).get!VisitResult;
1145             _currentContext = oldContext;
1146         }
1147         return Variant(vr);
1148 	}
1149 	
1150     /// handle delete statement
1151 	Variant visitDeleteStatementNode(DeleteStatementNode dsnode)
1152 	{
1153 		auto vr = dsnode.memberAccessOrArrayIndexNode.accept(this).get!VisitResult;
1154         // TODO handle array
1155         if(vr.accessType != VisitResult.AccessType.OBJECT_ACCESS)
1156         {
1157             vr.exception = new ScriptRuntimeException("Invalid operand for delete operator");
1158             return Variant(vr);
1159         }
1160         if(vr.objectToAccess.isObject)
1161         {
1162             auto obj = vr.objectToAccess.toValue!ScriptObject;
1163             obj.dictionary.remove(vr.memberOrVarToAccess);
1164         }
1165         vr.result = ScriptAny.UNDEFINED;
1166         return Variant(vr);
1167 	}
1168 	
1169     /// handle class declaration
1170 	Variant visitClassDeclarationStatementNode(ClassDeclarationStatementNode cdsnode)
1171 	{
1172 		VisitResult vr;
1173         // generate class
1174         try 
1175         {
1176             vr.result = cdsnode.classDefinition.create(_currentContext);
1177         }
1178         catch (ScriptRuntimeException ex)
1179         {
1180             vr.exception = ex;
1181             return Variant(vr);
1182         }
1183         auto ctor = vr.result;
1184         // first try to assign the constructor as a local function
1185         immutable ok = _currentContext.declareVariableOrConst(cdsnode.classDefinition.className, 
1186                 ctor, false);
1187         if(!ok)
1188         {
1189             vr.exception = new ScriptRuntimeException("Class declaration " ~ cdsnode.classDefinition.className 
1190                 ~ " may not overwrite local variable or const");
1191             return Variant(vr);
1192         }
1193 		
1194         return Variant(VisitResult(ScriptAny.UNDEFINED)); // everything was ok
1195 	}
1196 	
1197     /// handle super constructor calls TODO use super as reference to base class with correct "this"
1198 	Variant visitSuperCallStatementNode(SuperCallStatementNode scsnode)
1199 	{
1200         auto vr = scsnode.classConstructorToCall.accept(this).get!VisitResult;
1201         if(vr.exception !is null)
1202             return Variant(vr);
1203         if(vr.result.type != ScriptAny.Type.FUNCTION)
1204         {
1205             vr.exception = new ScriptRuntimeException("Invalid super call");
1206             return Variant(vr);
1207         }
1208         auto fn = vr.result.toValue!ScriptFunction;
1209         // get the args
1210         ScriptAny[] args = [];
1211         foreach(expression ; scsnode.argExpressionNodes)
1212         {
1213             vr = expression.accept(this).get!VisitResult;
1214             if(vr.exception !is null)
1215                 return Variant(vr);
1216             args ~= vr.result;
1217         }
1218         // get the "this" out of the context
1219         bool dontCare; // @suppress(dscanner.suspicious.unmodified)
1220         auto thisObjPtr = _currentContext.lookupVariableOrConst("this", dontCare);
1221         if(thisObjPtr == null)
1222         {
1223             vr.exception = new ScriptRuntimeException("Invalid `this` object in super call");
1224             return Variant(vr);
1225         }
1226         // TODO return values in constructors with expressions are invalid
1227         vr = callFn(fn, *thisObjPtr, args, false);
1228         return Variant(vr);
1229 	}
1230 	
1231     /// handle expression statements
1232 	Variant visitExpressionStatementNode(ExpressionStatementNode esnode)
1233 	{
1234 		VisitResult vr;
1235         if(esnode.expressionNode !is null)
1236             vr = esnode.expressionNode.accept(this).get!VisitResult;
1237         vr.result = ScriptAny.UNDEFINED; // they should never return a result
1238         return Variant(vr); // caller will handle any exception
1239 	}
1240 	
1241 package:
1242 	/// holds information from visiting nodes TODO redesign this as a union
1243 	struct VisitResult
1244 	{
1245 		enum AccessType { NO_ACCESS=0, VAR_ACCESS, ARRAY_ACCESS, OBJECT_ACCESS }
1246 
1247 		this(T)(T val)
1248 		{
1249 			result = ScriptAny(val);
1250 		}
1251 
1252 		this(T : ScriptAny)(T val)
1253 		{
1254 			result = val;
1255 		}
1256 
1257 		ScriptAny result;
1258 
1259 		AccessType accessType;
1260 		ScriptAny objectToAccess;
1261 		string memberOrVarToAccess;
1262 		size_t indexToAccess;
1263 
1264 		bool returnFlag, breakFlag, continueFlag;
1265 		string labelName;
1266 		ScriptRuntimeException exception;
1267 	}
1268 
1269 private:
1270 
1271     VisitResult callFn(ScriptFunction func, ScriptAny thisObj, ScriptAny[] args, bool returnThis = false)
1272 	{
1273 		VisitResult vr;
1274 		if(returnThis)
1275 		{
1276 			if(!thisObj.isObject)
1277 				thisObj = new ScriptObject(func.functionName, func["prototype"].toValue!ScriptObject, null);
1278 		}
1279 		// handle script functions
1280 		if(func.type == ScriptFunction.Type.SCRIPT_FUNCTION)
1281 		{
1282 			auto prevContext = _currentContext; // @suppress(dscanner.suspicious.unmodified)
1283 			_currentContext = new Context(func.closure, func.functionName);
1284 			// set args as locals
1285 			for(size_t i = 0; i < func.argNames.length; ++i)
1286 			{
1287 				if(i < args.length)
1288 					_currentContext.forceSetVarOrConst(func.argNames[i], args[i], false);
1289 				else
1290 					_currentContext.forceSetVarOrConst(func.argNames[i], ScriptAny.UNDEFINED, false);
1291 			}
1292 			// put all arguments inside "arguments" local
1293 			_currentContext.forceSetVarOrConst("arguments", ScriptAny(args), false);
1294 			// set up "this" local
1295 			_currentContext.forceSetVarOrConst("this", thisObj, true);
1296 			foreach(statement ; func.statementNodes)
1297 			{
1298 				vr = statement.accept(this).get!VisitResult;
1299 				if(vr.breakFlag) // TODO add enum stack to parser to prevent validation of breaks inside functions without loop
1300 					vr.breakFlag = false;
1301 				if(vr.continueFlag) // likewise
1302 					vr.continueFlag = false;
1303 				if(vr.returnFlag || vr.exception !is null)
1304 				{
1305 					if(vr.exception !is null)
1306 						vr.exception.scriptTraceback ~= statement;
1307 					vr.returnFlag = false;
1308 					break;
1309 				}
1310 			}
1311 			if(returnThis)
1312 			{
1313 				bool _; // @suppress(dscanner.suspicious.unmodified)
1314 				immutable thisPtr = cast(immutable)_currentContext.lookupVariableOrConst("this", _);
1315 				if(thisPtr != null)
1316 					vr.result = *thisPtr;
1317 			}
1318 			_currentContext = prevContext;
1319 			return vr;
1320 		}
1321 		else 
1322 		{
1323 			ScriptAny returnValue;
1324 			auto nfe = NativeFunctionError.NO_ERROR;
1325 			if(func.type == ScriptFunction.Type.NATIVE_FUNCTION)
1326 			{
1327 				auto nativefn = func.nativeFunction;
1328 				returnValue = nativefn(_currentContext, &thisObj, args, nfe);
1329 			}
1330 			else
1331 			{
1332 				auto nativedg = func.nativeDelegate;
1333 				returnValue = nativedg(_currentContext, &thisObj, args, nfe);
1334 			}
1335 			if(returnThis)
1336 				vr.result = thisObj;
1337 			else
1338 				vr.result = returnValue;
1339 			// check NFE
1340 			final switch(nfe)
1341 			{
1342 			case NativeFunctionError.NO_ERROR:
1343 				break;
1344 			case NativeFunctionError.WRONG_NUMBER_OF_ARGS:
1345 				vr.exception = new ScriptRuntimeException("Incorrect number of args to native method or function");
1346 				break;
1347 			case NativeFunctionError.WRONG_TYPE_OF_ARG:
1348 				vr.exception = new ScriptRuntimeException("Wrong argument type to native method or function");
1349 				break;
1350 			case NativeFunctionError.RETURN_VALUE_IS_EXCEPTION:
1351 				vr.exception = new ScriptRuntimeException(returnValue.toString());
1352 				break;
1353 			}
1354 
1355 			return vr;
1356 		}
1357 	}
1358 
1359 	VisitResult convertExpressionsToArgs(ExpressionNode[] exprs, out ScriptAny[] args)
1360 	{
1361 		args = [];
1362 		VisitResult vr;
1363 		foreach(expr ; exprs)
1364 		{
1365 			vr = expr.accept(this).get!VisitResult;
1366 			if(vr.exception !is null)
1367 			{
1368 				args = [];
1369 				return vr;
1370 			}
1371 			args ~= vr.result;
1372 		}
1373 		return vr;
1374 	}
1375 
1376 	VisitResult getObjectProperty(ScriptObject obj, in string propName)
1377 	{
1378 		VisitResult vr;
1379 		ScriptObject objToSearch = obj;
1380 		while(objToSearch !is null)
1381 		{
1382 			if(propName in objToSearch.getters)
1383 			{
1384 				vr = callFn(objToSearch.getters[propName], ScriptAny(obj), [], false);
1385 				return vr;
1386 			}
1387 			objToSearch = objToSearch.prototype;
1388 		}
1389 		vr.exception = new ScriptRuntimeException("Object " ~ obj.toString() ~ " has no get property `" ~ propName ~ "`");
1390 		return vr;
1391 	}
1392 
1393 	VisitResult handleArrayReassignment(Token opToken, ScriptAny arr, size_t index, ScriptAny value)
1394 	{
1395 		VisitResult vr;
1396 		if(arr.type != ScriptAny.Type.ARRAY)
1397 		{
1398 			vr.exception = new ScriptRuntimeException("Cannot assign to index of non-array");
1399 			return vr;
1400 		}
1401 		auto scriptArray = arr.toValue!ScriptArray;
1402 		if(index >= scriptArray.length)
1403 		{
1404 			vr.exception = new ScriptRuntimeException("Out of bounds array assignment");
1405 			return vr;
1406 		}
1407 
1408 		switch(opToken.type)
1409 		{
1410 		case Token.Type.ASSIGN:
1411 			scriptArray.array[index] = value;
1412 			break;
1413 		case Token.Type.PLUS_ASSIGN:
1414 			scriptArray.array[index] = scriptArray.array[index] + value;
1415 			break;
1416 		case Token.Type.DASH_ASSIGN:
1417 			scriptArray.array[index] = scriptArray.array[index] - value;
1418 			break;
1419 		default:
1420 			throw new Exception("Unhandled assignment operator");
1421 		}
1422 		vr.result = scriptArray.array[index];
1423 		return vr;
1424 	}
1425 
1426 	VisitResult handleObjectReassignment(Token opToken, ScriptAny objToAccess, in string index, ScriptAny value)
1427 	{
1428 		VisitResult vr;
1429 		if(!objToAccess.isObject)
1430 		{
1431 			vr.exception = new ScriptRuntimeException("Cannot index non-object");
1432 			return vr;
1433 		}
1434 		auto obj = objToAccess.toValue!ScriptObject;
1435 		// we may need the original value
1436 		ScriptAny originalValue, newValue;
1437 		if(obj.hasGetter(index))
1438 		{
1439 			// if getter with no setter this is an error
1440 			if(!obj.hasSetter(index))
1441 			{
1442 				vr.exception = new ScriptRuntimeException("Object " ~ obj.toString() ~ " has no set property `" ~ index ~ "`");
1443 				return vr;
1444 			}
1445 			vr = getObjectProperty(obj, index);
1446 			if(vr.exception !is null)
1447 				return vr;
1448 			originalValue = vr.result;
1449 		}
1450 		else
1451 		{
1452 			originalValue = obj[index];
1453 		}
1454 
1455 		switch(opToken.type)
1456 		{
1457 		case Token.Type.ASSIGN:
1458 			newValue = value;
1459 			break;
1460 		case Token.Type.PLUS_ASSIGN:
1461 			newValue = originalValue + value;
1462 			break;
1463 		case Token.Type.DASH_ASSIGN:
1464 			newValue = originalValue - value;
1465 			break;
1466 		default:
1467 			throw new Exception("Unhandled assignment operator");
1468 		}
1469 		if(obj.hasSetter(index))
1470 		{
1471 			setObjectProperty(obj, index, newValue);
1472 			if(obj.hasGetter(index))
1473 				vr.result = newValue;
1474 		}
1475 		else
1476 		{
1477 			obj.assignField(index, newValue);
1478 			vr.result = newValue;
1479 		}
1480 		return vr;
1481 	}
1482 
1483 	VisitResult handleVarDeclaration(in string qual, in string varName, ScriptAny value)
1484 	{
1485 		VisitResult vr;
1486 		bool ok = false;
1487 		string msg = "";
1488 		if(qual == "var")
1489 		{
1490 			ok = _globalContext.declareVariableOrConst(varName, value, false);
1491 			if(!ok)
1492 				msg = "Unable to redeclare global " ~ varName;
1493 		}
1494 		else if(qual == "let")
1495 		{
1496 			ok = _currentContext.declareVariableOrConst(varName, value, false);
1497 			if(!ok)
1498 				msg = "Unable to redeclare local variable " ~ varName;
1499 		}
1500 		else if(qual == "const")
1501 		{
1502 			ok = _currentContext.declareVariableOrConst(varName, value, true);
1503 			if(!ok)
1504 				msg = "Unable to redeclare local const " ~ varName;
1505 		}
1506 		if(!ok)
1507 			vr.exception = new ScriptRuntimeException(msg);
1508 		return vr;
1509 	}
1510 
1511 	VisitResult handleVarReassignment(Token opToken, in string varName, ScriptAny value)
1512 	{
1513 		bool isConst; // @suppress(dscanner.suspicious.unmodified)
1514 		auto ptr = _currentContext.lookupVariableOrConst(varName, isConst);
1515 		VisitResult vr;
1516 		if(isConst)
1517 			vr.exception = new ScriptRuntimeException("Unable to reassign const " ~ varName);
1518 		else if(ptr == null)
1519 			vr.exception = new ScriptRuntimeException("Unable to reassign undefined variable " ~ varName);
1520 		
1521 		if(vr.exception)
1522 			return vr;
1523 
1524 		switch(opToken.type)
1525 		{
1526 		case Token.Type.ASSIGN:
1527 			*ptr = value;
1528 			break;
1529 		case Token.Type.PLUS_ASSIGN:
1530 			*ptr = *ptr + value;
1531 			break;
1532 		case Token.Type.DASH_ASSIGN:
1533 			*ptr = *ptr - value;
1534 			break;
1535 		default:
1536 			throw new Exception("Unhandled reassignment operator");
1537 		}
1538 		vr.result = *ptr;
1539 		return vr;
1540 	}
1541 
1542 	VisitResult setObjectProperty(ScriptObject obj, string propName, ScriptAny value)
1543 	{
1544 		VisitResult vr;
1545         auto objectToSearch = obj;
1546         while(objectToSearch !is null)
1547         {
1548             if(propName in objectToSearch.setters)
1549 			{
1550 				vr = callFn(objectToSearch.setters[propName], ScriptAny(obj), [value], false);
1551 				return vr;
1552 			}
1553             objectToSearch = objectToSearch.prototype;
1554         }
1555 		vr.exception = new ScriptRuntimeException("Object " ~ obj.toString() ~ " has no set property `" ~ propName ~ "`");
1556         return vr;
1557 	}
1558 
1559     Context _globalContext;
1560     Context _currentContext;
1561 }
1562