diff --git a/hscript.hxml b/hscript.hxml index e9f3c44..3c99746 100644 --- a/hscript.hxml +++ b/hscript.hxml @@ -1,2 +1,3 @@ bin/build-each.hxml +-D hscriptPos -hl bin/Test.hl \ No newline at end of file diff --git a/hscript/CustomClassHandler.hx b/hscript/CustomClassHandler.hx index 10b399c..cb8f59a 100644 --- a/hscript/CustomClassHandler.hx +++ b/hscript/CustomClassHandler.hx @@ -155,6 +155,7 @@ class CustomClassHandler implements IHScriptCustomConstructor { class CustomTemplateClass implements IHScriptCustomClassBehaviour implements IHScriptCustomAccessBehaviour { public var __interp:Interp; + public var __customClass:hscript.customclass.CustomClass; public var __allowSetGet:Bool = true; public var __custom__variables:Map; public var __real_fields:Array; diff --git a/hscript/IHScriptCustomClassBehaviour.hx b/hscript/IHScriptCustomClassBehaviour.hx index 11d24fd..e51b19d 100644 --- a/hscript/IHScriptCustomClassBehaviour.hx +++ b/hscript/IHScriptCustomClassBehaviour.hx @@ -2,6 +2,7 @@ package hscript; interface IHScriptCustomClassBehaviour { public var __interp:Interp; + public var __customClass:hscript.customclass.CustomClass; public var __custom__variables:Map; public var __allowSetGet:Bool; public var __real_fields:Array; diff --git a/hscript/Interp.hx b/hscript/Interp.hx index e0ff26d..87af3c8 100644 --- a/hscript/Interp.hx +++ b/hscript/Interp.hx @@ -28,6 +28,8 @@ */ package hscript; +import hscript.customclass.CustomClassDecl; +import hscript.customclass.CustomClass; import haxe.CallStack; import hscript.utils.UnsafeReflect; import haxe.PosInfos; @@ -65,8 +67,30 @@ class RedeclaredVar { public var depth:Int; } +@:access(hscript.customclass.CustomClass) +@:access(hscript.customclass.CustomClassDecl) @:analyzer(optimize, local_dce, fusion, user_var_fusion) class Interp { + private static var _customClasses:Map = new Map(); + + public static function getCustomClass(name:String) { + return _customClasses.get(name); + } + + public static function customClassExist(name:String):Bool { + return _customClasses.exists(name); + } + + private static function registerCustomClass(c:CustomClassDecl) { + #if CUSTOM_CLASSES + var name = c.classDecl.name; + if (c.pkg != null) { + name = c.pkg.join(".") + "." + name; + } + _customClasses.set(name, c); + #end + } + public var scriptObject(default, set):Dynamic; private var _hasScriptObject(default, null):Bool = false; private var _scriptObjectType(default, null):ScriptObjectType = SNull; @@ -137,11 +161,21 @@ class Interp { var curExpr:Expr; #end - public function new() { + var _proxy(default, null):Null = null; + var _nextCallObject(default, null):Dynamic = null; + var _inCustomClass(get, never):Bool; + + private function get__inCustomClass():Bool { + return _proxy != null; + } + + public function new(?proxy:CustomClass) { locals = new Map(); declared = []; resetVariables(); initOps(); + + this._proxy = proxy; } private function resetVariables():Void { @@ -232,6 +266,12 @@ class Interp { } public function setVar(name:String, v:Dynamic):Void { + if (_inCustomClass && _proxy.superClass != null) { + if (_proxy.superHasField(name)) { + Reflect.setProperty(_proxy.superClass, name, v); + } + } + if (allowStaticVariables && staticVariables.exists(name)) staticVariables.set(name, v); else if (allowPublicVariables && publicVariables.exists(name)) @@ -244,6 +284,28 @@ class Interp { var v = expr(e2); switch (Tools.expr(e1)) { case EIdent(id): + // Make sure setting superclass/static fields directly works. + // Also ensures property functions are accounted for. + if (_inCustomClass) { + if (_proxy.__class.hasField(id)) { + var v = expr(e2); + _proxy.__class.hset(id, v); + return v; + } else if (_proxy.superClass != null && _proxy.superHasField(id)) { + var v = expr(e2); + Reflect.setProperty(_proxy.superClass, id, v); + return v; + } else if (_proxy.hasVar(id)) { + var v = expr(e2); + _proxy.hset(id, v); + return v; + } else if (_proxy.superClass == null && _proxy.__class.classDecl.extend != null) { + // Caches the declaration to set it once superClass is created + var v = expr(e2); + _proxy.cacheSuperField(id, v); + return v; + } + } var l = locals.get(id); if (l == null) { if (_hasScriptObject && !varExists(id)) { @@ -300,6 +362,33 @@ class Interp { } // TODO case EField(e, f, s): + if(_inCustomClass) { + switch (Tools.expr(e)) { + case EIdent(id0): + if (id0 == "this") { + if (_proxy.hasField(f)) { + var v = expr(e2); + _proxy.hset(f, v); + return v; + } + if (_proxy.superClass != null) { + if (_proxy.superHasField(f)) { + var v = expr(e2); + Reflect.setProperty(_proxy.superClass, f, v); + return v; + } + } + else if(_proxy.superClass == null && _proxy.__class.classDecl.extend != null){ + // Caches the declaration to set it once superClass is created + var v = expr(e2); + _proxy.cacheSuperField(f, v); + return v; + } + } + default: + // Do nothing + } + } var obj = expr(e); if(s && obj == null) return null; v = set(obj, f, v); @@ -556,6 +645,20 @@ class Interp { return null; id = StringTools.trim(id); + // Custom Class + if (_inCustomClass) { + _nextCallObject = null; + if (id == "super") { + if (_proxy.superClass == null) { + return _proxy.superConstructor; + } else { + return _proxy.superClass; + } + } + else if (id == "this") + return _proxy; + } + if (locals.exists(id)) { var l = locals.get(id); if(l != null) @@ -571,6 +674,35 @@ class Interp { if(customClasses.exists(id)) return customClasses.get(id); + if(customClassExist(id)) + return getCustomClass(id); + + // Custom Class + if (_inCustomClass) { + // We are calling a LOCAL function from the same module. + if (_proxy.__class.hasField(id)) { + // Static access + return _proxy.__class.hget(id); + } + if (_proxy.hasFunction(id)) { + _nextCallObject = _proxy; + return _proxy.resolveField(id); + } + else if (_proxy.superHasField(id)) { + _nextCallObject = _proxy.superClass; + return Reflect.getProperty(_proxy.superClass, id); + } else { + try { + var r = _proxy.resolveField(id); + _nextCallObject = _proxy; + return r; + } catch (e:Dynamic) { + if(doException) + error(EUnknownVariable(id)); + } + } + } + if (_hasScriptObject) { // search in object if (id == "this") { @@ -639,7 +771,7 @@ class Interp { var e = e.e; #end switch (e) { - case EClass(name, fields, extend, interfaces): + case EClass(name, fields, extend, interfaces): //Not sure if leave the old system or implement the new one. if (customClasses.exists(name)) error(EAlreadyExistingClass(name)); @@ -779,7 +911,21 @@ class Interp { error(EInvalidOp(op)); } case ECall(e, params): - var args:Array = [for(p in params) expr(p)]; + var args:Array = []; // [for (p in params) expr(p)]; + for (p in params) { + switch (Tools.expr(p)) { + case EIdent(id): + var ident = resolve(id); + if (ident is CustomClass) { + var customClass:CustomClass = cast ident; // Pass the underlying superclass if exist + args.push(customClass.superClass != null ? customClass.superClass : customClass); + } else { + args.push(ident); + } + default: + args.push(expr(p)); + } + } switch (Tools.expr(e)) { case EField(e, f, s): @@ -1195,6 +1341,13 @@ class Interp { if (o == null) error(EInvalidAccess(f)); + if (o is CustomClass) { + if (cast(o, CustomClass).__class.hasField(f)) { + var clsName:String = cast(o, CustomClass).className; + error(ECustom('The field ${clsName}.${f} should be accessed in a static way.')); + } + } + var cls = Type.getClass(o); if (useRedirects && { var cl:Null = getClassType(o, cls); @@ -1246,6 +1399,13 @@ class Interp { if (o == null) error(EInvalidAccess(f)); + if (o is CustomClass) { + if (cast(o, CustomClass).__class.hasField(f)) { + var clsName:String = cast(o, CustomClass).className; + error(ECustom('The field ${clsName}.${f} should be accessed in a static way.')); + } + } + if (useRedirects && { var cl:Null = getClassType(o); cl != null && setRedirects.exists(cl) && (_setRedirect = setRedirects[cl]) != null; @@ -1288,20 +1448,140 @@ class Interp { } function fcall(o:Dynamic, f:String, args:Array):Dynamic { + // Custom logic to handle super calls to prevent infinite recursion + if(_inCustomClass) { + // Force call super function. + if(o == _proxy.superClass) + return call(o, Reflect.field(_proxy.superClass, '_HX_SUPER__${f}'), args); + } + else if (o is CustomClass) { + _nextCallObject = null; + var proxy:CustomClass = cast o; + return proxy.callFunction(f, args); + } + else if (o is CustomClassDecl) { + // Static function call + var staticClass:CustomClassDecl = cast o; + if(staticClass.hasFunction(f)) + return staticClass.callFunction(f, args); + } + if(_hasScriptObject && o == CustomClassHandler.staticHandler) { return UnsafeReflect.callMethodUnsafe(scriptObject, UnsafeReflect.field(scriptObject, "_HX_SUPER__" + f), args); } - return call(o, get(o, f), args); + + var func = get(o, f); + + // Workaround for an HTML5-specific issue. + // https://github.com/HaxeFoundation/haxe/issues/11298 + #if js + if (func == null && f == "contains") { + func = get(o, "includes"); + } + #end + + return call(o, func, args); } function call(o:Dynamic, f:Dynamic, args:Array):Dynamic { if(f == CustomClassHandler.staticHandler) { return null; } + + if (_inCustomClass) { + if (o == null && _nextCallObject != null) { + o = _nextCallObject; + } + if (f == null) { + error(EInvalidAccess(f)); + } + if (o == _proxy) { + // If we are calling this.fn(), special handling is needed to prevent the local scope from being destroyed. + // By checking `o == _proxy`, we handle BOTH fn() and this.fn(). + // super.fn() is exempt since it is not scripted. + return callThis(f, args); + } + try { + var r = Reflect.callMethod(o, f, args); + _nextCallObject = null; + return r; + } catch (e) { + error(ECustom('Script threw an exception: \n${f}')); + _nextCallObject = null; + return null; + } + } + return UnsafeReflect.callMethodSafe(o, f, args); } + /** + * Call a given function on the current proxy with the given arguments. + * Ensures that the local scope is not destroyed. + * @param fun The function to call. + * @param args The arguments to apply to that function. + * @return The result of the function call. + */ + function callThis(fun:Dynamic, args:Array):Dynamic { + // If we are calling this.fn(), special handling is needed to prevent the local scope from being destroyed. + // Store the local scope. + var capturedLocals = this.duplicate(locals); + var capturedDeclared = this.declared; + var capturedDepth = this.depth; + + this.depth++; + + // Call the function. + try { + var result = Reflect.callMethod(_proxy, fun, args); + + // Restore the local scope. + this.locals = capturedLocals; + this.declared = capturedDeclared; + this.depth = capturedDepth; + + return result; + } catch (e) { + error(ECustom('Script threw an exception: \n${fun}')); + + // Restore the local scope. + this.locals = capturedLocals; + this.declared = capturedDeclared; + this.depth = capturedDepth; + + return null; + } + } + function cnew(cl:String, args:Array):Dynamic { + // Custom Class + if (customClassExist(cl)) { + var proxy:CustomClass = new CustomClass(getCustomClass(cl), args, null, this); + return proxy; + } + if (_inCustomClass) { + if (_proxy.__class.pkg != null) { + var packagedClass = _proxy.__class.pkg.join(".") + "." + cl; + if (customClassExist(packagedClass)) { + var proxy:CustomClass = new CustomClass(getCustomClass(cl), args, null, this); + return proxy; + } + } + + if (_proxy.__class.imports != null && _proxy.__class.imports.exists(cl)) { + var importedClass = _proxy.__class.imports.get(cl).fullPath; + if (customClassExist(importedClass)) { + var proxy:CustomClass = new CustomClass(getCustomClass(cl), args, null, this); + return proxy; + } + + var c = Type.resolveClass(importedClass); + if (c != null) { + return Type.createInstance(c, args); + } + } + } + var c:Dynamic = resolve(cl); if (c == null) c = Type.resolveClass(cl); @@ -1312,4 +1592,58 @@ class Interp { return Type.createInstance(c, args); } } + + public function registerModule(module:Array) { + var pkg:Array = null; + var imports:Map = []; + for (decl in module) { + switch (decl) { + case DPackage(path): + pkg = path; + case DImport(path, _): + if (importBlocklist.contains(path.join("."))) + continue; + var last = path[path.length - 1]; + + var importedClass:CustomClassImport = { + name: last, + pkg: [for (e in path) e.trim()], + fullPath: path.join(".") + } + imports.set(importedClass.name, importedClass); + case DClass(c): + var extend = c.extend; + if (extend != null) { + var superClassPath = new Printer().typeToString(extend); + if (imports.exists(superClassPath)) { + switch (extend) { + case CTPath(_, params): + extend = CTPath(imports.get(superClassPath).pkg, params); + case _: + } + } + } + var classDecl:ClassDecl = { + name: c.name, + params: c.params, + meta: c.meta, + isPrivate: c.isPrivate, + extend: extend, + implement: c.implement, + fields: c.fields, + isExtern: c.isExtern + }; + + var customClassDecl:CustomClassDecl = { + classDecl: classDecl, + imports: imports, + pkg: pkg + } + customClassDecl.cacheFields(); + registerCustomClass(customClassDecl); + case DTypedef(_): + //TODO: maybe make this work :3 + } + } + } } diff --git a/hscript/customclass/CustomClass.hx b/hscript/customclass/CustomClass.hx new file mode 100644 index 0000000..90fb360 --- /dev/null +++ b/hscript/customclass/CustomClass.hx @@ -0,0 +1,412 @@ +package hscript.customclass; + +import hscript.utils.UnsafeReflect; +import haxe.Constraints.Function; +import hscript.Expr; +import hscript.Expr.VarDecl; +import hscript.Expr.FunctionDecl; +import hscript.Expr.FieldDecl; + +using Lambda; +using StringTools; + +/** + * Provides handlers for custom classes. + * Based on Polymod Hscript class system + * @see https://github.com/larsiusprime/polymod/tree/master/polymod/hscript/_internal + */ +@:access(hscript.customclass.CustomClassDecl) +class CustomClass implements IHScriptCustomAccessBehaviour{ + public var interp:Interp; + + public var superClass:Dynamic; + public var superConstructor(default, null):Dynamic; + + public var className(get, never):String; + + private var __class:CustomClassDecl; + private var _cachedSuperFields:Null> = null; + + private var _cachedFieldDecls:Map = null; + private var _cachedFunctionDecls:Map = null; + private var _cachedVarDecls:Map = null; + + public var __allowSetGet:Bool = true; + + private function get_className():String { + var name = ""; + if (__class.pkg != null) { + name += __class.pkg.join("."); + } + name += __class.classDecl.name; + return name; + } + + public function new(__class:CustomClassDecl, args:Array, ?extendFieldDecl:Map, ?ogInterp:Interp) { + this.__class = __class; + this.interp = new Interp(this); + + if(ogInterp != null && ogInterp.importFailedCallback != null && ogInterp.errorHandler != null) { + interp.importFailedCallback = ogInterp.importFailedCallback; + interp.errorHandler = ogInterp.errorHandler; + interp.allowStaticVariables = ogInterp.allowStaticVariables; + interp.staticVariables = ogInterp.staticVariables; + } + + buildImports(); + + if(extendFieldDecl != null) + _cachedSuperFields = extendFieldDecl; + + buildClass(); + + if(hasFunction('new')) { + buildSuperConstructor(); + callFunction('new', args); + if(this.superClass == null && this.__class.classDecl.extend != null) + this.interp.error(ECustom("super() not called")); + } + else if(__class.classDecl.extend != null) { + createSuperClass(args); + } + } + + function buildClass() { + _cachedFieldDecls = []; + _cachedFunctionDecls = []; + _cachedVarDecls = []; + + if(_cachedSuperFields == null) _cachedSuperFields = []; + + for (f in __class.classDecl.fields) { + if(f.access.contains(AStatic)) continue; // Skip static field. It's handled by CustomClassDecl.hx + _cachedFieldDecls.set(f.name, f); + switch (f.kind) { + case KFunction(fn): + _cachedFunctionDecls.set(f.name, fn); + case KVar(v): + _cachedVarDecls.set(f.name, v); + if (v.expr != null) { + var varValue = this.interp.expr(v.expr); + this.interp.variables.set(f.name, varValue); + } + } + } + + if(!_cachedSuperFields.empty()) { + for (f => v in _cachedSuperFields) { + this.hset(f, v); + } + _cachedSuperFields.clear(); + } + } + + function buildSuperConstructor() { + superConstructor = Reflect.makeVarArgs(function(args:Array) { + createSuperClass(args); + }); + } + + private function createSuperClass(args:Array = null) { + if (args == null) + args = []; + + var extendString = new Printer().typeToString(__class.classDecl.extend); + if (__class.pkg != null && extendString.indexOf(".") == -1) { + extendString = __class.pkg.join(".") + "." + extendString; + } + + if (Interp.customClassExist(extendString)) { + var abstractSuperClass:CustomClass = new CustomClass(Interp.getCustomClass(extendString), args, _cachedSuperFields, this.interp); + superClass = abstractSuperClass; + } else { + var c = Type.resolveClass('${extendString}_HSX'); + if (c == null) { + interp.error(ECustom("could not resolve super class: " + extendString)); + } + if(_cachedSuperFields != null) { + Reflect.setField(c, "__cachedFields", _cachedSuperFields); // Static field + } + + superClass = Type.createInstance(c, args); + superClass.__customClass = this; + superClass.__real_fields = Type.getInstanceFields(c); + } + } + + function buildImports() { + // TODO: implement Alias imports + var i:Int = 0; + for(_import in __class.imports) { + var importedClass = _import.fullPath; + + if(Interp.customClassExist(importedClass) && this.interp.importFailedCallback != null) { + this.interp.importFailedCallback(importedClass.split(".")); + continue; + } + + #if hscriptPos + var e:Expr = { + e: ExprDef.EImport(importedClass), + pmin: 0, + pmax: 0, + origin: this.className, + line: i + }; + #else + var e = Expr.EImport(importedClass); + #end + this.interp.expr(e); + i++; + } + } + + public function callFunction(name:String, args:Array = null):Dynamic { + var r:Dynamic = null; + + if (hasField(name)) { + var fn = getFunction(name); + var previousValues:Map = []; + var i = 0; + for (a in fn.args) { + var value:Dynamic = null; + + if (args != null && i < args.length) { + value = args[i]; + } else if (a.value != null) { + value = interp.expr(a.value); + } + // NOTE: We assign these as variables rather than locals because those get wiped when we enter the function. + if (interp.variables.exists(a.name)) { + previousValues.set(a.name, interp.variables.get(a.name)); + } + interp.variables.set(a.name, value); + i++; + } + try{ + r = interp.execute(fn.body); + } + catch(e:hscript.Expr.Error){ + // A script error occurred while executing the script function. + // Purge the function from the cache so it is not called again. + purgeFunction(name); + } + + for (a in fn.args) { + if (previousValues.exists(a.name)) { + interp.variables.set(a.name, previousValues.get(a.name)); + } else { + interp.variables.remove(a.name); + } + } + } else { + var fixedArgs = []; + // OVERRIDE CHANGE: Use _HX_SUPER__ when calling superclass + var fixedName = '_HX_SUPER__${name}'; + for (a in args) { + if ((a is CustomClass)) { + fixedArgs.push(cast(a, CustomClass).superClass); + } else { + fixedArgs.push(a); + } + } + var superFn = Reflect.field(superClass, fixedName); + if(superFn == null) { + this.interp.error(ECustom('Error while calling function super.${name}(): EInvalidAccess' + + '\n' + + 'InvalidAccess error: Super function "${name}" does not exist! Define it or call the correct superclass function.')); + } + r = Reflect.callMethod(superClass, superFn, fixedArgs); + } + return r; + } + + private function hasField(name:String):Bool { + return _cachedFieldDecls != null ? _cachedFieldDecls.exists(name) : false; + } + + private function getField(name:String):FieldDecl { + return _cachedFieldDecls != null ? _cachedFieldDecls.get(name) : null; + } + + private function hasVar(name:String):Bool { + return _cachedVarDecls != null ? _cachedVarDecls.exists(name) : false; + } + + private function getVar(name:String):VarDecl { + return _cachedVarDecls != null ? _cachedVarDecls.get(name): null; + } + + private function hasFunction(name:String):Bool { + return _cachedFunctionDecls != null ? _cachedFunctionDecls.exists(name) : false; + } + + private function getFunction(name:String):FunctionDecl { + return _cachedFunctionDecls != null ? _cachedFunctionDecls.get(name) : null; + } + + private function cacheSuperField(name:String, value:Dynamic) { + if(_cachedSuperFields != null) { + _cachedSuperFields.set(name, value); + } + } + + var __superClassFieldList:Array = null; + + public function superHasField(name:String):Bool { + if(superClass == null) return false; + + // Reflect.hasField(this, name) is REALLY expensive so we use a cache. + if(__superClassFieldList == null) { + __superClassFieldList = Reflect.fields(superClass).concat(Type.getInstanceFields(Type.getClass(superClass))); + } + + return __superClassFieldList.indexOf(name) != -1; + } + + /** + * Remove a function from the cache. + * This is useful when a function is broken and needs to be skipped. + * @param name The name of the function to remove from the cache. + */ + private function purgeFunction(name:String):Void { + if (_cachedFunctionDecls != null) { + _cachedFunctionDecls.remove(name); + } + } + + // Access fields + + public function hget(name:String):Dynamic { + return resolveField(name); + } + + public function hset(name:String, val:Dynamic):Dynamic { + switch (name) { + default: + if (hasVar(name)) { + if(__allowSetGet && hasFunction('set_${name}')) + return __callSetter(name, val); + + this.interp.variables.set(name, val); + } + else if (this.superClass != null) { + if(Type.getClass(this.superClass) == null) { + // Anonymous structure + if(Reflect.hasField(this.superClass, name)) { + Reflect.setField(this.superClass, name, val); + } + return val; + } + else if (this.superClass is CustomClass) { + var superCustomClass:CustomClass = cast(this.superClass, CustomClass); + try { + superCustomClass.__allowSetGet = this.__allowSetGet; + return superCustomClass.hset(name, val); + } catch (e:Dynamic) {} + } + else if(superHasField(name)){ + if(__allowSetGet) + Reflect.setProperty(this.superClass, name, val); + else + Reflect.setField(this.superClass, name, val); + } + else { + throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '" + + Type.getClassName(Type.getClass(this.superClass)) + "'"; + } + } + else { + throw "field '" + name + "' does not exist in custom class '" + this.className + "'"; + } + } + return val; + } + + public function __callGetter(name:String):Dynamic { + __allowSetGet = false; + var r = callFunction('get_${name}'); + __allowSetGet = true; + return r; + } + + public function __callSetter(name:String, val:Dynamic):Dynamic { + __allowSetGet = false; + var r = callFunction('set_${name}', [val]); + __allowSetGet = true; + return r; + } + + private function resolveField(name:String):Dynamic { + switch (name) { + case "superClass": + return this.superClass; + case "createSuperClass": + return this.createSuperClass; + case "hasFunction": + return this.hasFunction; + case "callFunction": + return this.callFunction; + default: + if (hasFunction(name)) { + var fn:Function = Reflect.makeVarArgs(function(args:Array) { + return this.callFunction(name, args); + }); + + return fn; + } + + if (hasVar(name)) { + var value:Dynamic = null; + + if(__allowSetGet && hasFunction('get_${name}')) { + value = __callGetter(name); + } + else if (this.interp.variables.exists(name)) { + value = this.interp.variables.get(name); + } else { + var v = getVar(name); + + if (v.expr != null) { + value = this.interp.expr(v.expr); + this.interp.variables.set(name, value); + } + } + + return value; + } + + if (this.superClass != null) { + if (Type.getClass(this.superClass) == null) { + // Anonymous structure + if (Reflect.hasField(this.superClass, name)) { + return Reflect.field(this.superClass, name); + } else { + throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '" + + Type.getClassName(Type.getClass(this.superClass)) + "'"; + } + } + + if (this.superClass is CustomClass) { + var superCustomClass:CustomClass = cast(this.superClass, CustomClass); + try { + superCustomClass.__allowSetGet = this.__allowSetGet; + return superCustomClass.hget(name); + } catch (e:Dynamic) {} + } + + var fields = Type.getInstanceFields(Type.getClass(this.superClass)); + if (fields.contains(name)) { + return __allowSetGet ? Reflect.getProperty(this.superClass, name) : Reflect.field(this.superClass, name); + } else { + throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '" + + Type.getClassName(Type.getClass(this.superClass)) + "'"; + } + } + else { + throw "field '" + name + "' does not exist in custom class '" + this.className + "'"; + } + } + return null; + } +} diff --git a/hscript/customclass/CustomClassDecl.hx b/hscript/customclass/CustomClassDecl.hx new file mode 100644 index 0000000..f49c920 --- /dev/null +++ b/hscript/customclass/CustomClassDecl.hx @@ -0,0 +1,150 @@ +package hscript.customclass; + +import hscript.customclass.utils.FunctionUtils; +import haxe.Constraints.Function; +import hscript.Expr.FieldDecl; +import hscript.Expr.VarDecl; +import hscript.Expr.FunctionDecl; + +@:structInit +class CustomClassDecl implements IHScriptCustomAccessBehaviour { + public var classDecl:Expr.ClassDecl; + public var imports:Map; + public var pkg:Null> = null; + + public var staticInterp:Interp = new Interp(); + + var _cachedStaticFields:Map = []; + var _cachedStaticFunctions:Map = []; + var _cachedStaticVariables:Map = []; + + public var __allowSetGet:Bool = true; + + public function cacheFields() { + for (f in classDecl.fields) { + if (f.access.contains(AStatic)) { + _cachedStaticFields.set(f.name, f); + switch (f.kind) { + case KFunction(fn): + _cachedStaticFunctions.set(f.name, fn); + case KVar(v): + _cachedStaticVariables.set(f.name, v); + if (v.expr != null) { + var varValue = this.staticInterp.expr(v.expr); + this.staticInterp.variables.set(f.name, varValue); + } + } + } + } + } + + public function callFunction(name:String, ?args:Array) { + var func:FunctionDecl = getFunction(name); + + if(func != null) { + return FunctionUtils.callStaticFunction(name, this, staticInterp, func, args != null ? args : []); + } + + return null; + } + + public function hasField(name:String):Bool { + return _cachedStaticFields.exists(name); + } + + private function hasFunction(name:String) { + return _cachedStaticFunctions.exists(name); + } + + private function getFunction(name:String):FunctionDecl { + return _cachedStaticFunctions.get(name); + } + + private function hasVar(name:String):Bool { + return _cachedStaticVariables.exists(name); + } + + private function getVar(name:String):Dynamic { + var staticVar = _cachedStaticVariables.get(name); + + if(staticVar != null) { + var varValue:Dynamic = null; + if(!this.staticInterp.variables.exists(name)) { + if(staticVar.expr != null) { + varValue = this.staticInterp.expr(staticVar.expr); + this.staticInterp.variables.set(name, varValue); + } + } + else { + varValue = this.staticInterp.variables.get(name); + } + return varValue; + } + + return null; + } + + /** + * Remove a function from the cache. + * This is useful when a function is broken and needs to be skipped. + * @param name The name of the function to remove from the cache. + */ + private function purgeFunction(name:String):Void { + if (_cachedStaticFunctions != null) { + _cachedStaticFunctions.remove(name); + } + } + + public function hget(name:String):Dynamic { + var r:Dynamic = null; + + if(hasVar(name)) { + if(__allowSetGet && hasFunction('get_${name}')) + r = __callGetter(name); + else + r = getVar(name); + return r; + } + if(hasFunction(name)) { + var fn:Function = Reflect.makeVarArgs(function(args:Array) { + return this.callFunction(name, args); + }); + return fn; + } + + throw "static field '" + name + "' does not exist in custom class '" + this.classDecl.name + "'"; + } + + public function hset(name:String, val:Dynamic):Dynamic { + if (hasVar(name)) { + if (__allowSetGet && hasFunction('set_${name}')) + return __callSetter(name, val); + else { + this.staticInterp.variables.set(name, val); + return val; + } + } + + throw "static field '" + name + "' does not exist in custom class '" + this.classDecl.name + "'"; + } + + public function __callGetter(name:String):Dynamic { + __allowSetGet = false; + var r = callFunction('get_${name}'); + __allowSetGet = true; + return r; + } + + public function __callSetter(name:String, val:Dynamic):Dynamic { + __allowSetGet = false; + var r = callFunction('set_${name}', [val]); + __allowSetGet = true; + return r; + } +} + +typedef CustomClassImport = { + var ?name:String; + var ?pkg:Array; + var ?fullPath:String; // pkg.pkg.pkg.name +} diff --git a/hscript/customclass/utils/FunctionUtils.hx b/hscript/customclass/utils/FunctionUtils.hx new file mode 100644 index 0000000..26f121a --- /dev/null +++ b/hscript/customclass/utils/FunctionUtils.hx @@ -0,0 +1,46 @@ +package hscript.customclass.utils; + +import hscript.Expr.FunctionDecl; + +@:access(hscript.customclass.CustomClassDecl) +class FunctionUtils { + public static function callStaticFunction(name:String, classDecl:CustomClassDecl, interp:Interp, fn:FunctionDecl, args:Array = null) { + var r:Dynamic = null; + + var previousValues:Map = []; + var i = 0; + for (a in fn.args) { + var value:Dynamic = null; + + if (args != null && i < args.length) { + value = args[i]; + } else if (a.value != null) { + value = interp.expr(a.value); + } + // NOTE: We assign these as variables rather than locals because those get wiped when we enter the function. + if (interp.variables.exists(a.name)) { + previousValues.set(a.name, interp.variables.get(a.name)); + } + interp.variables.set(a.name, value); + i++; + } + try { + r = interp.execute(fn.body); + } catch (e:hscript.Expr.Error) { + // A script error occurred while executing the script function. + // Purge the function from the cache so it is not called again. + classDecl.purgeFunction(name); + interp.error(#if hscriptPos e.e #else e #end); + } + + for (a in fn.args) { + if (previousValues.exists(a.name)) { + interp.variables.set(a.name, previousValues.get(a.name)); + } else { + interp.variables.remove(a.name); + } + } + + return r; + } +} \ No newline at end of file diff --git a/hscript/macros/ClassExtendMacro.hx b/hscript/macros/ClassExtendMacro.hx index 18e5c59..91327b5 100644 --- a/hscript/macros/ClassExtendMacro.hx +++ b/hscript/macros/ClassExtendMacro.hx @@ -211,6 +211,15 @@ class ClassExtendMacro { continue; if (f.name == "new") { hasNew = true; + switch (f.kind) { + case FFun(fn): + var constructor:Field = buildConstructor(fn.args); + + shadowClass.fields.push(constructor); + definedFields.push(f.name); + default: + continue; + } continue; } if (f.name.startsWith(FUNC_PREFIX)) @@ -254,6 +263,11 @@ class ClassExtendMacro { } } } + + if (__customClass != null && @:privateAccess __customClass.hasFunction(name)) { + return __customClass.callFunction(name, [$a{arguments}]); + } + return super.$name($a{arguments}); }; } else { @@ -269,6 +283,12 @@ class ClassExtendMacro { } } } + + if (__customClass != null && @:privateAccess __customClass.hasFunction(name)) { + __customClass.callFunction(name, [$a{arguments}]); + return; + } + super.$name($a{arguments}); }; } @@ -339,6 +359,13 @@ class ClassExtendMacro { Utils.setupMetas(shadowClass, imports); Utils.processImport(imports, "hscript.utils.UnsafeReflect", "UnsafeReflect"); + shadowClass.fields.push({ + name: "__cachedFields", + pos: Context.currentPos(), + kind: FVar(macro: Map), + access: [APublic, AStatic] + }); + // Adding hscript getters and setters shadowClass.fields.push({ @@ -348,6 +375,13 @@ class ClassExtendMacro { access: [APublic] }); + shadowClass.fields.push({ + name: "__customClass", + pos: Context.currentPos(), + kind: FVar(macro: hscript.customclass.CustomClass), + access: [APublic] + }); + shadowClass.fields.push({ name: "__custom__variables", pos: Context.currentPos(), @@ -472,31 +506,38 @@ class ClassExtendMacro { var hgetField = if(hasHgetInSuper) { macro { - if(__allowSetGet && __custom__variables.exists("get_" + name)) - return __callGetter(name); - if (__custom__variables.exists(name)) - return __custom__variables.get(name); + if(__custom__variables != null) { + if(__allowSetGet && __custom__variables.exists("get_" + name)) + return __callGetter(name); + if (__custom__variables.exists(name)) + return __custom__variables.get(name); + } return super.hget(name); } } else { macro { - if(__allowSetGet && __custom__variables.exists("get_" + name)) - return __callGetter(name); - if (__custom__variables.exists(name)) - return __custom__variables.get(name); + if(__custom__variables != null) { + if(__allowSetGet && __custom__variables.exists("get_" + name)) + return __callGetter(name); + if (__custom__variables.exists(name)) + return __custom__variables.get(name); + } return UnsafeReflect.getProperty(this, name); } } var hsetField = if(hasHsetInSuper) { macro { - if(__allowSetGet && __custom__variables.exists("set_" + name)) - return __callSetter(name, val); - if (__custom__variables.exists(name)) { - __custom__variables.set(name, val); - return val; + if(__custom__variables != null) { + if(__allowSetGet && __custom__variables.exists("set_" + name)) + return __callSetter(name, val); + if (__custom__variables.exists(name)) { + __custom__variables.set(name, val); + return val; + } } - if(__real_fields.contains(name)) { + + if(__real_fields != null && __real_fields.contains(name)) { UnsafeReflect.setProperty(this, name, val); return UnsafeReflect.field(this, name); } @@ -504,17 +545,20 @@ class ClassExtendMacro { } } else { macro { - if(__allowSetGet && __custom__variables.exists("set_" + name)) - return __callSetter(name, val); - if (__custom__variables.exists(name)) { - __custom__variables.set(name, val); - return val; + if(__custom__variables != null) { + if(__allowSetGet && __custom__variables.exists("set_" + name)) + return __callSetter(name, val); + if (__custom__variables.exists(name)) { + __custom__variables.set(name, val); + return val; + } } - if(__real_fields.contains(name)) { + + if(__real_fields != null && __real_fields.contains(name)) { UnsafeReflect.setProperty(this, name, val); return UnsafeReflect.field(this, name); } - __custom__variables.set(name, val); + if(__custom__variables != null) __custom__variables.set(name, val); return val; } } @@ -579,6 +623,32 @@ class ClassExtendMacro { return fields; } + + static function buildConstructor(constArgs:Array):Field { + var superCallArgs:Array = [for (arg in constArgs) macro $i{arg.name}]; + + return { + name: 'new', + access: [APublic], + pos: Context.currentPos(), + kind: FFun({ + args: constArgs, + expr: macro + { + // Call the super constructor with appropriate args + super($a{superCallArgs}); + + if(__cachedFields != null) { + for(k => v in __cachedFields) { + Reflect.setProperty(this, k, v); + trace(k); + } + __cachedFields = null; + } + }, + }), + }; + } } #else class ClassExtendMacro {