HaxeFoundation / haxe

Haxe - The Cross-Platform Toolkit
https://haxe.org
6.2k stars 658 forks source link

Build Macro Compilation Error: Class has no field #11815

Open lemz1 opened 2 weeks ago

lemz1 commented 2 weeks ago

Reference Repository

https://github.com/lemz1/Funkin/tree/feature/registry-marco-broken

Error and Relevant Classes

Error ``` source/Prebuild.hx:15: Building... source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Song source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: SongRegistry source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: PlayableCharacter source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: PlayerRegistry source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: AlbumRegistry source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Album source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: Conversation source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: ConversationRegistry source/funkin/util/macro/RegistryMacro.hx:46: ENTRY: NoteStyle source/funkin/util/macro/RegistryMacro.hx:24: REGISTRY: NoteStyleRegistry ERROR source/funkin/ui/freeplay/Album.hx:99: characters 26-34 99 | return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id)); | ^^^^^^^^ | Class has no field instance ```
Album ```haxe package funkin.ui.freeplay; import funkin.data.freeplay.album.AlbumData; import funkin.data.freeplay.album.AlbumRegistry; import funkin.data.animation.AnimationData; import funkin.data.IRegistryEntry; import flixel.graphics.FlxGraphic; /** * A class representing the data for an album as displayed in Freeplay. */ @:build(funkin.util.macro.RegistryMacro.buildEntry()) class Album implements IRegistryEntry { /** * The internal ID for this album. */ public final id:String; /** * The full data for an album. */ public final _data:AlbumData; public function new(id:String) { this.id = id; this._data = _fetchData(id); if (_data == null) { throw 'Could not parse album data for id: $id'; } } /** * Return the name of the album. * @ */ public function getAlbumName():String { return _data.name; } /** * Return the artists of the album. * @return The list of artists */ public function getAlbumArtists():Array { return _data.artists; } /** * Get the asset key for the album art. * @return The asset key */ public function getAlbumArtAssetKey():String { return _data.albumArtAsset; } /** * Get the album art as a graphic, ready to apply to a sprite. * @return The built graphic */ public function getAlbumArtGraphic():FlxGraphic { return FlxG.bitmap.add(Paths.image(getAlbumArtAssetKey())); } /** * Get the asset key for the album title. */ public function getAlbumTitleAssetKey():String { return _data.albumTitleAsset; } public function hasAlbumTitleAnimations() { return _data.albumTitleAnimations.length > 0; } public function getAlbumTitleAnimations():Array { return _data.albumTitleAnimations; } public function toString():String { return 'Album($id)'; } public function destroy():Void {} static function _fetchData(id:String):Null { return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id)); } } ```
AlbumRegistry ```haxe package funkin.data.freeplay.album; import funkin.ui.freeplay.Album; import funkin.data.freeplay.album.AlbumData; import funkin.ui.freeplay.ScriptedAlbum; @:build(funkin.util.macro.RegistryMacro.buildRegistry()) class AlbumRegistry extends BaseRegistry { /** * The current version string for the album data format. * Handle breaking changes by incrementing this value * and adding migration to the `migrateAlbumData()` function. */ public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.0'; public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x'; public static final instance:AlbumRegistry = new AlbumRegistry(); public function new() { super('ALBUM', 'ui/freeplay/albums', ALBUM_DATA_VERSION_RULE); } /** * Read, parse, and validate the JSON data and produce the corresponding data object. * @param id The ID of the entry to load. * @return The parsed data object. */ public function parseEntryData(id:String):Null { // JsonParser does not take type parameters, // otherwise this function would be in BaseRegistry. var parser:json2object.JsonParser = new json2object.JsonParser(); parser.ignoreUnknownVariables = false; switch (loadEntryFile(id)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); default: return null; } if (parser.errors.length > 0) { printErrors(parser.errors, id); return null; } return parser.value; } /** * Parse and validate the JSON data and produce the corresponding data object. * * NOTE: Must be implemented on the implementation class. * @param contents The JSON as a string. * @param fileName An optional file name for error reporting. * @return The parsed data object. */ public function parseEntryDataRaw(contents:String, ?fileName:String):Null { var parser:json2object.JsonParser = new json2object.JsonParser(); parser.ignoreUnknownVariables = false; parser.fromJson(contents, fileName); if (parser.errors.length > 0) { printErrors(parser.errors, fileName); return null; } return parser.value; } function createScriptedEntry(clsName:String):Album { return ScriptedAlbum.init(clsName, 'unknown'); } function getScriptedClassNames():Array { return ScriptedAlbum.listScriptClasses(); } } ```
RegistryMacro ```haxe package funkin.util.macro; import haxe.macro.Context; import haxe.macro.Expr; import haxe.macro.Type; using haxe.macro.ExprTools; using haxe.macro.TypeTools; using StringTools; class RegistryMacro { public static macro function buildRegistry():Array { var fields = Context.getBuildFields(); var cls = Context.getLocalClass().get(); if (!cls.name.endsWith('Registry')) { throw '${cls.module}.${cls.name} needs to end with "Registry"'; } trace('REGISTRY: ${cls.name}'); var typeParams = getTypeParams(cls); var entryCls = typeParams.entryCls; var jsonCls = typeParams.jsonCls; var scriptedEntryCls = getScriptedEntryClass(entryCls); fields = fields.concat(buildRegistryVariables(cls)); fields = fields.concat(buildRegistryMethods(cls)); buildEntryImpl(entryCls, cls); buildRegistryImpl(cls, entryCls, scriptedEntryCls, jsonCls); return fields; } public static macro function buildEntry():Array { var fields = Context.getBuildFields(); var cls = Context.getLocalClass().get(); trace('ENTRY: ${cls.name}'); var entryData = getEntryData(cls); // since the registries also use a build macro // the fields aren't callable unless we first get // the class type of the registry makeFieldsCallable(cls); fields = fields.concat(buildEntryVariables(cls, entryData)); fields = fields.concat(buildEntryMethods(cls)); return fields; } #if macro static function makeFieldsCallable(cls:ClassType) { // TODO: lets not have this if statement // like what the hell is wrong with this // doing this if statement fixes the order // for the song build macros if (cls.name == 'Song') { MacroUtil.getClassTypeFromExpr(macro funkin.data.song.SongRegistry); return; } var registries:Array = []; for (localImport in Context.getLocalImports()) { var names = []; for (path in localImport.path) { names.push(path.name); } var fullName = names.join('.'); if (fullName.endsWith('Registry')) { registries.push(fullName); } } for (registry in registries) { MacroUtil.getClassTypeFromExpr(Context.parse(registry, Context.currentPos())); } } static function fieldAlreadyExists(name:String):Bool { for (field in Context.getBuildFields()) { if (field.name == name && !((field.access ?? []).contains(Access.AAbstract))) { return true; } } function fieldAlreadyExistsSuper(name:String, superClass:Null) { if (superClass == null) { return false; } for (field in superClass.fields.get()) { if (field.name == name && !field.isAbstract) { return true; } } // recursively check superclasses return fieldAlreadyExistsSuper(name, superClass.superClass?.t.get()); } return fieldAlreadyExistsSuper(name, Context.getLocalClass().get().superClass?.t.get()); } static function getTypeParams(cls:ClassType):RegistryTypeParamsNew { switch (cls.superClass.t.get().kind) { case KGenericInstance(_, params): var typeParams:Array = []; for (param in params) { switch (param) { case TInst(t, _): typeParams.push(t.get()); case TType(t, _): typeParams.push(t.get()); default: throw 'Not a class'; } } return {entryCls: typeParams[0], jsonCls: typeParams[1]}; default: throw 'Not in the correct format'; } } static function getScriptedEntryClass(entryCls:ClassType):ClassType { var scriptedEntryClsName = entryCls.pack.join('.') + '.Scripted' + entryCls.name; switch (Context.getType(scriptedEntryClsName)) { case Type.TInst(t, _): return t.get(); default: throw 'Not A Class (${scriptedEntryClsName})'; }; } static function getEntryData(cls:ClassType):Dynamic // DefType or ClassType { switch (cls.interfaces[0].params[0]) { case Type.TInst(t, _): return t.get(); case Type.TType(t, _): return t.get(); default: throw 'Entry Data is not a class or typedef'; } } static function buildRegistryVariables(cls:ClassType):Array { var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType(); var newInstance:String = 'new ${cls.module}.${cls.name}()'; return (macro class TempClass { static var _instance:Null<$clsType>; public static var instance(get, never):$clsType; static function get_instance():$clsType { if (_instance == null) { _instance = ${Context.parse(newInstance, Context.currentPos())}; } return _instance; } }).fields.filter((field) -> return !fieldAlreadyExists(field.name)); } static function buildRegistryMethods(cls:ClassType):Array { var impl:String = 'funkin.macro.impl._${cls.name}_Impl'; return (macro class TempClass { function getScriptedClassNames() { return ${Context.parse(impl, Context.currentPos())}.getScriptedClassNames(this); } function createScriptedEntry(clsName:String) { return ${Context.parse(impl, Context.currentPos())}.createScriptedEntry(this, clsName); } public function parseEntryData(id:String) { return ${Context.parse(impl, Context.currentPos())}.parseEntryData(this, id); } public function parseEntryDataRaw(contents:String, ?fileName:String) { return ${Context.parse(impl, Context.currentPos())}.parseEntryDataRaw(this, contents, fileName); } }).fields.filter((field) -> return !fieldAlreadyExists(field.name)); } static function buildEntryVariables(cls:ClassType, entryData:Dynamic):Array { var entryDataType:ComplexType = Context.getType('${entryData.module}.${entryData.name}').toComplexType(); return (macro class TempClass { public final id:String; public final _data:Null<$entryDataType>; }).fields.filter((field) -> return !fieldAlreadyExists(field.name)); } static function buildEntryMethods(cls:ClassType):Array { var impl:String = 'funkin.macro.impl._${cls.name}_Impl'; return (macro class TempClass { public function _fetchData(id:String) { return ${Context.parse(impl, Context.currentPos())}._fetchData(this, id); } public function toString() { return ${Context.parse(impl, Context.currentPos())}.toString(this); } public function destroy() { ${Context.parse(impl, Context.currentPos())}.destroy(this); } }).fields.filter((field) -> !fieldAlreadyExists(field.name)); } static function buildRegistryImpl(cls:ClassType, entryCls:ClassType, scriptedEntryCls:ClassType, jsonCls:Dynamic):Void { var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType(); var getScriptedClassName:String = '${scriptedEntryCls.module}.${scriptedEntryCls.name}'; var createScriptedEntry:String = '${scriptedEntryCls.module}.${scriptedEntryCls.name}.init(clsName, "unknown")'; var newJsonParser:String = 'new json2object.JsonParser<${jsonCls.module}.${jsonCls.name}>()'; Context.defineType( { pos: Context.currentPos(), pack: ['funkin', 'macro', 'impl'], name: '_${cls.name}_Impl', kind: TypeDefKind.TDClass(null, [], false, false, false), fields: (macro class TempClass { public static inline function getScriptedClassNames(me:$clsType) { return ${Context.parse(getScriptedClassName, Context.currentPos())}.listScriptClasses(); } public static inline function createScriptedEntry(me:$clsType, clsName:String) { return ${Context.parse(createScriptedEntry, Context.currentPos())}; } public static inline function parseEntryData(me:$clsType, id:String) { var parser = ${Context.parse(newJsonParser, Context.currentPos())}; parser.ignoreUnknownVariables = false; @:privateAccess switch (me.loadEntryFile(id)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); default: return null; } if (parser.errors.length > 0) { @:privateAccess me.printErrors(parser.errors, id); return null; } return parser.value; } public static inline function parseEntryDataRaw(me:$clsType, contents:String, ?fileName:String) { var parser = ${Context.parse(newJsonParser, Context.currentPos())}; parser.ignoreUnknownVariables = false; parser.fromJson(contents, fileName); if (parser.errors.length > 0) { @:privateAccess me.printErrors(parser.errors, fileName); return null; } return parser.value; } }).fields }); } static function buildEntryImpl(cls:ClassType, registryCls:ClassType):Void { var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType(); var registry:String = '${registryCls.module}.${registryCls.name}'; Context.defineType( { pos: Context.currentPos(), pack: ['funkin', 'macro', 'impl'], name: '_${cls.name}_Impl', kind: TypeDefKind.TDClass(null, [], false, false, false), fields: (macro class TempClass { public static inline function _fetchData(me:$clsType, id:String) { return $ { Context.parse(registry, Context.currentPos()) }.instance.parseEntryDataWithMigration(id, ${Context.parse(registry, Context.currentPos())}.instance.fetchEntryVersion(id)); } public static inline function toString(me:$clsType) { return $v{cls.name} + '(' + me.id + ')'; } public static inline function destroy(me:$clsType) {} }).fields }); } #end } #if macro typedef RegistryTypeParamsNew = { var entryCls:ClassType; var jsonCls:Dynamic; // DefType or ClassType } #end ```

Information

Haxe: 4.3.4 Platform: Windows Target: cpp

the RegistryMacro creates fields for an Entry or Registry if not yet defined

this works fine if the order of the build macro is Entry -> Registry (atleast i assume)

as you can see in the error when the Album and AlbumRegistry gets built, it builds the AlbumRegistry first and then Album, and i assume this is the problem

so my question is if there is a way to make the build order not matter, or if there is a way to determine the build order