Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build Macro Compilation Error: Class has no field #11815

Open
lemz1 opened this issue Nov 9, 2024 · 0 comments
Open

Build Macro Compilation Error: Class has no field #11815

lemz1 opened this issue Nov 9, 2024 · 0 comments

Comments

@lemz1
Copy link

lemz1 commented Nov 9, 2024

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<funkin.data.freeplay.album.AlbumRegistry> has no field instance
Album
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<AlbumData>
{
  /**
   * 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<String>
  {
    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<AnimationData>
  {
    return _data.albumTitleAnimations;
  }

  public function toString():String
  {
    return 'Album($id)';
  }

  public function destroy():Void {}

  static function _fetchData(id:String):Null<AlbumData>
  {
    return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id));
  }
}
AlbumRegistry
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<Album, AlbumData>
{
  /**
   * 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<AlbumData>
  {
    // JsonParser does not take type parameters,
    // otherwise this function would be in BaseRegistry.
    var parser:json2object.JsonParser<AlbumData> = new json2object.JsonParser<AlbumData>();
    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<AlbumData>
  {
    var parser:json2object.JsonParser<AlbumData> = new json2object.JsonParser<AlbumData>();
    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<String>
  {
    return ScriptedAlbum.listScriptClasses();
  }
}
RegistryMacro
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<Field>
  {
    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<Field>
  {
    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<String> = [];
    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<ClassType>)
    {
      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<Dynamic> = [];
        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<Field>
  {
    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<Field>
  {
    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<Field>
  {
    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<Field>
  {
    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant