From 28c72eabbe243bcb9ea53033250eb52630c709a0 Mon Sep 17 00:00:00 2001 From: nykwono Date: Thu, 20 Aug 2026 15:48:57 -0400 Subject: [PATCH 1/4] Add full support for interfaces --- polymod/hscript/_internal/Expr.hx | 20 + polymod/hscript/_internal/Interp.hx | 250 ++++++++++- polymod/hscript/_internal/Parser.hx | 32 +- .../hscript/_internal/PolymodScriptClass.hx | 175 +++++++- .../_internal/PolymodScriptClassMacro.hx | 200 ++++++++- .../PolymodStaticInterfaceReference.hx | 387 ++++++++++++++++++ polymod/util/MacroUtil.hx | 20 + 7 files changed, 1052 insertions(+), 32 deletions(-) create mode 100644 polymod/hscript/_internal/PolymodStaticInterfaceReference.hx diff --git a/polymod/hscript/_internal/Expr.hx b/polymod/hscript/_internal/Expr.hx index 8a4b5c2a..2d796102 100644 --- a/polymod/hscript/_internal/Expr.hx +++ b/polymod/hscript/_internal/Expr.hx @@ -345,9 +345,29 @@ typedef TypeDecl = typedef InterfaceDecl = { > ModuleType, + + /** + * The classes imported by the scripted class + * This gets resolved at interpretation time to save performance and improve sandboxing + */ + var imports:Map; + + /** + * A list of imports that have yet to be validated + * + * Scripted classes that import other scripted classes might be parsed before the class they import, + * so imports have to be done in two passes. + */ + var importsToValidate:Map; + var extend:Array; var fields:Array; var isExtern:Bool; + + /** + * The package the interface belongs to + */ + var pkg:Array; } typedef FieldDecl = diff --git a/polymod/hscript/_internal/Interp.hx b/polymod/hscript/_internal/Interp.hx index fa2dd663..057f367d 100644 --- a/polymod/hscript/_internal/Interp.hx +++ b/polymod/hscript/_internal/Interp.hx @@ -54,6 +54,7 @@ class Interp private static var _scriptClassUsings:Map> = new Map>(); private static var _scriptClassDescriptors:Map = new Map(); private static var _scriptEnumDescriptors:Map = new Map(); + private static var _scriptInterfaceDescriptors:Map = new Map(); var _propTrack:Map = []; @@ -594,6 +595,28 @@ class Interp } } + static function registerScriptInterface(i:InterfaceDecl) + { + var name:String = i.name; + if (i.pkg != null && i.pkg.length > 0) + { + name = i.pkg.join('.') + '.' + i.name; + } + + if (_scriptInterfaceDescriptors.exists(name) || PolymodScriptClass.interfaceImpls.exists(name) || _scriptClassDescriptors.exists(name)) + { + Polymod.error(SCRIPTED_CLASS_ALREADY_REGISTERED, + 'Scripted interface with fully qualified name "$name" has already been defined. Please change the interface or the package name to ensure uniqueness.', + SCRIPT_RUNTIME); + return; + } + else + { + Polymod.debug('Registering scripted interface $name'); + _scriptInterfaceDescriptors.set(name, i); + } + } + private static function registerScriptEnum(e:EnumDecl) { var name = e.name; @@ -636,6 +659,11 @@ class Interp return _scriptClassDescriptors.get(name); } + public static function findScriptInterfaceDescriptor(name:String) + { + return _scriptInterfaceDescriptors.get(name); + } + private function resetVariables() { variables = new Map(); @@ -675,6 +703,17 @@ class Interp @:privateAccess PolymodScriptClass._scriptClassesByPackage = null; + // This needs to be cleared since the scripted interface data could be outdated. Will be later re-populated. + @:privateAccess + { + PolymodScriptClass._classesExtendingInterfaces?.clear(); + PolymodScriptClass._classesExtendingInterfaces = null; + } + + // Do this first since scripted interfaces are checked through their scripted decls. + PolymodStaticInterfaceReference.clearScriptedInterfaces(); + _scriptInterfaceDescriptors.clear(); + // Also clear the imports from the import.hx files. _scriptClassImports.clear(); _scriptClassUsings.clear(); @@ -1315,6 +1354,10 @@ class Interp var enumResult:Null = PolymodEnum.tryResolve(importedClass.fullPath); if (enumResult != null) return enumResult; + // Resolve imported scripted interface. + var resultInterface = PolymodStaticInterfaceReference.tryBuild(importedClass.fullPath); + if (resultInterface != null) return resultInterface; + // Resolve imported scripted classes. var result = PolymodStaticClassReference.tryBuild(importedClass.fullPath); if (result != null) return result; @@ -1342,6 +1385,9 @@ class Interp var enumResult:Null = PolymodEnum.tryResolve(localClassId); if (enumResult != null) return enumResult; + var resultInterface = PolymodStaticInterfaceReference.tryBuild(localClassId); + if (resultInterface != null) return resultInterface; + var result = PolymodStaticClassReference.tryBuild(localClassId); if (result != null) return result; } @@ -1349,6 +1395,9 @@ class Interp var enumResult:Null = PolymodEnum.tryResolve(id); if (enumResult != null) return enumResult; + var resultInterface = PolymodStaticInterfaceReference.tryBuild(id); + if (resultInterface != null) return resultInterface; + // Try to retrieve a scripted class with this name in the base package. var result = PolymodStaticClassReference.tryBuild(id); if (result != null) return result; @@ -1414,6 +1463,7 @@ class Interp return null; } + /** * Tries to resolve the type of an imported class, which will end up in `cls`, `enm` or `abs`. * @param importedClass The import to resolve. @@ -1444,7 +1494,7 @@ class Interp importedClass.cls = PolymodScriptClass.typedefs.get(fullPath); break; } - else + else if (!PolymodScriptClass.interfaceImpls.exists(fullPath)) // Base interfaces can be resolved, we don't want that. { var resultCls:Class = Type.resolveClass(fullPath); #if POLYMOD_CPPIA @@ -3025,6 +3075,23 @@ class Interp staticFields: staticFields, }; registerScriptClass(classDecl); + case DInterface(i): + if (isImportFile) continue; + + var interfaceDecl:InterfaceDecl = + { + imports: imports, + importsToValidate: importsToValidate, + name: i.name, + params: i.params, + meta: i.meta, + isPrivate: i.isPrivate, + pkg: pkg, + extend: i.extend, + isExtern: i.isExtern, + fields: i.fields, + } + registerScriptInterface(interfaceDecl); case DEnum(e): if (isImportFile) continue; @@ -3050,7 +3117,6 @@ class Interp registerScriptEnum(enumDecl); case DTypedef(_): - case DInterface(_): } } } @@ -3062,6 +3128,144 @@ class Interp registerModules(decls, origin); } + public static function validateInterfaceImports():Void + { + // Mostly the same with `validateImports` except we don't need to check for using. + for (path => inter in _scriptInterfaceDescriptors) + { + // Automatically import interfaces classes with the same package or a parent package. + var interfaceList:Array = [for (key in PolymodScriptClass.interfaceImpls.keys()) key].concat([for (key in _scriptInterfaceDescriptors.keys()) key]); + for (fullInterfacePath in interfaceList) + { + var fullPathSplit:Array = fullInterfacePath.split('.'); + var interfaceName:String = fullPathSplit[fullPathSplit.length - 1]; + var interfacePkg:Null> = fullPathSplit.length == 1 ? null : fullPathSplit.slice(0, -1); + + var interfaceImport:ClassImport = + { + name: interfaceName, + pkg: interfacePkg, + fullPath: fullInterfacePath, + } + + if ((interfacePkg?.length ?? 0) == 0) + { + inter.imports.set(interfaceName, interfaceImport); + continue; + } + + if (interfacePkg != null && fullInterfacePath.indexOf(interfacePkg.join('.')) == 0) + { + inter.imports.set(interfaceName, interfaceImport); + } + } + + // Now we need to import scripted classes. + for (cls in _scriptClassDescriptors) + { + var clsPath:String = Util.getFullClassName(cls); + var classImport:ClassImport = + { + name: cls.name, + pkg: cls.pkg, + fullPath: clsPath + } + + if ((cls.pkg?.length ?? 0) == 0) + { + inter.imports.set(cls.name, classImport); + continue; + } + + var hasPackage:Bool = cls.pkg != null && cls.pkg.length > 0; + var fullPackage:String = hasPackage ? cls.pkg.join(".") + "." : ""; + if (hasPackage && clsPath.indexOf(fullPackage) == 0) + { + inter.imports.set(cls.name, classImport); + } + } + + // Import classes from the import.hx files. + var pkg:String = inter.pkg?.join(".") ?? ""; + + for (key => imps in _scriptClassImports) + { + if (!pkg.startsWith(key) && key.length != 0) continue; + + for (imp in imps) + inter.imports.set(imp.name, imp); + } + + // Add validated imports. + for (key => imp in inter.importsToValidate) + { + if (PolymodScriptClass.interfaceImpls.exists(imp.fullPath) || _scriptInterfaceDescriptors.exists(imp.fullPath) || _scriptClassDescriptors.exists(imp.fullPath) + || _scriptEnumDescriptors.exists(imp.fullPath)) + { + inter.imports.set(key, imp); + continue; + } + + Polymod.error(SCRIPTED_CLASS_UNRESOLVED_IMPORT, 'Could not import ${imp.fullPath}. Check to ensure the module exists and is spelled correctly.', SCRIPT_RUNTIME); + } + } + + // Re-iterate through the interfaces to validate that any extends are properly imported. + // We don't have an Interp inside interfaces so we have to do this. + for (path => inter in _scriptInterfaceDescriptors) + { + if (inter.extend.length == 0) continue; + + var interfacePath:String = path; + + for (extend in inter.extend) + { + var superClassPath:String = new Printer().typeToString(extend); + var baseInterfaceName:String = superClassPath; + + switch (extend) + { + case CTPath(path, params): + if (params != null && params.length > 0) + { + Polymod.error(SCRIPTED_CLASS_UNRESOLVED_IMPORT, 'Could not extend ${superClassPath}, do not include type parameters in super class name.', SCRIPT_RUNTIME); + + _scriptInterfaceDescriptors.remove(interfacePath); + break; + } + baseInterfaceName = path[path.length - 1]; + + // The full package was used for the interface. + // Check to see if said interface exists. + if (path.length > 1) + { + if (!PolymodScriptClass.interfaceImpls.exists(superClassPath) && !_scriptInterfaceDescriptors.exists(superClassPath)) + { + Polymod.error(SCRIPTED_CLASS_NOT_REGISTERED, 'Could not import ${superClassPath}. Check to ensure the module exists and is spelled correctly.', SCRIPT_RUNTIME); + _scriptInterfaceDescriptors.remove(interfacePath); + break; + } + } + else + { + // Check to see if it's been properly imported. + var interfaceImport:ClassImport = inter.imports.get(baseInterfaceName); + + // Interface isn't imported. + if (interfaceImport == null) + { + Polymod.error(SCRIPTED_CLASS_UNRESOLVED_IMPORT, 'Interface $superClassPath has not been defined.', SCRIPT_RUNTIME); + _scriptInterfaceDescriptors.remove(interfacePath); + break; + } + } + default: + } + } + } + PolymodStaticInterfaceReference.cacheScriptedInterfaces(); + } + public static function validateImports():Void { for (cls in _scriptClassDescriptors) @@ -3069,11 +3273,13 @@ class Interp var clsPath = Util.getFullClassName(cls); // Automatically import classes with the same package or a parent package. + // First scripted classes. for (imp in _scriptClassDescriptors) { if (cls == imp) continue; - var classImport = { + var classImport = + { name: imp.name, pkg: imp.pkg, fullPath: Util.getFullClassName(imp) @@ -3093,6 +3299,34 @@ class Interp } } + // Now import interfaces. + // Populate list of interfaces to validate. + var interfaceList:Array = [for (key in PolymodScriptClass.interfaceImpls.keys()) key].concat([for (key in _scriptInterfaceDescriptors.keys()) key]); + for (fullInterfacePath in interfaceList) + { + var fullPathSplit:Array = fullInterfacePath.split('.'); + var interfaceName:String = fullPathSplit[fullPathSplit.length - 1]; + var interfacePkg:Null> = fullPathSplit.length == 1 ? null : fullPathSplit.slice(0, -1); + + var interfaceImport:ClassImport = + { + name: interfaceName, + pkg: interfacePkg, + fullPath: fullInterfacePath, + } + + if ((interfacePkg?.length ?? 0) == 0) + { + cls.imports.set(interfaceName, interfaceImport); + continue; + } + + if (interfacePkg != null && fullInterfacePath.indexOf(interfacePkg.join('.')) == 0) + { + cls.imports.set(interfaceName, interfaceImport); + } + } + // Import classes from the import.hx files. var pkg:String = cls.pkg?.join(".") ?? ""; @@ -3125,13 +3359,8 @@ class Interp continue; } - if (_scriptClassDescriptors.exists(imp.fullPath)) - { - cls.imports.set(key, imp); - continue; - } - - if (_scriptEnumDescriptors.exists(imp.fullPath)) + if (PolymodScriptClass.interfaceImpls.exists(imp.fullPath) || _scriptInterfaceDescriptors.exists(imp.fullPath) || + _scriptClassDescriptors.exists(imp.fullPath) || _scriptEnumDescriptors.exists(imp.fullPath)) { cls.imports.set(key, imp); continue; @@ -3200,6 +3429,7 @@ class Interp } } } + validateInterfaceImports(); } static function importWildcard(cls:ClassDecl, wildcardImport:ClassImport):Void diff --git a/polymod/hscript/_internal/Parser.hx b/polymod/hscript/_internal/Parser.hx index 0853ddcd..78e51fba 100644 --- a/polymod/hscript/_internal/Parser.hx +++ b/polymod/hscript/_internal/Parser.hx @@ -1643,10 +1643,23 @@ class Parser var fields = []; ensure(TBrOpen); while (!maybe(TBrClose)) - fields.push(parseInterfaceField()); + { + var newField:FieldDecl = parseInterfaceField(); + + for (field in fields) + { + if (field.name == newField.name) + { + error(ECustom('Duplicate field declaration: ${newField.name}'), currentPos, currentPos); + } + } + fields.push(newField); + } return DInterface( { + imports: [], + importsToValidate: [], name: name, meta: meta, params: params, @@ -1654,6 +1667,7 @@ class Parser extend: extend, fields: fields, isExtern: isExtern, + pkg: [], }); default: unexpected(TId(ident)); @@ -1767,18 +1781,26 @@ class Parser function parseInterfaceField():Null { var meta = parseMetadata(); - var access = []; + var access = [APublic]; // Interface fields default to public. while (true) { var id = getIdent(); switch (id) { case "public": - access.push(APublic); + access.remove(APrivate); + + if (!access.contains(APublic)) + access.push(APublic); case "private": - access.push(APrivate); + access.remove(APublic); + + if (!access.contains(APrivate)) + access.push(APrivate); case "static": - access.push(AStatic); + if (!access.contains(AStatic)) + access.push(AStatic); + case "function": var name = getIdent(); ensure(TPOpen); diff --git a/polymod/hscript/_internal/PolymodScriptClass.hx b/polymod/hscript/_internal/PolymodScriptClass.hx index 0a8e481c..50b23e4e 100644 --- a/polymod/hscript/_internal/PolymodScriptClass.hx +++ b/polymod/hscript/_internal/PolymodScriptClass.hx @@ -261,6 +261,28 @@ class PolymodScriptClass return _abstractClassImpls; } + static var _interfaceImpls:Map; + public static var interfaceImpls(get, never):Map; + + static function get_interfaceImpls():Map + { + if (_interfaceImpls == null) + { + _interfaceImpls = new Map(); + + var impls = PolymodScriptClassMacro.listInterfaceImpls(); + if (impls != null) + { + for (key in impls.keys()) + { + _interfaceImpls.set(key, PolymodStaticInterfaceReference.tryBuild(key)); + } + } + } + return _interfaceImpls; + } + + /** * Define a list of `typeName -> Class` which provides a reference to each typedef, * since typedefs can't be normally resolved at runtime. @@ -329,8 +351,7 @@ class PolymodScriptClass var list:Array = _scriptClassesByPackage.get(pack) ?? []; var fullPath:String = Util.getFullClassName(cls); - if (!list.contains(fullPath)) - list.push(fullPath); + if (!list.contains(fullPath)) list.push(fullPath); _scriptClassesByPackage.set(cls.pkg.join('.'), list); } @@ -338,6 +359,56 @@ class PolymodScriptClass return _scriptClassesByPackage; } + static var _classesExtendingInterfaces:Map>; + + /** + * Defines the list of classes that extend what interfaces. + * Used for when we want to use `Std.isOfType` to check if a class implements an interface. + * @return Map> + */ + public static var classesExtendingInterfaces(get, never):Map>; + + static function get_classesExtendingInterfaces():Map> + { + if (_classesExtendingInterfaces == null) + { + _classesExtendingInterfaces = PolymodScriptClassMacro.listClassesExtendingInterfaces(); + + // Append for scripted interfaces as well. + @:privateAccess + for (key => decl in Interp._scriptClassDescriptors) + { + if (decl.implement.length == 0) + continue; + + for (extend in decl.implement) + { + var interfaceExtends:Array = []; + + var extendName:String = new Printer().typeToString(extend); + var interfaceName:String = decl.imports.get(extendName)?.fullPath ?? extendName; + + // Retrieve the interface reference first. A cache will be used if found. + var ref:PolymodStaticInterfaceReference = PolymodStaticInterfaceReference.tryBuild(extendName); + if (ref != null) + { + if (!interfaceExtends.contains(ref.id)) + interfaceExtends.push(ref.id); + + for (int in ref.superInterfaces) + { + if (!interfaceExtends.contains(int)) + interfaceExtends.push(int); + } + } + _classesExtendingInterfaces.set(key, interfaceExtends); + } + } + } + + return _classesExtendingInterfaces; + } + /** * Register a scripted class by retrieving the script from the given path. * @@ -628,8 +699,16 @@ class PolymodScriptClass } var typeClassDecl:ClassDecl = null; + var typeInterface:PolymodStaticInterfaceReference = null; var typeFullName:String = ''; - if (t is PolymodStaticClassReference) + + if (t is PolymodStaticInterfaceReference) + { + var ref = cast(t, PolymodStaticInterfaceReference); + typeInterface = ref; + typeFullName = typeInterface.id; // `id` is always the full package name for an interface. + } + else if (t is PolymodStaticClassReference) { var o = cast(t, PolymodStaticClassReference); typeClassDecl = o.cls; @@ -642,7 +721,7 @@ class PolymodScriptClass // Check again for a class descriptor just in case. // We check for the full package name in case the scripted class was packaged. - if (typeClassDecl == null) + if (typeClassDecl == null && typeInterface == null) { var typeNameSplit:Array = typeFullName.split('.'); var typeName:String = typeNameSplit.length < 1 ? typeFullName : typeNameSplit[typeNameSplit.length - 1]; @@ -656,18 +735,45 @@ class PolymodScriptClass } // `v` can be a PolymodScriptClass if you call `this` from a scripted class. - if (v is HScriptedClass || v is PolymodStaticClassReference || v is PolymodScriptClass) + if (v is HScriptedClass || v is PolymodScriptClass) { var proxy:PolymodAbstractScriptClass = switch (v) { case (_ is HScriptedClass) => true: v._asc; - case (_ is PolymodStaticClassReference) => true: v.cls; default: cast v; } - var allPackages:Array = [proxy.fullyQualifiedName].concat(getSuperClasses(proxy._c)); + var fullClassName:String = proxy.fullyQualifiedName; + if (typeInterface != null) + { + // This scripted class does not extend an interface. + if (!classesExtendingInterfaces.exists(fullClassName)) + return false; + + // Check for whether the interface exists in the extends list. + var interfaceList:Array = classesExtendingInterfaces.get(fullClassName); + return interfaceList.contains(typeFullName); + } + else + { + var allPackages:Array = [fullClassName].concat(getSuperClasses(proxy._c)); - // Check whether the base class or any super classes are the same type as the type class. - return allPackages.indexOf(typeFullName) != -1; + // Check whether the base class or any super classes are the same type as the type class. + return allPackages.indexOf(typeFullName) != -1; + } + } + + // This interface reference is from a source code class. + if (typeInterface != null && typeInterface.interfaceDecl == null) + { + var clsName:String = Util.getTypeNameOf(v); + + // This scripted class does not extend an interface. + if (!classesExtendingInterfaces.exists(clsName)) return false; + + // Check for whether the interface exists in the extends list. + var interfaceList:Array = classesExtendingInterfaces.get(clsName); + + return interfaceList.contains(typeFullName); } // If we're on this line then it means `v` isn't a scripted class and `t` is instead. @@ -749,6 +855,7 @@ class PolymodScriptClass } _interp = new Interp(targetClass, this); _c = c; + validateInterfaces(); buildCaches(); var ctorField = findField("new"); @@ -1057,6 +1164,7 @@ class PolymodScriptClass private var _c:ClassDecl; private var _interp:Interp; + private var _interfacesList:Map; public var superClass:Dynamic = null; public var topASC(default, null):Null; @@ -1068,6 +1176,55 @@ class PolymodScriptClass return Util.getFullClassName(_c); } + private function validateInterfaces():Void + { + if (_c.implement.length == 0) return; + + _interfacesList = new Map(); + for (implement in _c.implement) + { + var extendName:String = new Printer().typeToString(implement); + + // Attempt to resolve the interface, will throw an error if it isn't able to. + var ref:PolymodStaticInterfaceReference = this._interp.resolve(extendName); + + if (ref == null || !Std.isOfType(ref, PolymodStaticInterfaceReference)) + { + this._interp.error(ECustom("You can only implement an interface")); + } + else + { + // We need to check that this interface isn't already extended through a super class. + // Else, this interface is redundant. + if (classesExtendingInterfaces.exists(fullyQualifiedName) && classesExtendingInterfaces.get(fullyQualifiedName).contains(ref.id)) + { + continue; + } + + var superInterfaceList:Array = []; + for (inter in _interfacesList) + { + superInterfaceList = superInterfaceList.concat(inter.superInterfaces); + } + + // Don't append this interface if it's already being extended or if it's a parent of another. + if (!_interfacesList.exists(ref.id) && !superInterfaceList.contains(ref.id)) + { + _interfacesList.set(ref.id, ref); + } + } + } + + for (interfaceRef in _interfacesList) + { + var errors:Array = interfaceRef.trySatisfy(_c); + if (errors.length > 0) + { + throw errors.join('\n'); + } + } + } + /** * Search for a function field with the given name. Excludes variables and static functions. * @param name The name of the function to search for. diff --git a/polymod/hscript/_internal/PolymodScriptClassMacro.hx b/polymod/hscript/_internal/PolymodScriptClassMacro.hx index fba5409a..959da7b3 100644 --- a/polymod/hscript/_internal/PolymodScriptClassMacro.hx +++ b/polymod/hscript/_internal/PolymodScriptClassMacro.hx @@ -69,6 +69,34 @@ class PolymodScriptClassMacro return macro polymod.hscript._internal.PolymodScriptClassMacro.fetchPackagesList(); } + /** + * @return An expression containing a map of each interface package name and the data for it needed for it to be constructed at runtime. + */ + public static macro function listInterfaceImpls():ExprOf>> + { + if (!onGenerateCallbackRegistered) + { + onGenerateCallbackRegistered = true; + haxe.macro.Context.onGenerate(onGenerate); + } + return macro polymod.hscript._internal.PolymodScriptClassMacro.fetchInterfaceImpls(); + } + + /** + * @return An expression containing a map of each class package and the interfaces it extends. + * We aren't able to resolve this at runtime so we use a macro. + */ + public static macro function listClassesExtendingInterfaces():ExprOf>> + { + if (!onGenerateCallbackRegistered) + { + onGenerateCallbackRegistered = true; + haxe.macro.Context.onGenerate(onGenerate); + } + return macro polymod.hscript._internal.PolymodScriptClassMacro.fetchClassesExtendingInterfaces(); + } + + #if macro static var onGenerateCallbackRegistered:Bool = false; static var onAfterTypingCallbackRegistered:Bool = false; @@ -81,6 +109,8 @@ class PolymodScriptClassMacro // Reset these, since onGenerate persists across multiple builds. var abstractImplEntries:Array> = []; var typedefEntries:Array> = []; + var interfaces:Map> = []; + var classesExtendingInterfaces:Map> = []; var startTime:Float = Sys.time(); @@ -95,6 +125,123 @@ class PolymodScriptClassMacro var classPack:String = classType.pack.join('.'); var classPath:String = t.toString(); + if (classType.isInterface) + { + var interfacePath:String = t.toString(); + + if (_params.length > 0) continue; + + var interfaceFields:Array = classType.fields.get(); + var staticFields:Array = classType.statics.get(); + var extend:Array = [for (inter in classType.interfaces) inter.t.toString()]; + + // We can't use the typedefs from `Expr` as they throw an error, so we have to define out own info to process later. + var fieldDecls:Array = []; + + for (field in interfaceFields) + { + if (field.meta.get().length > 0) continue; + + var fieldAccess:Array = []; + var fieldKind:Null = null; + var fieldKindParams:Dynamic = null; + + if (field.isPublic) + fieldAccess.push('public'); + else + fieldAccess.push('private'); + + if (staticFields.contains(field)) + fieldAccess.push('static'); + + switch (field.kind) + { + case FVar(read, write): + var getAccess = fetchVarAccess(read); + var setAccess = fetchVarAccess(write); + + fieldKind = 'var'; + fieldKindParams = { + get: getAccess ?? 'get', + set: setAccess ?? 'set', + isFinal: field.isFinal, + } + case FMethod(k): + if (k != MethNormal) continue; + + var args:Array = []; + switch (field.type) + { + case TFun(funcArgs, ret): + for (arg in funcArgs) + { + args.push(arg.name); + } + default: + } + fieldKind = 'method'; + fieldKindParams = { + args: args, + } + } + + var fieldDecl:InterfaceFieldDecl = { + name: field.name, + access: fieldAccess, + kind: fieldKind, + kindParams: fieldKindParams, + } + fieldDecls.push(fieldDecl); + } + interfaceCount++; + interfaces.set(interfacePath, [fieldDecls, extend]); + } + else + { + var interfacesImplemented:Array> = [for (inter in classType.interfaces) inter.t]; + var superCls:ClassType = classType.superClass?.t?.get() ?? null; + while (superCls != null) + { + var superClassInterfaces:Array> = [for (inter in superCls.interfaces) inter.t]; + interfacesImplemented = interfacesImplemented.concat(superClassInterfaces); + + superCls = superCls.superClass?.t?.get() ?? null; + } + + // Only append classes that actually implement interfaces. + if (interfacesImplemented.length > 0) + { + // Store all unique interfaces that this class has. + var extend:Array = []; + for (implement in interfacesImplemented) + { + if (implement.get().params.length > 0) continue; + + var interfacePath:String = implement.toString(); + + if (!extend.contains(interfacePath)) extend.push(interfacePath); + + var superInterfaces:Array = MacroUtil.listSuperInterfaces(implement.get()); + for (superInt in superInterfaces) + { + if (!extend.contains(superInt)) extend.push(superInt); + } + } + classesExtendingInterfaces.set(classPath, extend); + } + + if (MacroUtil.implementsInterface(classType, hscriptedClassType)) + { + var superClass:Null = classType.superClass != null ? classType.superClass.t.get() : null; + + if (superClass == null) throw 'No superclass for ' + classPath; + + var superClassPath:String = '${superClass.pack.concat([superClass.name]).join(".")}'; + var entryData = [superClassPath, classPath]; + hscriptedClassEntries.push(entryData); + } + } + addPackageClass(classPack, classPath); case TType(t, _params): var typedefPath:String = t.toString(); @@ -182,7 +329,9 @@ class PolymodScriptClassMacro var metaData = { abstractImpls: abstractImplEntries, typedefs: typedefEntries, - packages: packageEntries + packages: packageEntries, + interfaceEntries: interfaces, + extendingInterfaces: classesExtendingInterfaces, }; var metaDataHXSF = haxe.Serializer.run(metaData); @@ -194,7 +343,8 @@ class PolymodScriptClassMacro Context.info('PolymodScriptClassMacro: ' + 'Registered ${abstractImplEntries.length} abstract impls, ' - + '${typedefEntries.length} typedefs ' + + '${typedefEntries.length} typedefs, ' + + '${interfaceCount} interfaces ' + 'in ${duration} sec.', Context.currentPos()); } @@ -464,8 +614,30 @@ class PolymodScriptClassMacro packageEntries.set(pack, list); } } + + static function fetchVarAccess(access:haxe.macro.Type.VarAccess):Null + { + return switch (access) + { + case AccNormal: 'default'; + case AccNo: 'null'; + case AccNever: 'never'; + default: null; + } + } #end + static var _metadata:Dynamic = null; + + static function fetchMetadata():Dynamic + { + if (_metadata != null) return _metadata; + + var metaDataHXSF:String = haxe.Resource.getString(METADATA_RESOURCE_NAME); + _metadata = haxe.Unserializer.run(metaDataHXSF); + return _metadata; + } + public static function fetchAbstractImpls():Map { var metaData = fetchMetadata(); @@ -564,15 +736,18 @@ class PolymodScriptClassMacro return []; } - static var _metadata:Dynamic = null; + public static function fetchInterfaceImpls():Map> + { + var metadata = fetchMetadata(); - static function fetchMetadata():Dynamic + return metadata.interfaceEntries; + } + + public static function fetchClassesExtendingInterfaces():Map> { - if (_metadata != null) return _metadata; + var metadata = fetchMetadata(); - var metaDataHXSF:String = haxe.Resource.getString(METADATA_RESOURCE_NAME); - _metadata = haxe.Unserializer.run(metaDataHXSF); - return _metadata; + return metadata.extendingInterfaces; } #if js @@ -601,3 +776,12 @@ typedef AbstractImplEntry = cls:Class, polymodCls:Null>, }; + + +typedef InterfaceFieldDecl = +{ + var name:String; + var access:Array; + var kind:String; + var kindParams:Dynamic; +} diff --git a/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx b/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx new file mode 100644 index 00000000..3ac611fe --- /dev/null +++ b/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx @@ -0,0 +1,387 @@ +package polymod.hscript._internal; + +import polymod.hscript._internal.Expr; + +using Lambda; + +typedef InterfaceFields = Map>; + +/** + * Handles reference to an interface. + */ +class PolymodStaticInterfaceReference +{ + /** + * Internal cache for all interface references to prevent needing to create a new instance. + */ + static var _interfaceCache:Map = new Map(); + + /** + * The full path of this interface. + */ + public var id:String; + + /** + * The scripted class declaration of this interface. + * `null` unless the id finds a scripted class. + */ + public var interfaceDecl(get, never):InterfaceDecl; + + function get_interfaceDecl():InterfaceDecl + { + if (Interp.findScriptInterfaceDescriptor(id) != null) + { + return Interp.findScriptInterfaceDescriptor(id); + } + return null; + } + + /** + * The list of interfaces that are extended by this interface. + */ + public var superInterfaces(get, never):Array; + + function get_superInterfaces():Array + { + return [for (i in getFields().keys()) i]; + } + + /** + * The cached fields of this interface. + * This is a map so we're easily able to track which field belongs to what interface in the case of error reporting. + */ + var _fieldsDecl:InterfaceFields = null; + + /** + * Instantiates instances of all scripted interfaces. + */ + public static function cacheScriptedInterfaces():Void + { + @:privateAccess + for (key in Interp._scriptInterfaceDescriptors.keys()) + { + tryBuild(key); + } + } + + /** + * Clear all scripted interfaces and remove them from the cache. + * We don't remove any base interfaces as those were initalized on compilation and aren't going to change. + */ + public static function clearScriptedInterfaces():Void + { + @:privateAccess + for (key in Interp._scriptInterfaceDescriptors.keys()) + { + var ref = _interfaceCache.get(key); + if (ref != null) + { + // Just for good measure. + ref._fieldsDecl.clear(); + ref._fieldsDecl = null; + } + _interfaceCache.remove(ref.id); + } + } + + /** + * Retrieves a static interface reference through an id. + * @param id The id (class path) of this interface. + * @return PolymodStaticInterfaceReference + */ + public static function tryBuild(id:String):PolymodStaticInterfaceReference + { + if (!PolymodScriptClass.interfaceImpls.exists(id) && Interp.findScriptInterfaceDescriptor(id) == null) + { + return null; + } + else + { + if (_interfaceCache.exists(id)) return _interfaceCache.get(id); + + var ref:PolymodStaticInterfaceReference = new PolymodStaticInterfaceReference(id); + _interfaceCache.set(id, ref); + + return ref; + } + } + + public function new(id:String) + { + this.id = id; + getFields(); // Cache fields. + } + + /** + * Iterates through each field of this interface to see whether this class meets all of the requirements. + * If it doesn't, a list of errors will be thrown. + * @param cls The class to check. + * @return A list of errors needed to be resolved. + */ + public function trySatisfy(cls:ClassDecl):Array + { + var errorList:Array = []; + for (interfaceId => interfaceFields in getFields()) + { + for (field in interfaceFields) + { + var foundField:Null = cls.fields.find((clsField:FieldDecl) -> return clsField.name == field.name); + if (foundField != null) + { + // Check to make sure the access is correct for the field. + for (accessVal in field.access) + { + switch (accessVal) + { + case APrivate: + if (!foundField.access.contains(APrivate) || foundField.access.contains(APublic)) + errorList.push('Field "${foundField.name}" should be private as requested by "$interfaceId"'); + + case APublic: + if (!foundField.access.contains(APublic) || foundField.access.contains(APrivate)) + errorList.push('Field "${foundField.name}" should be public as requested by "$interfaceId"'); + + case AStatic: + if (!foundField.access.contains(AStatic)) + errorList.push('Field "${foundField.name}" should be static as requested by "$interfaceId"'); + default: + } + } + + // Check to make sure kind is the same. + // When it is, we do checks to make sure it satisfies the declaration as well. + switch (field.kind) + { + case KVar(v): + switch (foundField.kind) + { + case KVar(v2): + // Throw an error if the property accessor is not the same. + if (v.get != null && v.set != null && !(v.set == v2.set && v.get == v2.get)) + { + var clsFieldVarAccess:String = (v2.get == null && v2.set == null) ? 'var' : '(${v2.get}, ${v2.set})'; + var interfaceFieldVarAccess:String = (v.get == null && v.set == null) ? 'var' : '(${v.get}, ${v.set})'; + + errorList.push('Field "${foundField.name}" has different property access than in "$interfaceId": $clsFieldVarAccess should be "$interfaceFieldVarAccess"'); + } + if (v.isfinal != v2.isfinal) + { + errorList.push('Field "${foundField.name}" should be final as requested by "$interfaceId"'); + } + case KFunction(_): + // Field should be a var and not a function! + errorList.push('Field "${foundField.name}" should be "var" instead of "function" as requested by "$interfaceId"'); + } + case KFunction(f): + switch (foundField.kind) + { + case KFunction(f2): + if (f.args.length != f2.args.length) + { + errorList.push('"${foundField.name}" has different number of function arguments than in "${interfaceId}"'); + } + case KVar(_): + // Field should be a function and not a var! + errorList.push('Field "${foundField.name}" should be "function" instead of "var" as requested by "$interfaceId"'); + } + } + } + else + { + // Field wasn't able to be found, the class needs to implement it. + errorList.push('Field "${field.name}" needed by $interfaceId is missing.'); + } + } + } + return errorList; + } + + /** + * Retrieves all fields of this interface. + */ + public function getFields():InterfaceFields + { + if (_fieldsDecl != null) return _fieldsDecl; + + // `getBaseInterfaceField` is only called if the id isn't a scripted class, otherwise their caches are fetched instead. + _fieldsDecl = interfaceDecl != null ? getScriptInterfaceFields(id) : getBaseInterfaceFields(id); + return _fieldsDecl; + } + + /** + * Appends an interface to the given list. + * @param list The list to append the fields to. + * @param toAdd The interface fields to append. + */ + public function appendInterfaceToList(currentFieldList:InterfaceFields, toAdd:InterfaceFields):Void + { + var errorFields:Array = []; + + for (toAddInterfaceId => fields in toAdd) + { + // Make sure we only append fields of new interfaces to prevent redundency. + if (!currentFieldList.exists(toAddInterfaceId)) + { + var shouldAddToList:Bool = true; + + // Go through each field to make sure there's no duplicates, we throw an error otherwise. + for (listId => val in currentFieldList) + { + for (toAddField in fields) + { + for (listField in val) + { + // Error reporting for fields with the same name. + // Check to see if this field has the same name, but is a different kind of field. + if (toAddField.name == listField.name) + { + if (Type.enumConstructor(toAddField.kind) != Type.enumConstructor(listField.kind)) + { + errorFields.push('Field "${toAddField.name}" of "$toAddInterfaceId" has different property access than in "$toAddInterfaceId"'); + } + else + { + errorFields.push('Field "${toAddField.name}" of $listId already exists in "$toAddInterfaceId"'); + } + shouldAddToList = false; + } + } + } + } + + if (shouldAddToList) + { + currentFieldList.set(toAddInterfaceId, fields); + } + } + } + + if (errorFields.length > 0) + { + throw errorFields.join('\n'); + } + } + + public function getScriptInterfaceFields(key:String):InterfaceFields + { + var fieldsDecl = new InterfaceFields(); + fieldsDecl.set(key, interfaceDecl.fields); + + for (e in interfaceDecl.extend) + { + switch (e) + { + case CTPath(path, _): + var baseInterfaceName:String = path[path.length - 1]; + var fullName:String = interfaceDecl.imports?.get(baseInterfaceName)?.fullPath ?? path.join('.'); + + var extendFieldsList:InterfaceFields = new InterfaceFields(); + if (PolymodScriptClass.interfaceImpls.exists(fullName)) + { + // Fetch the base internal interface and retrieve its fields. + // These fields should be cached by now so we can just easily retrieve them. + var baseInterface = PolymodScriptClass.interfaceImpls.get(fullName); + extendFieldsList = baseInterface.getFields(); + } + else + { + extendFieldsList = getScriptInterfaceFields(fullName); + } + appendInterfaceToList(fieldsDecl, extendFieldsList); + default: + } + } + return fieldsDecl; + } + + public function getBaseInterfaceFields(key:String):InterfaceFields + { + var fields:InterfaceFields = new InterfaceFields(); + + var interfaceData:Array = PolymodScriptClassMacro.listInterfaceImpls().get(key); + var fieldsList:Array = interfaceData[0]; + var extendList:Array = interfaceData[1]; + + // Base fields for this interface. + fields.set(key, convertInterfaceFields(fieldsList)); + + for (extend in extendList) + { + var extendFieldsList:InterfaceFields = getBaseInterfaceFields(extend); + + appendInterfaceToList(fields, extendFieldsList); + } + return fields; + } + + public function toString():String + { + return 'PolymodStaticInterfaceReference($id)'; + } + + /** + * Converts a list of fields constructed from `PolymodScriptClassMacro` into regular field decls. + * Used to convert interfaces implemented in source code into runtime interfaces we're able to use. + * @param fields The list of fields to convert. + * @return Array + */ + static function convertInterfaceFields(fields:Array):Array + { + var fieldDecls:Array = []; + for (field in fields) + { + var accessList:Array = field.access; + var kind:String = field.kind; + var kindParams:Dynamic = field.kindParams; + + var fieldAccess:Array = []; + for (access in accessList) + { + switch (access) + { + case 'public': + fieldAccess.push(APublic); + case 'private': + fieldAccess.push(APrivate); + case 'static': + fieldAccess.push(AStatic); + } + } + + var fieldKind:FieldKind = switch (kind) + { + case 'var': + KVar({ + get: kindParams.get, + set: kindParams.set, + expr: null, // Shouldn't be defined in the interface, + type: null, + isfinal: kindParams.isFinal + }); + case 'method': + var functionArgs:Array = kindParams.args; + KFunction({ + args: [for (arg in functionArgs) { + name: arg, + t: null, + opt: null, + value: null + }], + expr: null, + ret: null, + }); + default: + null; + } + + var fieldDecl:FieldDecl = { + name: field.name, + meta: null, + kind: fieldKind, + access: fieldAccess + } + fieldDecls.push(fieldDecl); + } + return fieldDecls; + } +} diff --git a/polymod/util/MacroUtil.hx b/polymod/util/MacroUtil.hx index 84a4943c..d6f4b368 100644 --- a/polymod/util/MacroUtil.hx +++ b/polymod/util/MacroUtil.hx @@ -7,6 +7,26 @@ import haxe.macro.Type; class MacroUtil { #if macro + public static function listSuperInterfaces(classType:ClassType):Array + { + if (!classType.isInterface) return []; + + var superInterfaces:Array = []; + for (i in classType.interfaces) + { + superInterfaces.push(i.t.toString()); + + // Will recursively do this until there's no nothing extends. + var extend:Array = listSuperInterfaces(i.t.get()); + for (e in extend) + { + if (!superInterfaces.contains(e)) + superInterfaces.push(e); + } + } + return superInterfaces; + } + public static function implementsInterface(classType:ClassType, interfaceType:ClassType):Bool { for (i in classType.interfaces) From 4729b64aced998adee9615c8a6e35d907e971a4e Mon Sep 17 00:00:00 2001 From: nykwono Date: Tue, 25 Aug 2026 16:27:32 -0400 Subject: [PATCH 2/4] Fix extending a base interface causing an error, and extending script interfaces causing an infinite loop --- .../hscript/_internal/PolymodScriptClass.hx | 37 +++++++++++++++---- .../_internal/PolymodScriptClassMacro.hx | 2 +- .../PolymodStaticInterfaceReference.hx | 12 +++--- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/polymod/hscript/_internal/PolymodScriptClass.hx b/polymod/hscript/_internal/PolymodScriptClass.hx index 50b23e4e..88b3239a 100644 --- a/polymod/hscript/_internal/PolymodScriptClass.hx +++ b/polymod/hscript/_internal/PolymodScriptClass.hx @@ -261,7 +261,33 @@ class PolymodScriptClass return _abstractClassImpls; } - static var _interfaceImpls:Map; + static var _baseInterfaceClasses:Array = null; + + /** + * The list of source code base interfaces classes. + * Automatically populated at compile time. + */ + public static var baseInterfaceClasses(get, never):Array; + + static function get_baseInterfaceClasses():Array + { + if (_baseInterfaceClasses == null) + { + _baseInterfaceClasses = new Array(); + for (key in PolymodScriptClassMacro.listInterfaceImpls().keys()) + { + _baseInterfaceClasses.push(key); + } + } + return _baseInterfaceClasses; + } + + + static var _interfaceImpls:Map = null; + + /** + * A list of static references for all interface classes available at runtime. + */ public static var interfaceImpls(get, never):Map; static function get_interfaceImpls():Map @@ -270,19 +296,14 @@ class PolymodScriptClass { _interfaceImpls = new Map(); - var impls = PolymodScriptClassMacro.listInterfaceImpls(); - if (impls != null) + for (key in baseInterfaceClasses) { - for (key in impls.keys()) - { - _interfaceImpls.set(key, PolymodStaticInterfaceReference.tryBuild(key)); - } + _interfaceImpls.set(key, PolymodStaticInterfaceReference.tryBuild(key)); } } return _interfaceImpls; } - /** * Define a list of `typeName -> Class` which provides a reference to each typedef, * since typedefs can't be normally resolved at runtime. diff --git a/polymod/hscript/_internal/PolymodScriptClassMacro.hx b/polymod/hscript/_internal/PolymodScriptClassMacro.hx index 959da7b3..eeab83b6 100644 --- a/polymod/hscript/_internal/PolymodScriptClassMacro.hx +++ b/polymod/hscript/_internal/PolymodScriptClassMacro.hx @@ -135,7 +135,7 @@ class PolymodScriptClassMacro var staticFields:Array = classType.statics.get(); var extend:Array = [for (inter in classType.interfaces) inter.t.toString()]; - // We can't use the typedefs from `Expr` as they throw an error, so we have to define out own info to process later. + // We can't use the typedefs from `Expr` as they throw an error, so we have to define our own info to process later. var fieldDecls:Array = []; for (field in interfaceFields) diff --git a/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx b/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx index 3ac611fe..df18fd2e 100644 --- a/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx +++ b/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx @@ -91,7 +91,7 @@ class PolymodStaticInterfaceReference */ public static function tryBuild(id:String):PolymodStaticInterfaceReference { - if (!PolymodScriptClass.interfaceImpls.exists(id) && Interp.findScriptInterfaceDescriptor(id) == null) + if (!PolymodScriptClass.baseInterfaceClasses.contains(id) && Interp.findScriptInterfaceDescriptor(id) == null) { return null; } @@ -265,18 +265,20 @@ class PolymodStaticInterfaceReference public function getScriptInterfaceFields(key:String):InterfaceFields { var fieldsDecl = new InterfaceFields(); - fieldsDecl.set(key, interfaceDecl.fields); + var scriptDecl = Interp.findScriptInterfaceDescriptor(key); - for (e in interfaceDecl.extend) + fieldsDecl.set(key, scriptDecl.fields); + + for (e in scriptDecl.extend) { switch (e) { case CTPath(path, _): var baseInterfaceName:String = path[path.length - 1]; - var fullName:String = interfaceDecl.imports?.get(baseInterfaceName)?.fullPath ?? path.join('.'); + var fullName:String = scriptDecl.imports?.get(baseInterfaceName)?.fullPath ?? path.join('.'); var extendFieldsList:InterfaceFields = new InterfaceFields(); - if (PolymodScriptClass.interfaceImpls.exists(fullName)) + if (PolymodScriptClass.baseInterfaceClasses.contains(fullName)) { // Fetch the base internal interface and retrieve its fields. // These fields should be cached by now so we can just easily retrieve them. From 4b33caebdbf55fb51f31186ef8fe8addaebd854b Mon Sep 17 00:00:00 2001 From: nykwono Date: Wed, 26 Aug 2026 16:17:36 -0400 Subject: [PATCH 3/4] Rework validating interfaces to fix some issues. --- .../hscript/_internal/PolymodScriptClass.hx | 27 +++++++++++++------ .../PolymodStaticInterfaceReference.hx | 21 +++++++++++++-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/polymod/hscript/_internal/PolymodScriptClass.hx b/polymod/hscript/_internal/PolymodScriptClass.hx index 88b3239a..20f9a229 100644 --- a/polymod/hscript/_internal/PolymodScriptClass.hx +++ b/polymod/hscript/_internal/PolymodScriptClass.hx @@ -1215,34 +1215,45 @@ class PolymodScriptClass } else { - // We need to check that this interface isn't already extended through a super class. + // We need to check that this interface aren't already extended through a super class. // Else, this interface is redundant. - if (classesExtendingInterfaces.exists(fullyQualifiedName) && classesExtendingInterfaces.get(fullyQualifiedName).contains(ref.id)) + var superClasses:Array = getSuperClasses(_c); + for (cls in superClasses) { - continue; + if (classesExtendingInterfaces.exists(cls)) + { + // We can assume the super interfaces are satisfied as long as this top interface is. + if (classesExtendingInterfaces.get(cls).contains(ref.id)) + { + continue; + } + } } - var superInterfaceList:Array = []; + // We retrieve the current list of super interfaces to check that we don't accidentally implement a super interface to the class. + var currentSuperInterfaceList:Array = []; for (inter in _interfacesList) { - superInterfaceList = superInterfaceList.concat(inter.superInterfaces); + currentSuperInterfaceList = currentSuperInterfaceList.concat(inter.superInterfaces); } - // Don't append this interface if it's already being extended or if it's a parent of another. - if (!_interfacesList.exists(ref.id) && !superInterfaceList.contains(ref.id)) + // Don't append this interface if the class already implements it. + if (!_interfacesList.exists(ref.id) && !currentSuperInterfaceList.contains(ref.id)) { _interfacesList.set(ref.id, ref); } } } + var satisfiedList:Array = []; for (interfaceRef in _interfacesList) { - var errors:Array = interfaceRef.trySatisfy(_c); + var errors:Array = interfaceRef.trySatisfy(_c, satisfiedList); if (errors.length > 0) { throw errors.join('\n'); } + satisfiedList.push(interfaceRef); } } diff --git a/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx b/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx index df18fd2e..748f946c 100644 --- a/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx +++ b/polymod/hscript/_internal/PolymodStaticInterfaceReference.hx @@ -43,7 +43,15 @@ class PolymodStaticInterfaceReference function get_superInterfaces():Array { - return [for (i in getFields().keys()) i]; + var list:Array = []; + for (i in getFields().keys()) + { + if (i == id) + continue; + + list.push(i); + } + return list; } /** @@ -116,13 +124,22 @@ class PolymodStaticInterfaceReference * Iterates through each field of this interface to see whether this class meets all of the requirements. * If it doesn't, a list of errors will be thrown. * @param cls The class to check. + * @param satisfied The list of interfaces that this class has already satisifed. Only really used to prevent redundant checks for interfaces we don't need to. * @return A list of errors needed to be resolved. */ - public function trySatisfy(cls:ClassDecl):Array + public function trySatisfy(cls:ClassDecl, ?satisfied:Array):Array { + satisfied ??= []; + var errorList:Array = []; for (interfaceId => interfaceFields in getFields()) { + // If this is an interface that the scripted class already satisfied, continue to the next one, no need to check again. + if (satisfied.length > 0 && satisfied.findIndex((inter:PolymodStaticInterfaceReference) -> return [inter.id].concat(inter.superInterfaces).contains(interfaceId)) != -1) + { + continue; + } + for (field in interfaceFields) { var foundField:Null = cls.fields.find((clsField:FieldDecl) -> return clsField.name == field.name); From ce8149000ec9ae42f93ac72b7016513835f0af33 Mon Sep 17 00:00:00 2001 From: nykwono Date: Tue, 1 Sep 2026 16:50:55 -0400 Subject: [PATCH 4/4] Allow wildcards to work with interfaces --- polymod/hscript/_internal/Interp.hx | 47 +++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/polymod/hscript/_internal/Interp.hx b/polymod/hscript/_internal/Interp.hx index 057f367d..a38b4e6f 100644 --- a/polymod/hscript/_internal/Interp.hx +++ b/polymod/hscript/_internal/Interp.hx @@ -3193,12 +3193,31 @@ class Interp if (!pkg.startsWith(key) && key.length != 0) continue; for (imp in imps) - inter.imports.set(imp.name, imp); + { + if (imp.wildcard) + { + for (name => clsImport in importWildcard(inter.imports, imp)) + { + inter.imports.set(name, clsImport); + } + } + else + inter.imports.set(imp.name, imp); + } } // Add validated imports. for (key => imp in inter.importsToValidate) { + if (imp.wildcard) + { + for (name => clsImport in importWildcard(inter.imports, imp)) + { + inter.imports.set(name, clsImport); + } + continue; + } + if (PolymodScriptClass.interfaceImpls.exists(imp.fullPath) || _scriptInterfaceDescriptors.exists(imp.fullPath) || _scriptClassDescriptors.exists(imp.fullPath) || _scriptEnumDescriptors.exists(imp.fullPath)) { @@ -3337,7 +3356,12 @@ class Interp for (imp in imps) { if (imp.wildcard) - importWildcard(cls, imp); + { + for (name => clsImport in importWildcard(cls.imports, imp)) + { + cls.imports.set(name, clsImport); + } + } else cls.imports.set(imp.name, imp); } @@ -3355,7 +3379,10 @@ class Interp { if (imp.wildcard) { - importWildcard(cls, imp); + for (name => clsImport in importWildcard(cls.imports, imp)) + { + cls.imports.set(name, clsImport); + } continue; } @@ -3432,7 +3459,7 @@ class Interp validateInterfaceImports(); } - static function importWildcard(cls:ClassDecl, wildcardImport:ClassImport):Void + static function importWildcard(importList:Map, wildcardImport:ClassImport):Map { var pack:String = wildcardImport.fullPath; var classesToImport:Array = []; @@ -3444,15 +3471,16 @@ class Interp classesToImport = classesToImport.concat(PolymodScriptClass.scriptClassesByPackage.get(pack)); if (classesToImport.length == 0) - return; + return []; + var validImports:Map = []; for (clsName in classesToImport) { var name:String = clsName.substr(pack.length + 1); - if (cls.imports.exists(name)) + if (importList.exists(name)) { - if (cls.imports.get(name) == null) + if (importList.get(name) == null) { Polymod.error(SCRIPTED_CLASS_BLACKLISTED_MODULE, 'Scripted class ${name} is blacklisted and cannot be used in scripts.', SCRIPT_RUNTIME); } @@ -3477,12 +3505,13 @@ class Interp // Check if this is a scripted class. if (_scriptClassDescriptors.exists(classImport.fullPath) || _scriptEnumDescriptors.exists(classImport.fullPath)) { - cls.imports.set(classImport.name, classImport); + validImports.set(classImport.name, classImport); continue; } } - cls.imports.set(classImport.name, classImport); + validImports.set(classImport.name, classImport); } + return validImports; } /**