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 /// Generator class
32 class ScriptGenerator : Generator!ScriptAny
33 {
34     /// ctor
35     this(Environment env, ScriptFunction func, ScriptAny[] args, ScriptAny thisObj = ScriptAny.UNDEFINED)
36     {
37         // first get the thisObj
38         bool _; // @suppress(dscanner.suspicious.unmodified)
39         if(thisObj == ScriptAny.UNDEFINED)
40         {
41             if(func.boundThis)
42             {
43                 thisObj = func.boundThis;
44             }
45             if(func.closure && func.closure.variableOrConstExists("this"))
46             {
47                 thisObj = *func.closure.lookupVariableOrConst("this", _);
48             }
49             else if(env.variableOrConstExists("this"))
50             {
51                 thisObj = *env.lookupVariableOrConst("this", _);
52             }
53             // else it's undefined and that's ok
54         }
55 
56         // next get a VM copy that will live in the following closure
57         if(env.g.interpreter is null)
58             throw new Exception("Global environment has null interpreter");
59         auto vm = env.g.interpreter.vm.copy();
60 
61         _name = func.functionName;
62 
63         super({
64             _returnValue = vm.runFunction(func, thisObj, args, ScriptAny(new ScriptFunction("yield",
65                     &this.native_yield)));
66         });
67     }
68 
69     override string toString() const 
70     {
71         return "Generator " ~ _name;
72     }
73 package:
74 
75     ScriptAny native_yield(Environment env, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError nfe)
76     {
77         if(args.length < 1)
78             .yield!ScriptAny(ScriptAny());
79         else
80             .yield!ScriptAny(args[0]);
81         auto result = this._yieldValue;
82         this._yieldValue = ScriptAny.UNDEFINED;
83         return result;
84     }
85 
86 private:
87     string _name;
88     bool _markedAsFinished = false;
89     ScriptAny _yieldValue;
90     ScriptAny _returnValue;
91 }
92 
93 /**
94  * Initializes the public Generator constructor. Generator functions are a first class language
95  * feature so this is unnecessary. See https://pillager86.github.io/dmildew/Generator.html for how
96  * to use the constructor and methods in Mildew.
97  */
98 void initializeGeneratorLibrary(Interpreter interpreter)
99 {
100     ScriptAny ctor = new ScriptFunction("Generator", &native_Generator_ctor, true);
101     ctor["prototype"] = getGeneratorPrototype();
102     ctor["prototype"]["constructor"] = ctor;
103     interpreter.forceSetGlobal("Generator", ctor, false);
104 }
105 
106 private ScriptObject _generatorPrototype;
107 
108 /// Gets the Generator prototype. The VM will eventually need this.
109 ScriptObject getGeneratorPrototype()
110 {
111     if(_generatorPrototype is null)
112     {
113         _generatorPrototype = new ScriptObject("Generator", null);
114         _generatorPrototype.addGetterProperty("name", new ScriptFunction("Generator.prototype.name",
115                 &native_Generator_p_name));
116         _generatorPrototype["next"] = new ScriptFunction("Generator.prototype.next",
117                 &native_Generator_next);
118         _generatorPrototype["return"] = new ScriptFunction("Generator.prototype.return",
119                 &native_Generator_return);
120         _generatorPrototype.addGetterProperty("returnValue", new ScriptFunction("Generator.prototype.returnValue",
121                 &native_Generator_p_returnValue));
122     }
123     return _generatorPrototype;
124 }
125 
126 private ScriptAny native_Generator_ctor(Environment env, ScriptAny* thisObj,
127                                         ScriptAny[] args, ref NativeFunctionError nfe)
128 {
129     if(args.length < 1 || args[0].type != ScriptAny.Type.FUNCTION)
130     {
131         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
132         return ScriptAny("First argument to new Generator() must exist and be a Function");
133     }
134     if(!thisObj.isObject)
135     {
136         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
137         return ScriptAny.UNDEFINED;
138     }
139     auto obj = thisObj.toValue!ScriptObject;
140     obj.nativeObject = new ScriptGenerator(env, args[0].toValue!ScriptFunction, args[1..$]);
141     return ScriptAny.UNDEFINED;
142 }
143 
144 private ScriptAny native_Generator_p_name(Environment env, ScriptAny* thisObj,
145                                         ScriptAny[] args, ref NativeFunctionError nfe)
146 {
147     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
148     if(thisGen is null)
149     {
150         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
151         return ScriptAny.UNDEFINED;
152     }
153     return ScriptAny(thisGen._name);
154 }
155 
156 /// This is public for opIter
157 ScriptAny native_Generator_next(Environment env, ScriptAny* thisObj,
158                                         ScriptAny[] args, ref NativeFunctionError nfe)
159 {
160     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
161     if(thisGen is null)
162     {
163         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
164         return ScriptAny.UNDEFINED;
165     }
166     auto valueToYield = args.length > 0 ? args[0] : ScriptAny.UNDEFINED; // @suppress(dscanner.suspicious.unmodified)
167     thisGen._yieldValue = valueToYield;
168 
169     if(thisGen._markedAsFinished)
170     {
171         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
172         return ScriptAny("Cannot call next on a finished Generator");
173     }
174     if(!thisGen.empty)
175     {
176         auto obj = new ScriptObject("iteration", null);
177         obj["done"] = ScriptAny(false);
178         obj["value"] = thisGen.front();
179         try 
180         {
181             thisGen.popFront();
182         }
183         catch(ScriptRuntimeException ex)
184         {
185             thisGen._markedAsFinished = true;
186             return ScriptAny.UNDEFINED;
187         }
188         return ScriptAny(obj);
189     }
190     else
191     {
192         thisGen._markedAsFinished = true;
193         auto obj = new ScriptObject("iteration", null);
194         obj["done"] = ScriptAny(true);
195         return ScriptAny(obj);
196     }
197 }
198 
199 private ScriptAny native_Generator_return(Environment env, ScriptAny* thisObj,
200                                           ScriptAny[] args, ref NativeFunctionError nfe)
201 {
202     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
203     if(thisGen is null)
204     {
205         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
206         return ScriptAny.UNDEFINED;
207     }
208     ScriptAny retVal = args.length > 0 ? args[0] : ScriptAny.UNDEFINED; // @suppress(dscanner.suspicious.unmodified)
209     if(thisGen._markedAsFinished)
210     {
211         nfe = NativeFunctionError.RETURN_VALUE_IS_EXCEPTION;
212         return ScriptAny("Cannot call return on a finished Generator");
213     }
214     while(!thisGen.empty)
215     {
216         try 
217         {
218             thisGen.popFront();
219         }
220         catch(ScriptRuntimeException ex)
221         {
222             thisGen._markedAsFinished = true;
223             return ScriptAny.UNDEFINED;
224         }
225     }
226     thisGen._markedAsFinished = true;
227     auto obj = new ScriptObject("iteration", null);
228     obj["value"] = retVal;
229     obj["done"] = ScriptAny(true);
230     return ScriptAny(obj);
231 }
232 
233 // we are not adding throw I've ripped up the Lexer enough as it is
234 
235 private ScriptAny native_Generator_p_returnValue(Environment env, ScriptAny* thisObj,
236                                                ScriptAny[] args, ref NativeFunctionError nfe)
237 {
238     auto thisGen = thisObj.toNativeObject!ScriptGenerator;
239     if(thisGen is null)
240     {
241         nfe = NativeFunctionError.WRONG_TYPE_OF_ARG;
242         return ScriptAny.UNDEFINED;
243     }
244     return thisGen._returnValue;
245 }
246