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