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