1 /**
2 This module implements the ScriptFunction class, which holds script defined functions as well as native D
3 functions or delegates with the correct signature.
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.types.func;
22 
23 import mildew.environment: Environment;
24 import mildew.compiler;
25 import mildew.types.any: ScriptAny;
26 import mildew.types.object: ScriptObject;
27 import mildew.vm;
28 
29 /** 
30  * When a native function or delegate encounters an error with the arguments sent,
31  * the last reference parameter should be set to the appropriate enum value.
32  * A specific exception can be thrown by setting the flag to RETURN_VALUE_IS_EXCEPTION and
33  * returning a string.
34  * If an exception is thrown directly inside a native function, the user will not be able to
35  * see a traceback of the script source code lines where the error occurred.
36  */
37 enum NativeFunctionError 
38 {
39     NO_ERROR = 0,
40     WRONG_NUMBER_OF_ARGS,
41     WRONG_TYPE_OF_ARG,
42     RETURN_VALUE_IS_EXCEPTION
43 }
44 
45 /// native function signature to be usable by scripting language
46 alias NativeFunction = ScriptAny function(Environment, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError);
47 /// native delegate signature to be usable by scripting language
48 alias NativeDelegate = ScriptAny delegate(Environment, ScriptAny* thisObj, ScriptAny[] args, ref NativeFunctionError);
49 
50 /**
51  * This class encapsulates all types of script functions including native D functions and delegates. A
52  * native function must first be wrapped in this class before it can be given to a ScriptAny assignment.
53  * When an object is created with "new FunctionName()" its __proto__ is assigned to the function's "prototype"
54  * field. This allows OOP in the scripting language and is analogous to JavaScript.
55  */
56 class ScriptFunction : ScriptObject
57 {
58     import mildew.nodes: StatementNode;
59     import mildew.interpreter: Interpreter;
60 public:
61     /// The type of function held by the object
62     enum Type { SCRIPT_FUNCTION, NATIVE_FUNCTION, NATIVE_DELEGATE }
63 
64     /**
65      * Constructs a new ScriptFunction out of a native D function.
66      * Params:
67      *  fname = The name of the function.
68      *  nfunc = The address of the native function. See NativeFunction alias for correct signature
69      *  isClass = Whether or not this function is a constructor. This information is used when printing
70      */
71     this(string fname, NativeFunction nfunc, bool isClass = false)
72     {
73         immutable tname = isClass? "class" : "function";
74         import mildew.types.bindings: getFunctionPrototype;
75         super(tname, getFunctionPrototype, null);
76         _functionName = fname;
77 		_isClass = isClass;
78         initializePrototypeProperty();
79         _type = Type.NATIVE_FUNCTION;
80         _nativeFunction = nfunc;
81     }
82 
83     /**
84      * Constructs a new ScriptFunction out of a native D delegate.
85      * Params:
86      *  fname = The name of the function.
87      *  nfunc = The address of the native delegate. See NativeDelegate alias for correct signature
88      *  isClass = Whether or not this function is a constructor. This information is used when printing
89      */
90     this(string fname, NativeDelegate ndele, bool isClass = false)
91     {
92         immutable tname = isClass? "class" : "function";
93         import mildew.types.bindings: getFunctionPrototype;
94         super(tname, getFunctionPrototype, null);
95         _functionName = fname;
96 		_isClass = isClass;
97         initializePrototypeProperty();
98         _type = Type.NATIVE_DELEGATE;
99         _nativeDelegate = ndele;
100     }
101 
102     /**
103      * Check if an object is an instance of a "class" constructor
104      */
105     static bool isInstanceOf(ScriptObject obj, ScriptFunction clazz)
106     {
107         if(obj is null || clazz is null)
108             return false;
109         auto proto = obj.prototype;
110         while(proto !is null)
111         {
112             if(proto["constructor"].toValue!ScriptFunction is clazz)
113                 return true;
114             proto = proto.prototype;
115         }
116         return false;
117     }
118 
119     /**
120      * Binds a specific this to be used no matter what. Internal users of ScriptFunction such as
121      * mildew.vm.virtualmachine.VirtualMachine must manually check the boundThis property and set this up.
122      * Unbinding is done by passing UNDEFINED as the parameter.
123      */
124     void bind(ScriptAny thisObj)
125     {
126         _boundThis = thisObj;
127     }
128 
129     /// Returns a string representing the type and name.
130     override string toString() const
131     {
132         return name ~ " " ~ _functionName;
133     }
134 
135     /// Returns the type of function stored, such as native function, delegate, or script function
136     auto type() const { return _type; }
137     /// Returns the name of the function
138     auto functionName() const { return _functionName; }
139     /// Property argNames. Note: native functions do not have this.
140     auto argNames() { return _argNames; }
141     /// Compiled form cached
142     ubyte[] compiled() { return _compiled; }
143     /// bound this property. change with bind()
144     ScriptAny boundThis() { return _boundThis; }
145     /// isGenerator property. used by various pieces
146     bool isGenerator() const { return _isGenerator; }
147 
148     alias opCmp = ScriptObject.opCmp;
149     int opCmp(const ScriptFunction other) const
150     {
151         if(_type != other._type)
152             return cast(int)_type - cast(int)other._type;
153 
154         if(_functionName < other._functionName)
155             return -1;
156         else if(_functionName > other._functionName)
157             return 1;
158 
159         if(_isClass && !other._isClass)
160             return 1;
161         else if(!_isClass && other._isClass)
162             return -1;
163         
164         if(_type == Type.SCRIPT_FUNCTION)
165         {
166             if(_compiled.length != other._compiled.length)
167             {
168                 if(_compiled.length > other._compiled.length)
169                     return 1;
170                 else if(_compiled.length < other._compiled.length)
171                     return -1;
172             }
173             else if(_compiled > other._compiled)
174                 return 1;
175             else if(_compiled < other._compiled)
176                 return -1;
177             else if(_compiled.length == 0)
178             {
179                 if(_argNames > other._argNames)
180                     return 1;
181                 else if(_argNames < other._argNames)
182                     return -1;
183 
184                 if(_statementNodes > other._statementNodes)
185                     return 1;
186                 else if(_statementNodes < other._statementNodes)
187                     return -1;
188             }
189         }
190         else if(_type == Type.NATIVE_DELEGATE)
191         {
192             if(_nativeDelegate > other._nativeDelegate)
193                 return 1;
194             else if(_nativeDelegate < other._nativeDelegate)
195                 return -1;
196         }
197         else if(_type == Type.NATIVE_FUNCTION)
198         {
199             if(_nativeFunction > other._nativeFunction)
200                 return 1;
201             else if(_nativeFunction < other._nativeFunction)
202                 return -1;
203         }
204         return 0;
205     }
206 
207     override size_t toHash() const @safe nothrow
208     {
209         if(_compiled.length > 0)
210         {
211             return typeid(_compiled).getHash(&_compiled);
212         }
213         // lacking but not sure what else to do
214         return typeid(_functionName).getHash(&_functionName);
215     }
216 
217     alias opEquals = ScriptObject.opEquals;
218     bool opEquals(ScriptFunction other) const
219     {
220         return opCmp(other) == 0;
221     }
222 
223     /// ct property
224     ConstTable constTable() { return _constTable; }
225     /// ct property
226     ConstTable constTable(ConstTable ct) { return _constTable = ct; }
227 
228 package(mildew):
229 
230     /**
231      * Constructor for functions created from compilation of statements.
232      */
233     this(string fnname, string[] args, ubyte[] bc, bool isClass = false, bool isGenerator = false, ConstTable ct = null)
234     {
235         import mildew.types.bindings: getFunctionPrototype;
236         immutable tname = isClass? "class" : "function";
237         super(tname, getFunctionPrototype(), null);
238         _functionName = fnname;
239         _argNames = args;
240         _compiled = bc;
241         _isClass = isClass;
242         _isGenerator = isGenerator;
243         _constTable = ct;
244         initializePrototypeProperty();
245         _type = Type.SCRIPT_FUNCTION;
246     }
247 
248     /**
249      * Method to copy fresh compiled functions with the correct environment
250      */
251     ScriptFunction copyCompiled(Environment env)
252     {
253         auto newFunc = new ScriptFunction(_functionName, _argNames, _compiled, _isClass, _isGenerator, _constTable);
254         newFunc._closure = env;
255         return newFunc;
256     }
257 
258     // must check type before using these properties or one gets an exception
259 
260     /// get the native function ONLY if it is one
261     auto nativeFunction()
262     {
263         if(_type == Type.NATIVE_FUNCTION)
264             return _nativeFunction;
265         else
266             throw new Exception("This is not a native function");
267     }
268 
269     /// get the delegate only if it is one
270     auto nativeDelegate()
271     {
272         if(_type == Type.NATIVE_DELEGATE)
273             return _nativeDelegate;
274         else
275             throw new Exception("This is not a native delegate");
276     }
277 
278     /// Sets the function name
279     auto functionName(in string fnName) { return _functionName = fnName; }
280 
281     /// Property statementNodes
282     auto statementNodes() { return _statementNodes; }
283 
284 	/// Property get closure
285 	auto closure() { return _closure; }
286     /// Property set closure
287     auto closure(Environment c) { return _closure = c; }
288 
289     /// Property isClass
290     auto isClass() const { return _isClass; }
291 
292     /// used by the parser for missing constructors in classes that don't extend
293     static deprecated ScriptFunction emptyFunction(in string name, bool isClass)
294     {
295         return new ScriptFunction(name, &native_EMPTY_FUNCTION, isClass);
296     }
297 
298 private:
299     Type _type;
300     string _functionName;
301     string[] _argNames;
302     StatementNode[] _statementNodes;
303     ScriptAny _boundThis;
304 	Environment _closure = null;
305 	bool _isClass = false;
306     bool _isGenerator = false;
307     ConstTable _constTable;
308     union 
309     {
310         NativeFunction _nativeFunction;
311         NativeDelegate _nativeDelegate;
312     }
313 
314     void initializePrototypeProperty()
315     {
316         _dictionary["prototype"] = ScriptAny(new ScriptObject("Object", null));
317         _dictionary["prototype"]["constructor"] = ScriptAny(this);
318     }
319 
320     ubyte[] _compiled;
321 
322 }
323 
324 deprecated private ScriptAny native_EMPTY_FUNCTION(Environment e, ScriptAny* thisObj, ScriptAny[] args, 
325                                         ref NativeFunctionError nfe)
326 {
327     return ScriptAny.UNDEFINED;
328 }
329