1 /**
2  * This module implements the Context class.
3  */
4 module mildew.context;
5 
6 import std.container.rbtree;
7 
8 import mildew.types.any;
9 import mildew.interpreter: Interpreter;
10 
11 private alias VariableTable = ScriptAny[string];
12 
13 /**
14  * Holds the variables and consts of a script stack frame. The global context can be accessed by
15  * climbing the Context.parent chain until reaching the Context whose parent is null. This allows
16  * native functions to define local and global variables. Note that calling a native function does
17  * not create a stack frame so one could write a native function that adds local variables to the
18  * stack frame where it was called.
19  */
20 class Context
21 {
22 public:
23     /**
24      * Constructs a new Context.
25      * Params:
26      *  par = The parent context, which should be null when the global context is created
27      *  nam = The name of the context. When script functions are called this is set to the name
28      *        of the function being called.
29      */
30     this(Context par = null, in string nam = "<context>")
31     {
32         _parent = par;
33         _name = nam;
34     }
35 
36     /**
37      * Constructs a global context
38      */
39     this(Interpreter interpreter)
40     {
41         _parent = null;
42         _name = "<global>";
43         _interpreter = interpreter;
44     }
45 
46     /**
47      * Attempts to look up existing variable or const throughout the stack. If found, returns a pointer to the 
48      * variable location, and if it is const, sets isConst to true. Note, this pointer should not be stored by 
49      * native functions because the variable table may be modified between function calls.
50      * Params:
51      *  name = The name of the variable to look up.
52      *  isConst = Whether or not the found variable is constant. Will remain false if variable is not found
53      * Returns:
54      *  A pointer to the located variable, or null if the variable was not found. If this value is needed for later
55      *  the caller should make a copy of the variable immediately.
56      */
57     ScriptAny* lookupVariableOrConst(in string varName, out bool isConst)
58     {
59         auto context = this;
60         while(context !is null)
61         {
62             if(varName in context._varTable)
63             {
64                 isConst = false;
65                 return (varName in context._varTable);
66             }
67             if(varName in context._constTable)
68             {
69                 isConst = true;
70                 return (varName in context._constTable);
71             }
72             context = context._parent;
73         }
74         isConst = false;
75         return null; // found nothing
76     }
77 
78     /**
79      * Removes a variable from anywhere on the Context stack it is located. This function cannot
80      * be used to unset consts.
81      * Params:
82      *  name = The name of the variable.
83      */
84     void unsetVariable(in string name)
85     {
86         auto context = this;
87         while(context !is null)
88         {
89             if(name in context._varTable)
90             {
91                 context._varTable.remove(name);
92                 return;
93             }
94             context = context._parent;
95         }
96     }
97 
98     /** 
99      * Attempt to declare and assign a new variable in the current context. Returns false if it already exists.
100      * Params:
101      *  nam = the name of the variable to set.
102      *  value = the initial value of the variable. This can be ScriptAny.UNDEFINED
103      *  isConst = whether or not the variable was declared as a const
104      * Returns:
105      *  True if the declaration was successful, otherwise false.
106      */
107     bool declareVariableOrConst(in string nam, ScriptAny value, in bool isConst)
108     {
109         if(nam in _varTable || nam in _constTable)
110             return false;
111         
112         if(isConst)
113         {
114             _constTable[nam] = value;
115         }
116         else
117         {
118             _varTable[nam] = value;
119         }
120         return true;
121     }
122 
123     /**
124      * Searches the entire Context stack for a variable starting with the current context and climbing the parent
125      * chain.
126      * Params:
127      *  name = The name of the variable to look for.
128      * Returns:
129      *  True if the variable is found, otherwise false.
130      */
131     bool variableOrConstExists(in string name)
132     {
133         auto context = this;
134         while(context !is null)
135         {
136             if(name in context._varTable)
137                 return true;
138             if(name in context._constTable)
139                 return true;
140             context = context._parent;
141         }
142         return false;
143     }
144 
145     /**
146      * Attempts to reassign a variable anywhere in the stack and returns a pointer to the variable or null
147      * if the variable doesn't exist or is const. If the failure is due to const, failedBecauseConst is
148      * set to true. Note: this pointer should not be stored by native functions due to modifications
149      * to the variable table that may invalidate it and result in undefined behavior.
150      * Params:
151      *  name = The name of the variable to reassign.
152      *  newValue = The value to assign. If this is undefined and the variable isn't const, the variable
153      *             will be deleted from the table where it is found.
154      *  failedBecauseConst = If the reassignment fails due to the variable being a const, this is set to true
155      * Returns:
156      *  A pointer to the variable in the table where it is found, or null if it was const or not located.
157      */
158     ScriptAny* reassignVariable(in string name, ScriptAny newValue, out bool failedBecauseConst)
159     {
160         bool isConst; // @suppress(dscanner.suspicious.unmodified)
161         auto scriptAnyPtr = lookupVariableOrConst(name, isConst);
162         if(scriptAnyPtr == null)
163         {
164             failedBecauseConst = false;
165             return null;
166         }
167         if(isConst)
168         {
169             failedBecauseConst = true;
170             return null;
171         }
172         *scriptAnyPtr = newValue;
173         failedBecauseConst = false;
174         return scriptAnyPtr;
175     }
176 
177     /**
178      * Force sets a variable or const no matter if the variable was declared already or is const. This is
179      * used by the host application to set globals or locals.
180      * Params:
181      *  name = The name of the variable or const
182      *  value = The value of the variable
183      *  isConst = Whether or not the variable should be considered const and unable to be overwritten by the script
184      */
185     void forceSetVarOrConst(in string name, ScriptAny value, bool isConst)
186     {
187         if(isConst)
188         {
189             _constTable[name] = value;
190         }
191         else
192         {
193             _varTable[name] = value;
194         }
195     }
196 
197     /**
198      * Forces the removal of a const or variable in the current context.
199      */
200     void forceRemoveVarOrConst(in string name)
201     {
202         if(name in _constTable)
203             _constTable.remove(name);
204         if(name in _varTable)
205             _varTable.remove(name);
206     }
207 
208     /// climb context stack until finding one without a parent
209     Context getGlobalContext()
210     {
211         Context c = this;
212         while(c._parent !is null)
213         {
214             c = c._parent;
215         }
216         return c;
217     }
218 
219     /// inserts a label into the list of valid labels
220     void insertLabel(string label)
221     {
222         _labelList.insert(label);
223     }
224 
225     /// Retrieves the interpreter object from the top level context
226     Interpreter interpreter()
227     {
228         auto search = this;
229         while(search !is null)
230         {
231             if(search._interpreter !is null)
232                 return search._interpreter;
233             search = search._parent;
234         }
235         return null;
236     }
237 
238     /// checks context stack for a label
239     bool labelExists(string label)
240     {
241         auto context = this;
242         while(context !is null)
243         {
244             if(label in context._labelList)
245                 return true;
246             context = context._parent;
247         }
248         return false;
249     }
250 
251     /// removes a label from the existing context
252     void removeLabelFromCurrent(string label)
253     {
254         _labelList.removeKey(label);
255     }
256 
257     /// returns the parent property
258     Context parent()
259     {
260         return _parent;
261     }
262 
263     /// returns the name property of the Context
264     string name() const
265     {
266         return _name;
267     }
268 
269     /// Returns a string representing the type and name
270     override string toString() const
271     {
272         return "Context: " ~ _name;
273     }
274 
275 private:
276 
277     /// parent context. null if this is the global context
278     Context _parent;
279     /// name of context
280     string _name;
281     /// holds variables
282     VariableTable _varTable;
283     /// holds consts, which can be shadowed by other consts or lets
284     VariableTable _constTable;
285     /// holds a list of labels
286     auto _labelList = new RedBlackTree!string;
287     /// Interpreter object can be held by global context
288     Interpreter _interpreter;
289 }