diff --git a/.vscode/settings.json b/.vscode/settings.json index b73526a2..66b44b88 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "[haxe]": { "editor.formatOnSave": false, "editor.formatOnPaste": false - } + }, + "lime.projectFile": "samples/openfl_hscript_class/project.xml", } \ No newline at end of file diff --git a/polymod/Polymod.hx b/polymod/Polymod.hx index acb14301..6e0e843d 100644 --- a/polymod/Polymod.hx +++ b/polymod/Polymod.hx @@ -655,6 +655,7 @@ class Polymod #if hscript @:privateAccess polymod.hscript._internal.PolymodScriptClass.clearScriptedClasses(); + polymod.hscript._internal.PolymodEnum.clearScriptedEnums(); polymod.hscript.HScriptable.ScriptRunner.clearScripts(); #else Polymod.warning(SCRIPT_HSCRIPT_NOT_INSTALLED, "Cannot register script classes, HScript is not available."); @@ -691,6 +692,8 @@ class Polymod polymod.hscript._internal.PolymodScriptClass.registerScriptClassByPath(path); } } + + polymod.hscript._internal.PolymodInterpEx.validateImports(); } #else Polymod.warning(SCRIPT_HSCRIPT_NOT_INSTALLED, "Cannot register script classes, HScript is not available."); @@ -728,6 +731,9 @@ class Polymod if (future != null) futures.push(future); } } + + polymod.hscript._internal.PolymodInterpEx.validateImports(); + return futures; #else Polymod.warning(SCRIPT_HSCRIPT_NOT_INSTALLED, "Cannot register script classes, HScript is not available."); @@ -1324,6 +1330,13 @@ enum abstract PolymodErrorCode(String) from String to String */ var SCRIPT_CLASS_MODULE_BLACKLISTED:String = 'script_class_module_blacklisted'; + /** + * You attempted to register a new enum with a name that is already in use. + * - Rename the enum to one that is unique and will not conflict with other enums. + * - If you need to clear all existing enum descriptors, call `Polymod.clearScripts()`. + */ + var SCRIPT_ENUM_ALREADY_REGISTERED:String = 'script_enum_already_registered'; + /** * One or more scripts are about to be parsed. * - This is an info message. You can log it or ignore it if you like. diff --git a/polymod/fs/SysFileSystem.hx b/polymod/fs/SysFileSystem.hx index 81375f42..0e3311d6 100644 --- a/polymod/fs/SysFileSystem.hx +++ b/polymod/fs/SysFileSystem.hx @@ -6,6 +6,9 @@ import polymod.fs.PolymodFileSystem; import polymod.util.Util; import polymod.util.VersionUtil; import thx.semver.VersionRule; +#if linux +using StringTools; +#end /** * An implementation of IFileSystem which accesses files from folders in the local directory. @@ -22,11 +25,18 @@ class SysFileSystem implements IFileSystem public function exists(path:String) { + #if linux + return getPathLike(path) != null; + #else return sys.FileSystem.exists(path); + #end } public function isDirectory(path:String) { + #if linux + path = getPathLike(path); + #end return sys.FileSystem.isDirectory(path); } @@ -34,6 +44,9 @@ class SysFileSystem implements IFileSystem { try { + #if linux + path = getPathLike(path); + #end return sys.FileSystem.readDirectory(path); } catch (e) @@ -45,13 +58,21 @@ class SysFileSystem implements IFileSystem public function getFileContent(path:String) { + #if linux + path = getPathLike(path); + #end return getFileBytes(path).toString(); } public function getFileBytes(path:String) { + #if linux + path = getPathLike(path); + if(path == null) return null; + #else if (!exists(path)) return null; + #end return sys.io.File.getBytes(path); } @@ -148,6 +169,65 @@ class SysFileSystem implements IFileSystem return all; } + #if linux + /** + * Returns a path to the existing file similar to the given one. + * (For instance "mod/firelight" and "Mod/FireLight" are *similar* paths) + * @param path The path to find + * @return Null Found path or null if such doesn't exist + */ + private function getPathLike(path:String):Null { + + if(sys.FileSystem.exists(path)) return path; + + var baseParts:Array = path.replace('\\', '/').split('/'); + var keyParts = []; + if (baseParts.length == 0) return null; + + while(!sys.FileSystem.exists(baseParts.join("/")) && baseParts.length != 0) + keyParts.insert(0, baseParts.pop()); + + return findFile(baseParts.join("/"),keyParts); + } + + private function findFile(base_path:String,keys:Array):Null { + var nextDir:String = base_path; + for (part in keys) { + if (part == '') continue; + + var foundNode = findNode(nextDir, part); + + if (foundNode == null) { + return null; + } + nextDir = nextDir+"/"+foundNode; + } + + return nextDir; + } + /** + * Searches a given directory and returns a name of the existing file/directory + * *similar* to the **key** + * @param dir Base directory to search + * @param key The file/directory you want to find + * @return Either a file name, or null if the one doesn't exist + */ + private function findNode(dir:String, key:String):Null { + try { + var allFiles:Array = sys.FileSystem.readDirectory(dir); + var fileMap:Map = new Map(); + + for (file in allFiles) { + fileMap.set(file.toLowerCase(), file); + } + + return fileMap.get(key.toLowerCase()); + } catch (e:Dynamic) { + return null; + } + } + #end + private function _readDirectoryRecursive(str:String):Array { if (exists(str) && isDirectory(str)) diff --git a/polymod/hscript/_internal/PolymodAbstractScriptClass.hx b/polymod/hscript/_internal/PolymodAbstractScriptClass.hx index 628d5db6..3452d5b2 100644 --- a/polymod/hscript/_internal/PolymodAbstractScriptClass.hx +++ b/polymod/hscript/_internal/PolymodAbstractScriptClass.hx @@ -47,6 +47,15 @@ abstract PolymodAbstractScriptClass(PolymodScriptClass) from PolymodScriptClass { var v = this.findVar(name); + switch (v.get) { + case "get": + return this.callFunction('get_$name'); + + case "null": + throw "Invalid access to field " + name; + return null; + } + var varValue:Dynamic = null; if (this._interp.variables.exists(name) == false) { @@ -118,6 +127,17 @@ abstract PolymodAbstractScriptClass(PolymodScriptClass) from PolymodScriptClass case _: if (this.findVar(name) != null) { + var decl = this.findVar(name); + switch (decl.set) { + case "set": + this.callFunction('set_$name', [value]); + return value; + + case "never" | "null": + throw "Invalid access to field " + name; + return value; + } + this._interp.variables.set(name, value); return value; } diff --git a/polymod/hscript/_internal/PolymodClassDeclEx.hx b/polymod/hscript/_internal/PolymodClassDeclEx.hx index a5d45619..6aa5895c 100644 --- a/polymod/hscript/_internal/PolymodClassDeclEx.hx +++ b/polymod/hscript/_internal/PolymodClassDeclEx.hx @@ -14,9 +14,11 @@ typedef PolymodClassDeclEx = * Save performance and improve sandboxing by resolving imports at interpretation time. */ @:optional var imports:Map; + @:optional var importsToValidate:Map; @:optional var pkg:Array; @:optional var staticFields:Array; + @:optional var usings:Map; } /** diff --git a/polymod/hscript/_internal/PolymodEnum.hx b/polymod/hscript/_internal/PolymodEnum.hx new file mode 100644 index 00000000..7efae699 --- /dev/null +++ b/polymod/hscript/_internal/PolymodEnum.hx @@ -0,0 +1,66 @@ +package polymod.hscript._internal; + +#if hscript +import hscript.Expr; + +@:access(hscript.Interp) +@:allow(polymod.Polymod) +class PolymodEnum +{ + private static final scriptInterp = new PolymodInterpEx(null, null); + + private var _e:PolymodEnumDeclEx; + + private var _value:String; + + private var _args:Array; + + public function new(e:PolymodEnumDeclEx, value:String, args:Array) + { + this._e = e; + + var field = getField(value); + + if (field == null) + { + Polymod.error(SCRIPT_PARSE_ERROR, '${e.name}.${value} does not exist.'); + return; + } + + this._value = value; + + if (args.length != field.args.length) + { + Polymod.error(SCRIPT_PARSE_ERROR, '${e.name}.${value} got the wrong number of arguments.'); + return; + } + + this._args = args; + } + + public static function clearScriptedEnums():Void + { + scriptInterp.clearScriptEnumDescriptors(); + } + + private function getField(name:String):Null + { + for (field in _e.fields) + { + if (field.name == name) + { + return field; + } + } + return null; + } + + public function toString():String + { + var result:String = '${_e.name}.${_value}'; + if(_args.length > 0) + result += '(${_args.join(',')})'; + return result; + } +} +#end diff --git a/polymod/hscript/_internal/PolymodEnumDeclEx.hx b/polymod/hscript/_internal/PolymodEnumDeclEx.hx new file mode 100644 index 00000000..bafd0d13 --- /dev/null +++ b/polymod/hscript/_internal/PolymodEnumDeclEx.hx @@ -0,0 +1,13 @@ +package polymod.hscript._internal; + +#if hscript +import hscript.Expr; + +typedef PolymodEnumDeclEx = +{ + > EnumDecl, + + @:optional var pkg:Array; +} + +#end diff --git a/polymod/hscript/_internal/PolymodInterpEx.hx b/polymod/hscript/_internal/PolymodInterpEx.hx index 8438dffb..0f5cb459 100644 --- a/polymod/hscript/_internal/PolymodInterpEx.hx +++ b/polymod/hscript/_internal/PolymodInterpEx.hx @@ -8,12 +8,15 @@ import polymod.hscript._internal.PolymodExprEx; import polymod.hscript._internal.PolymodClassDeclEx.PolymodClassImport; import polymod.hscript._internal.PolymodClassDeclEx.PolymodStaticClassReference; +using StringTools; + /** * Based on code by Ian Harrigan * @see https://github.com/ianharrigan/hscript-ex */ @:access(polymod.hscript._internal.PolymodScriptClass) @:access(polymod.hscript._internal.PolymodAbstractScriptClass) +@:access(polymod.hscript._internal.PolymodEnum) class PolymodInterpEx extends Interp { var targetCls:Class; @@ -133,6 +136,14 @@ class PolymodInterpEx extends Interp var func = get(o, f); + @:privateAccess + { + if (_proxy != null && _proxy._cachedUsingFunctions.exists(f)) + { + return _proxy._cachedUsingFunctions[f]([o].concat(args)); + } + } + // Workaround for an HTML5-specific issue. // https://github.com/HaxeFoundation/haxe/issues/11298 if (func == null && f == "contains") { @@ -193,6 +204,53 @@ class PolymodInterpEx extends Interp return _scriptClassDescriptors.get(name); } + private static var _scriptEnumDescriptors:Map = new Map(); + + private static function registerScriptEnum(e:PolymodEnumDeclEx) + { + var name = e.name; + if (e.pkg != null) + { + name = e.pkg.join(".") + "." + name; + } + + if (_scriptEnumDescriptors.exists(name)) { + Polymod.error(SCRIPT_ENUM_ALREADY_REGISTERED, 'An enum with the fully qualified name "$name" has already been defined. Please change the enum name to ensure a unique name.'); + return; + } else { + Polymod.debug('Registering enum $name'); + _scriptEnumDescriptors.set(name, e); + } + } + + public function clearScriptEnumDescriptors():Void { + // Clear the script enum descriptors. + _scriptEnumDescriptors.clear(); + + // Also destroy local variable scope. + this.resetVariables(); + } + + public static function validateImports():Void + { + for (cls in _scriptClassDescriptors) + { + var clsPath = cls.pkg != null ? (cls.pkg.join(".") + ".") : ""; + clsPath += cls.name; + + for (key => imp in cls.importsToValidate) + { + if (_scriptEnumDescriptors.exists(imp.fullPath)) + { + cls.imports.set(key, imp); + continue; + } + + Polymod.error(SCRIPT_CLASS_MODULE_NOT_FOUND, 'Could not import ${imp.fullPath}', clsPath); + } + } + } + override function setVar(id:String, v:Dynamic) { if (_proxy != null && _proxy.superClass != null) @@ -225,6 +283,25 @@ class PolymodInterpEx extends Interp return v; } } + + @:privateAccess + { + if (_proxy != null) + { + var decl = _proxy.findVar(id); + var v = expr(e2); + switch (decl?.set) + { + case "set": + var out = _proxy.callFunction('set_$id', [v]); + return (out == null) ? v : out; + + case "never": + errorEx(EInvalidAccess(id)); + return null; + } + } + } case EField(e0, id): // Make sure setting superclass fields works when using this. // Also ensures property functions are accounted for. @@ -252,6 +329,82 @@ class PolymodInterpEx extends Interp return super.assign(e1, e2); } + override function increment(e:Expr, prefix:Bool, delta:Int) + { + switch (Tools.expr(e)) + { + case EIdent(id): + @:privateAccess + { + if (_proxy != null) + { + var decl = _proxy.findVar(id); + if (decl != null) + { + var v = switch (decl.get) + { + case "get": _proxy.callFunction('get_$id'); + default: expr(decl.expr); + } + + if (prefix) + v += delta; + + switch(decl.set) + { + case "set": + _proxy.callFunction('set_$id', [prefix ? v : (v += delta)]); + return prefix ? v : (v += delta); + case "never": + errorEx(EInvalidAccess(id)); + return prefix ? v : (v += delta); + } + } + } + } + default: + } + + return super.increment(e, prefix, delta); + } + + override function evalAssignOp(op:String, fop:Dynamic->Dynamic->Dynamic, e1:Expr, e2:Expr) + { + switch (Tools.expr(e1)) + { + case EIdent(id): + @:privateAccess + { + if (_proxy != null) + { + var decl = _proxy.findVar(id); + if (decl != null) + { + var value = switch (decl.get) + { + case "get": _proxy.callFunction('get_$id'); + default: expr(e1); + } + + var v = fop(value,expr(e2)); + + switch(decl.set) + { + case "set": + _proxy.callFunction('set_$id', [v]); + return v; + case "never": + errorEx(EInvalidAccess(id)); + return v; + } + } + } + } + default: + } + return super.evalAssignOp(op, fop, e1, e2); + } + public override function expr(e:Expr):Dynamic { // Override to provide some fixes, falling back to super.expr() when not needed. @@ -273,8 +426,22 @@ class PolymodInterpEx extends Interp var result = (expression != null) ? exprWithType(expression, type) : null; locals.set(name, {r: result}); - return null; - case EFunction(params, fexpr, name, _): // Fix to ensure callback functions catch thrown errors. + case EIdent(id): + // When resolving a variable, check if it is a property with a getter, and call it if necessary. + @:privateAccess + { + if (_proxy != null) + { + var decl = _proxy.findVar(id); + switch (decl?.get) + { + case "get": + return _proxy.callFunction('get_$id'); + } + } + } + case EFunction(params, fexpr, name, _): + // Fix to ensure callback functions catch thrown errors. var capturedLocals = duplicate(locals); var me = this; var hasOpt = false, minParams = 0; @@ -426,42 +593,123 @@ class PolymodInterpEx extends Interp #else var err = error; #end - switch (err) { - case EScriptThrow(errValue): - // restore vars - restore(old); - inTry = oldTry; - // declare 'v' - declared.push({ n : n, old : locals.get(n) }); - locals.set(n,{ r : errValue }); - var v : Dynamic = expr(ecatch); - restore(old); - return v; - default: - throw err; - } - } catch( err : Dynamic ) { - // I can't handle this error the normal way because Stop is private GRAAAAA - if (Type.getEnumName(err) == "hscript.Interp.Stop") { + // restore vars + restore(old); + inTry = oldTry; + // declare 'v' + declared.push({ n : n, old : locals.get(n) }); + locals.set(n, { r : switch (err) { + case EScriptThrow(errValue): errValue; + default: error; + }}); + var v : Dynamic = expr(ecatch); + restore(old); + return v; + } catch (error : Dynamic) { + var en = Type.getEnum(error); + if (en != null && (en.getName() == "hscript._Interp.Stop" || en.getName() == "hscript.Interp.Stop")) { + // HScript catches errors specifically of the type Stop, and uses them to handle + // `break`, `continue`, and `return` statements without extensive logic to skip subsequent expressions. + // This is safe to throw since it won't escalate outside of Polymod. inTry = oldTry; - throw err; + throw error; } - // restore vars restore(old); inTry = oldTry; // declare 'v' declared.push({ n : n, old : locals.get(n) }); - locals.set(n,{ r : err }); + locals.set(n, { r : error }); var v : Dynamic = expr(ecatch); restore(old); return v; } case EThrow(e): - var str = 'Script Error: ${expr(e)}'; // If there is a try/catch block, the error will be caught. // If there is no try/catch block, the error will be reported. - errorEx(EScriptThrow(str)); + errorEx(EScriptThrow('${expr(e)}')); + // Enums + case EField(e,f): + var name = getIdent(e); + name = getClassDecl().imports.get(name)?.fullPath ?? name; + if (name != null && _scriptEnumDescriptors.exists(name)) + { + return new PolymodEnum(_scriptEnumDescriptors.get(name), f, []); + } + case ECall(e,params): + var args = new Array(); + for (p in params) + args.push(expr(p)); + + switch(Tools.expr(e)) { + case EField(e,f): + var name = getIdent(e); + name = getClassDecl().imports.get(name)?.fullPath ?? name; + if (name != null && _scriptEnumDescriptors.exists(name)) + { + return new PolymodEnum(_scriptEnumDescriptors.get(name), f, args); + } + default: + } + case ESwitch(e, cases, def): + var val:Dynamic = expr(e); + + if (Std.isOfType(val, PolymodEnum)) + { + var old:Int = declared.length; + var match = false; + for(c in cases) + { + for(v in c.values) + { + switch (Tools.expr(v)) + { + case ECall(e, params): + switch (Tools.expr(e)) + { + case EField(_, f): + if (val._value == f) + { + for (i => p in params) + { + switch (Tools.expr(p)) + { + case EIdent(n): + declared.push({ + n: n, + old: {r: locals.get(n)} + }); + locals.set(n, {r: val._args[i]}); + default: + } + } + match = true; + break; + } + default: + } + case EField(_, f): + if (val._value == f) + { + match = true; + break; + } + default: + } + } + if(match) + { + val = expr(c.expr); + break; + } + } + if (!match) + { + val = def == null ? null : expr(def); + } + restore(old); + return val; + } default: // Do nothing. } @@ -600,6 +848,21 @@ class PolymodInterpEx extends Interp return a; } + function getIdent(e:Expr):Null { + #if hscriptPos + switch (e.e) + { + #else + switch (e) + { + #end + case EIdent(v): + return v; + default: + return null; + } + } + override function makeIterator(v:Dynamic):Iterator { if (v.iterator != null) @@ -793,6 +1056,11 @@ class PolymodInterpEx extends Interp // return result; } + var abstractKey:String = Type.getClassName(o) + '.' + f; + if (PolymodScriptClass.abstractClassStatics.exists(abstractKey)) { + return Reflect.getProperty(PolymodScriptClass.abstractClassStatics[abstractKey], abstractKey.replace('.', '_')); + } + // Default behavior if (Reflect.hasField(o, f)) { return Reflect.field(o, f); @@ -1154,8 +1422,8 @@ class PolymodInterpEx extends Interp } } else { Polymod.error(SCRIPT_RUNTIME_EXCEPTION, - 'Error while retrieving static field ${fieldName}(): EInvalidAccess' + '\n' + - 'InvalidAccess error: Static field "${fieldName}" does not exist! Define it or access the correct variable.'); + 'Error while retrieving static field ${fieldName}: EInvalidAccess' + '\n' + + 'Static field "${fieldName}" does not exist! Define it, import it, or access the correct variable.'); return null; } } @@ -1218,8 +1486,8 @@ class PolymodInterpEx extends Interp return value; } else { Polymod.error(SCRIPT_RUNTIME_EXCEPTION, - 'Error while modifying static field ${fieldName}(): EInvalidAccess' + '\n' + - 'InvalidAccess error: Static field "${fieldName}" does not exist! Define it or access the correct variable.'); + 'Error while modifying static field ${fieldName}: EInvalidAccess' + '\n' + + 'Static field "${fieldName}" does not exist! Define it or modify the correct variable.'); return null; } } @@ -1256,6 +1524,8 @@ class PolymodInterpEx extends Interp { var pkg:Array = null; var imports:Map = []; + var importsToValidate:Map = []; + var usings:Map = []; for (importPath in PolymodScriptClass.defaultImports.keys()) { @@ -1270,14 +1540,65 @@ class PolymodInterpEx extends Interp }); } + var importClass = function(path:Array, isUsing:Bool = false) + { + var mapToUse = (isUsing ? usings : imports); + var clsName = path[path.length - 1]; + + if (mapToUse.exists(clsName)) + { + if (mapToUse.get(clsName) == null) { + Polymod.error(SCRIPT_CLASS_MODULE_BLACKLISTED, 'Scripted class ${clsName} is blacklisted and cannot be used in scripts.', origin); + } else { + Polymod.warning(SCRIPT_CLASS_MODULE_ALREADY_IMPORTED, 'Scripted class ${clsName} has already been imported.', origin); + } + continue; + } + + var importedClass:PolymodClassImport = { + name: clsName, + pkg: path.slice(0, path.length - 1), + fullPath: path.join("."), + cls: null, + enm: null + }; + + if (PolymodScriptClass.importOverrides.exists(importedClass.fullPath)) { + // importOverrides can exist but be null (if it was set to null). + // If so, that means the class is blacklisted. + + importedClass.cls = PolymodScriptClass.importOverrides.get(importedClass.fullPath); + } else { + var resultCls:Class = Type.resolveClass(importedClass.fullPath); + + // If the class is not found, try to find it as an enum. + var resultEnm:Enum = null; + if (resultCls == null) + resultEnm = Type.resolveEnum(importedClass.fullPath); + + // If the class is still not found, skip this import entirely. + if (resultCls == null && resultEnm == null) { + Polymod.error(SCRIPT_CLASS_MODULE_NOT_FOUND, 'Could not import class ${importedClass.fullPath}', origin); + continue; + } else if (resultCls != null) { + importedClass.cls = resultCls; + } else if (resultEnm != null) { + importedClass.enm = resultEnm; + } + } + + mapToUse.set(importedClass.name, importedClass); + } + for (decl in module) { switch (decl) { case DPackage(path): pkg = path; - case DImport(path, _): + case DImport(path, _, name): var clsName = path[path.length - 1]; + if (name != null) clsName = name; if (imports.exists(clsName)) { @@ -1307,6 +1628,8 @@ class PolymodInterpEx extends Interp importedClass.cls = PolymodScriptClass.abstractClassImpls.get(importedClass.fullPath); trace('RESOLVED ABSTRACT CLASS ${importedClass.fullPath} -> ${Type.getClassName(importedClass.cls)}'); trace(Type.getClassFields(importedClass.cls)); + } else if (_scriptEnumDescriptors.exists(importedClass.fullPath)) { + // do nothing } else { var resultCls:Class = Type.resolveClass(importedClass.fullPath); @@ -1317,7 +1640,9 @@ class PolymodInterpEx extends Interp // If the class is still not found, skip this import entirely. if (resultCls == null && resultEnm == null) { - Polymod.error(SCRIPT_CLASS_MODULE_NOT_FOUND, 'Could not import class ${importedClass.fullPath}', origin); + //Polymod.error(SCRIPT_CLASS_MODULE_NOT_FOUND, 'Could not import class ${importedClass.fullPath}', origin); + // this could be a scripted class or enum that hasn't been registered yet + importsToValidate.set(importedClass.name, importedClass); continue; } else if (resultCls != null) { importedClass.cls = resultCls; @@ -1328,6 +1653,55 @@ class PolymodInterpEx extends Interp Polymod.debug('Imported class ${importedClass.name} from ${importedClass.fullPath}'); imports.set(importedClass.name, importedClass); + case DUsing(path): + var clsName = path[path.length - 1]; + + if (usings.exists(clsName)) + { + if (usings.get(clsName) == null) { + Polymod.error(SCRIPT_CLASS_MODULE_BLACKLISTED, 'Scripted class ${clsName} is blacklisted and cannot be used in scripts.', origin); + } else { + Polymod.warning(SCRIPT_CLASS_MODULE_ALREADY_IMPORTED, 'Scripted class ${clsName} has already been used.', origin); + } + continue; + } + + var importedClass:PolymodClassImport = { + name: clsName, + pkg: path.slice(0, path.length - 1), + fullPath: path.join("."), + cls: null, + enm: null + }; + + if (PolymodScriptClass.importOverrides.exists(importedClass.fullPath)) { + // importOverrides can exist but be null (if it was set to null). + // If so, that means the class is blacklisted. + + importedClass.cls = PolymodScriptClass.importOverrides.get(importedClass.fullPath); + } else if (PolymodScriptClass.abstractClassImpls.exists(importedClass.fullPath)) { + // We used a macro to map each abstract to its implementation. + importedClass.cls = PolymodScriptClass.abstractClassImpls.get(importedClass.fullPath); + trace('RESOLVED ABSTRACT CLASS ${importedClass.fullPath} -> ${Type.getClassName(importedClass.cls)}'); + trace(Type.getClassFields(importedClass.cls)); + } else if (_scriptEnumDescriptors.exists(importedClass.fullPath)) { + // do nothing + } else { + var resultCls:Class = Type.resolveClass(importedClass.fullPath); + + // If the class is still not found, skip this import entirely. + if (resultCls == null) { + //Polymod.error(SCRIPT_CLASS_MODULE_NOT_FOUND, 'Could not import class ${importedClass.fullPath}', origin); + // this could be a scripted class that hasn't been registered yet + importsToValidate.set(importedClass.name, importedClass); + continue; + } else { + importedClass.cls = resultCls; + } + } + + Polymod.debug('Using class ${importedClass.name} from ${importedClass.fullPath}'); + usings.set(importedClass.name, importedClass); case DClass(c): var extend = c.extend; if (extend != null) @@ -1374,6 +1748,8 @@ class PolymodInterpEx extends Interp var classDecl:PolymodClassDeclEx = { imports: imports, + importsToValidate: importsToValidate, + usings: usings, pkg: pkg, name: c.name, params: c.params, @@ -1386,6 +1762,25 @@ class PolymodInterpEx extends Interp staticFields: staticFields, }; registerScriptClass(classDecl); + case DEnum(e): + if (pkg != null) + { + imports.set(e.name, { + name: e.name, + pkg: pkg, + fullPath: pkg.join(".") + "." + e.name, + cls: null, + enm: null, + }); + } + + var enumDecl:PolymodEnumDeclEx = { + pkg: pkg, + name: e.name, + fields: e.fields, + }; + + registerScriptEnum(enumDecl); case DTypedef(_): } } diff --git a/polymod/hscript/_internal/PolymodScriptClass.hx b/polymod/hscript/_internal/PolymodScriptClass.hx index 54d352e9..16dcef6b 100644 --- a/polymod/hscript/_internal/PolymodScriptClass.hx +++ b/polymod/hscript/_internal/PolymodScriptClass.hx @@ -97,6 +97,23 @@ class PolymodScriptClass return _abstractClassImpls; } + public static var abstractClassStatics(get, never):Map>; + static var _abstractClassStatics:Map> = null; + + static function get_abstractClassStatics():Map> { + if (_abstractClassStatics == null) { + _abstractClassStatics = new Map>(); + + var baseAbstractClassStatics:Map> = PolymodScriptClassMacro.listAbstractStatics(); + + for (key => value in baseAbstractClassStatics) { + _abstractClassStatics.set(key, value); + } + } + + return _abstractClassStatics; + } + /** * Register a scripted class by retrieving the script from the given path. */ @@ -842,6 +859,7 @@ class PolymodScriptClass private var _cachedSuperFunctionDecls:Map = []; private var _cachedFunctionDecls:Map = []; private var _cachedVarDecls:Map = []; + private var _cachedUsingFunctions:Map = []; private function buildCaches() { @@ -849,6 +867,24 @@ class PolymodScriptClass _cachedSuperFunctionDecls.clear(); _cachedFunctionDecls.clear(); _cachedVarDecls.clear(); + _cachedUsingFunctions.clear(); + + for (n => u in _c.usings) + { + var fields = Type.getClassFields(u.cls); + if (fields.length == 0) continue; + + for (fld in fields) + { + var func:Dynamic = function(params:Array) + { + var prop:Dynamic = Reflect.getProperty(u.cls, fld); + return Reflect.callMethod(u.cls, prop, params); + } + + _cachedUsingFunctions.set(fld, func); + } + } for (f in _c.fields) { diff --git a/polymod/hscript/_internal/PolymodScriptClassMacro.hx b/polymod/hscript/_internal/PolymodScriptClassMacro.hx index 720d9295..7d7e562a 100644 --- a/polymod/hscript/_internal/PolymodScriptClassMacro.hx +++ b/polymod/hscript/_internal/PolymodScriptClassMacro.hx @@ -10,6 +10,8 @@ import polymod.util.MacroUtil; import haxe.rtti.Meta; +using StringTools; + /** * Provides a macro which, after types are generated, populates a list of classes which extend `polymod.hscript.HScriptedClass`. * We have to do weird shenanigans to make the data accessible at runtime though. @@ -39,8 +41,25 @@ class PolymodScriptClassMacro { return macro polymod.hscript._internal.PolymodScriptClassMacro.fetchAbstractImpls(); } + public static macro function listAbstractStatics():ExprOf>> { + if (!onAfterTypingCallbackRegistered) + { + onAfterTypingCallbackRegistered = true; + haxe.macro.Context.onAfterTyping(onAfterTyping); + } + + if (!onGenerateCallbackRegistered) + { + onGenerateCallbackRegistered = true; + haxe.macro.Context.onGenerate(onGenerate); + } + + return macro polymod.hscript._internal.PolymodScriptClassMacro.fetchAbstractStatics(); + } + #if macro static var onGenerateCallbackRegistered:Bool = false; + static var onAfterTypingCallbackRegistered:Bool = false; static function onGenerate(allTypes:Array) { // Reset these, since onGenerate persists across multiple builds. @@ -48,6 +67,7 @@ class PolymodScriptClassMacro { var hscriptedClassEntries:Array = []; var abstractImplEntries:Array = []; + var abstractStaticEntries:Array = []; for (type in allTypes) { switch (type) { @@ -88,6 +108,30 @@ class PolymodScriptClassMacro { abstractImplEntries.push(macro $a{entryData}); + for (field in abstractImpl.statics.get()) { + switch (field.type) { + case TAbstract(_, _): + // + case TType(_, _): + // + default: + continue; + } + + var key:String = '${abstractImplPath}.${field.name}'; + + if (!staticFieldToClass.exists(key)) { + continue; + } + + var staticEntryData = [ + macro $v{key}, + macro $v{staticFieldToClass[key]}, + ]; + + abstractStaticEntries.push(macro $a{staticEntryData}); + } + // Try to apply RTTI? abstractType.meta.add(':rtti', [], Context.currentPos()); abstractImpl.meta.add(':rtti', [], Context.currentPos()); @@ -100,6 +144,101 @@ class PolymodScriptClassMacro { var polymodScriptClassClassType:ClassType = MacroUtil.getClassType('polymod.hscript._internal.PolymodScriptClassMacro'); polymodScriptClassClassType.meta.add('hscriptedClasses', hscriptedClassEntries, Context.currentPos()); polymodScriptClassClassType.meta.add('abstractImpls', abstractImplEntries, Context.currentPos()); + polymodScriptClassClassType.meta.add('abstractStatics', abstractStaticEntries, Context.currentPos()); + } + + static var iteration:Int = 0; + static var staticFieldToClass:Map = []; + static function onAfterTyping(types: Array):Void { + var fields:Array = []; + + for (type in types) { + switch (type) { + case TAbstract(a): + var abstractPath = a.toString(); + var abstractType = a.get(); + + if (abstractPath != 'flixel.util.FlxColor') { + continue; + } + + if (abstractType.impl == null) { + continue; + } + + var abstractImplPath = abstractType.impl.toString(); + var abstractImplType = abstractType.impl.get(); + + var excludes:Array = []; + for (field in abstractImplType.statics.get()) { + switch (field.type) { + case TFun(_, _): + default: + continue; + } + + // exclude anything that has a getter or setter + // most of the time i think variables that have them are not static + // hopefully that's true + if (field.name.startsWith('get_') || field.name.startsWith('_set')) { + excludes.push(field.name.replace('get_', '').replace('set_', '')); + } + } + + for (field in abstractImplType.statics.get()) { + switch (field.type) { + case TFun(_, _): + continue; + default: + } + + if (excludes.contains(field.name)) { + continue; + } + + var fieldName:String = '${abstractImplPath.replace('.', '_')}_${field.name}'; + + fields.push({ + pos: Context.currentPos(), + name: fieldName, + access: [Access.APublic, Access.AStatic], + kind: FProp('get', 'never', Context.toComplexType(field.type), null) + }); + + fields.push({ + pos: Context.currentPos(), + name: 'get_${fieldName}', + access: [Access.APublic, Access.AStatic], + kind: FFun({ + args: [], + ret: null, + expr: macro { + @:privateAccess + return ${Context.parse(abstractPath + '.' + field.name, Context.currentPos())}; + } + }) + }); + + staticFieldToClass.set('${abstractImplPath}.${field.name}', 'polymod.hscript._internal.AbstractStaticMembers_${iteration}'); + } + default: + continue; + } + } + + if (fields.length == 0) { + return; + } + + Context.defineType({ + pos: Context.currentPos(), + pack: ['polymod', 'hscript', '_internal'], + name: 'AbstractStaticMembers_${iteration}', + kind: TDClass(null, [], false, false, false), + fields: fields + }); + + iteration++; } #end @@ -173,6 +312,32 @@ class PolymodScriptClassMacro { } } + public static function fetchAbstractStatics():Map> { + var metaData = Meta.getType(PolymodScriptClassMacro); + + if (metaData.abstractStatics != null) { + var result:Map> = []; + + // Each element is formatted as `[abstractPathImpl.fieldName, reflectClass]`. + + for (element in metaData.abstractStatics) { + if (element.length != 2) { + throw 'Malformed element in abstractStatics: ' + element; + } + + var fieldPath:String = element[0]; + var reflectClassPath:String = element[1]; + var reflectClass:Class = cast Type.resolveClass(reflectClassPath); + + result.set(fieldPath, reflectClass); + } + + return result; + } else { + throw 'No abstractStatics found in PolymodScriptClassMacro!'; + } + } + #if js static var PACKAGE_NAME_INVALID = ~/[^.a-zA-Z0-9]/; diff --git a/samples/openfl_hscript_class/project.xml b/samples/openfl_hscript_class/project.xml index fc22983c..bb7d5665 100644 --- a/samples/openfl_hscript_class/project.xml +++ b/samples/openfl_hscript_class/project.xml @@ -26,5 +26,6 @@ + \ No newline at end of file