1 /**
2 This module implements Generators for the Mildew scripting language.
3 See https://pillager86.github.io/dmildew/Generator.html for more details.
4 
5 ────────────────────────────────────────────────────────────────────────────────
6 
7 Copyright (C) 2021 pillager86.rf.gd
8 
9 This program is free software: you can redistribute it and/or modify it under 
10 the terms of the GNU General Public License as published by the Free Software 
11 Foundation, either version 3 of the License, or (at your option) any later 
12 version.
13 
14 This program is distributed in the hope that it will be useful, but WITHOUT ANY
15 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
16 PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17 
18 You should have received a copy of the GNU General Public License along with 
19 this program.  If not, see <https://www.gnu.org/licenses/>.
20 */
21 module mildew.stdlib.generator;
22 
23 import std.concurrency;
24 
25 import mildew.environment;
26 import mildew.exceptions;
27 import mildew.interpreter;
28 import mildew.types;
29 import mildew.vm;
30 
31 /**
32  * The Generator class implementation.
33  */
34 class ScriptGenerator : Generator!ScriptAny
35 {
36     /// ctor
37     this(Environment env, ScriptFunction func, ScriptAny[] args, ScriptAny thisObj = ScriptAny.UNDEFINED)
38     {
39         // first get the thisObj
40         bool _; // @suppress(dscanner.suspicious.unmodified)
41         if(thisObj == ScriptAny.UNDEFINED)
42         {
43             if(func.boundThis)
44             {
45                 thisObj = func.boundThis;
46             }
47             if(func.closure && func.closure.variableOrConstExists("this"))
48             {
49                 thisObj = *func.closure.lookupVariableOrConst("this", _);
50             }
51             else if(env.variableOrConstExists("this"))
52             {
53                 thisObj = *env.lookupVariableOrConst("this", _);
54             }
55             // else it's undefined and that's ok
56         }
57 
58         // next get a VM copy that will live in the following closure
59         if(env.g.interpreter is null)
60             throw new Exception("Global environment has null interpreter");
61         
62         auto parentVM = env.g.interpreter.vm;
63         auto childVM = parentVM.copy();
64 
65         _name = func.functionName;
66 
67         super({
68             ScriptAny[string] map;
69             map["__yield__"] = ScriptAny(new ScriptFunction("yield", &this.native_yield));
70             map["yield"] = map["__yield__"];
71             try 
72             {
73                 _returnValue = childVM.runFunction(func, thisObj, args, map);
74             }
75             catch(ScriptRuntimeException ex)
76             {
77                 this._markedAsFinished = true;
78                 parentVM.setException(ex);
79             }
80         });
81     }
82 
83     override string toString() const 
84     {
85         return "Generator " ~ _name;
86     }
87 package:
88 
89     ScriptAny native_yield(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
90     {
91         if(args.length < 1)
92             .yield!ScriptAny(ScriptAny());
93         else
94             .yield!ScriptAny(args[0]);
95         auto result = this._yieldValue;
96         this._yieldValue = ScriptAny.UNDEFINED;
97         if(_excFlag)
98         {
99             auto exc = _excFlag; // @suppress(dscanner.suspicious.unmodified)
100             _excFlag = null;
101             throw exc;
102         }
103         return result;
104     }
105 
106 private:
107     string _name;
108     bool _markedAsFinished = false;
109     ScriptRuntimeException _excFlag;
110     ScriptAny _yieldValue;
111     ScriptAny _returnValue;
112 }
113 
114 /**
115  * Initializes the public Generator constructor. Generator functions are a first class language
116  * feature so this is unnecessary. See https://pillager86.github.io/dmildew/Generator.html for how
117  * to use the constructor and methods in Mildew.
118  * Params:
119  *  interpreter = The Interpreter instance to load the Generator constructor into.
120  */
121 void initializeGeneratorLibrary(Interpreter interpreter)
122 {
123     ScriptAny ctor = new ScriptFunction("Generator", &native_Generator_ctor, true);
124     ctor["prototype"] = getGeneratorPrototype();
125     ctor["prototype"]["constructor"] = ctor;
126     interpreter.forceSetGlobal("Generator", ctor, false);
127 }
128 
129 private ScriptObject _generatorPrototype;
130 
131 /// Gets the Generator prototype. The VM uses this.
132 package(mildew) ScriptObject getGeneratorPrototype()
133 {
134     if(_generatorPrototype is null)
135     {
136         _generatorPrototype = new ScriptObject("Generator", null);
137         _generatorPrototype.addGetterProperty("name", new ScriptFunction("Generator.prototype.name",
138                 &native_Generator_p_name));
139         _generatorPrototype["next"] = new ScriptFunction("Generator.prototype.next",
140                 &native_Generator_next);
141         _generatorPrototype["return"] = new ScriptFunction("Generator.prototype.return",
142                 &native_Generator_return);
143         _generatorPrototype["throw"] = new ScriptFunction("Generator.prototype.throw",
144                 &native_Generator_throw);
145         _generatorPrototype.addGetterProperty("returnValue", new ScriptFunction("Generator.prototype.returnValue",
146                 &native_Generator_p_returnValue));
147     }
148     return _generatorPrototype;
149 }
150 
151 private ScriptAny native_Generator_ctor(Environment env, ScriptAny* thisObj,
152                                         ScriptAny[] args, ref NativeFunctionError nfe)
153 {
154     if(args.length < 1 || args[0].type != ScriptAny.Type.FUNCTION)
155     {
156         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
157         return ScriptAny("First argument to new Generator() must exist and be a Function");
158     }
159     if(!thisObj.isObject)
160     {
161         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
162         return ScriptAny.UNDEFINED;
163     }
164     auto obj = thisObj.toValue!ScriptObject;
165     obj.nativeObject = new ScriptGenerator(env, args[0].toValue!ScriptFunction, args[1..$]);
166     return ScriptAny.UNDEFINED;
167 }
168 
169 private ScriptAny native_Generator_p_name(Environment env, ScriptAny* thisObj,
170                                           ScriptAny[] args, ref NativeFunctionError nfe)
171 {
172     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
173     if(thisGen is null)
174     {
175         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
176         return ScriptAny.UNDEFINED;
177     }
178     return ScriptAny(thisGen._name);
179 }
180 
181 /// The virtual machine uses this
182 package(mildew) ScriptAny native_Generator_next(Environment env, ScriptAny* thisObj,
183                                 ScriptAny[] args, ref NativeFunctionError nfe)
184 {
185     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
186     if(thisGen is null)
187     {
188         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
189         return ScriptAny.UNDEFINED;
190     }
191     auto valueToYield = args.length > 0 ? args[0] : ScriptAny.UNDEFINED; // @suppress(dscanner.suspicious.unmodified)
192     thisGen._yieldValue = valueToYield;
193 
194     if(thisGen._markedAsFinished)
195     {
196         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
197         return ScriptAny("Cannot call next on a finished Generator");
198     }
199     if(!thisGen.empty)
200     {
201         auto obj = new ScriptObject("iteration", null);
202         obj["done"] = ScriptAny(false);
203         obj["value"] = thisGen.front();
204         try 
205         {
206             thisGen.popFront();
207         }
208         catch(ScriptRuntimeException ex)
209         {
210             thisGen._markedAsFinished = true;
211             return ScriptAny.UNDEFINED;
212         }
213         return ScriptAny(obj);
214     }
215     else
216     {
217         thisGen._markedAsFinished = true;
218         auto obj = new ScriptObject("iteration", null);
219         obj["done"] = ScriptAny(true);
220         return ScriptAny(obj);
221     }
222 }
223 
224 private ScriptAny native_Generator_return(Environment env, ScriptAny* thisObj,
225                                           ScriptAny[] args, ref NativeFunctionError nfe)
226 {
227     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
228     if(thisGen is null)
229     {
230         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
231         return ScriptAny.UNDEFINED;
232     }
233     ScriptAny retVal = args.length > 0 ? args[0] : ScriptAny.UNDEFINED; // @suppress(dscanner.suspicious.unmodified)
234     if(thisGen._markedAsFinished)
235     {
236         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
237         return ScriptAny("Cannot call return on a finished Generator");
238     }
239     while(!thisGen.empty)
240     {
241         try 
242         {
243             thisGen.popFront();
244         }
245         catch(ScriptRuntimeException ex)
246         {
247             thisGen._markedAsFinished = true;
248             return ScriptAny.UNDEFINED;
249         }
250     }
251     thisGen._markedAsFinished = true;
252     auto obj = new ScriptObject("iteration", null);
253     obj["value"] = retVal;
254     obj["done"] = ScriptAny(true);
255     return ScriptAny(obj);
256 }
257 
258 private ScriptAny native_Generator_throw(Environment env, ScriptAny* thisObj,
259                                          ScriptAny[] args, ref NativeFunctionError nfe)
260 {
261     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
262     if(thisGen is null)
263     {
264         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
265         return ScriptAny.UNDEFINED;
266     }
267     if(args.length < 1)
268     {
269         nfe = NativeFunctionError.WRONG_NUMBER_OF_ARGS;
270         return ScriptAny.UNDEFINED;
271     }
272     if(thisGen._markedAsFinished)
273     {
274         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
275         return ScriptAny("Cannot call throw on a finished Generator");
276     }
277     auto exc = new ScriptRuntimeException("Generator exception");
278     exc.thrownValue = args[0];
279     thisGen._excFlag = exc;
280     auto result = native_Generator_next(env, thisObj, [], nfe);
281     return result;
282 }
283 
284 private ScriptAny native_Generator_p_returnValue(Environment env, ScriptAny* thisObj,
285                                                ScriptAny[] args, ref NativeFunctionError nfe)
286 {
287     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
288     if(thisGen is null)
289     {
290         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
291         return ScriptAny.UNDEFINED;
292     }
293     return thisGen._returnValue;
294 }
295