airsdk / Adobe-Runtime-Support

Report, track and discuss issues in Adobe AIR. Monitored by Adobe - and HARMAN - and maintained by the AIR community.
197 stars 11 forks source link

Getting class reference at static constructor #3334

Open ylazy opened 3 days ago

ylazy commented 3 days ago

Main class Test.as

package
{
    import flash.display.Sprite;

    [SWF(width="800", height="600", backgroundColor="#FFFFFF", frameRate="60")]

    public class Test extends Sprite
    {
        public function Test():void
        {
            SubClass;
        }
    }
}

class SubClass.as

package
{
    public class SubClass extends SuperClass
    {
        // calling static method test at static constructor
        {test; test2(SubClass);}
    }
}

class SuperClass.as

package
{
    import flash.utils.getDefinitionByName;

    public class SuperClass
    {
        protected static function get test():*
        {
            // Error
            //  at SuperClass$/get test()[Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SuperClass.as:20]
            //  at SubClass$cinit()[Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SubClass.as:0]
            //  at global$init()
            //  at Test()[Y:\Development\ActionScript\Projects\VSCode\src\Test.as:0]
            //  at runtime::ContentPlayer/loadInitialContent()
            //  at runtime::ContentPlayer/playRawContent()
            //  at runtime::ContentPlayer/playContent()
            //  at runtime::AppRunner/run()
            //  at ADLAppEntry/run()

            var stackTrace:String = new Error().getStackTrace();

            trace("stackTrace");
            trace(stackTrace);

            //  at SubClass$cinit()[Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SubClass.as:0]

            var line2:String = stackTrace.split("\n")[2];

            trace("line2");
            trace(line2);

            var beforeQualifiedClassName:int = line2.indexOf("at ") + 3;
            var afterQualifiedClassName:int = line2.indexOf("$cinit");

            // SubClass

            var qualifiedClassName:String = line2.slice(beforeQualifiedClassName, afterQualifiedClassName);

            trace("qualifiedClassName");
            trace(qualifiedClassName);

            // null

            var classReference:* = getDefinitionByName(qualifiedClassName);

            trace("classReference");
            trace(classReference);
        }

        protected static function test2(input:Class):*
        {
            trace("input");
            trace(input); // [class SubClass]
        }
    }
}

The expected result of classReference shoud be [class SubClass]. The actual result is null.

I don't know whether this's a bug or not. Maybe I got the null result because of the SubClass was not initialized (because the static constructor was not complete, so the static class reference SubClass was not available to use). But, if so, then the input must also be null (but I got [class SubClass]). So... maybe Harman can make some changes to make getDefinitionByName() work for this case?

I really need this feature. If this issue can be resolved, I can make Enum feature available for AS3, just simple like bellow:

package
{
    public class PhoneOS extends Enum
    {
        {enum}

        public static var ANDROID:PhoneOS, IOS:PhoneOS;
    }
}

and I can also make more {tag} to expanding AS3 features.

Btw I found a minor bug in the stack traces:

Error
    at SuperClass$/get test()[Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SuperClass.as:20]
    at SubClass$cinit()[Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SubClass.as:0] // wrong line number
    at global$init()
    at Test()[Y:\Development\ActionScript\Projects\VSCode\src\Test.as:0] // wrong line number
...

Thanks!

ajwfrost commented 3 days ago

Hi

I suspect that the failure of getDefinitionByName is, as you say, because the initialization hasn't finished: specifically, setting the class definition as a property of the global script object hasn't been completed. Whereas, when you reference the class name directly, it's looking at the scope stack instead.

Which might mean that we can adjust getDefinitionByName to also search the scope stack in case it doesn't find the definition in the global script...

For that stack trace, I'm not sure what line number you would expect to see there? It's the 'global' initialisation for that file, not actually the class initialiser .. and it would be the compiler that's at fault really, it hasn't generated a debugline instruction but I guess there's a debugfile already.

ylazy commented 3 days ago

Which might mean that we can adjust getDefinitionByName to also search the scope stack in case it doesn't find the definition in the global script...

I'm happy to hear that it's possible.

For that stack trace, I'm not sure what line number you would expect to see there?

If possible, it would be nice to see:

Error
    at SuperClass$/get test()[Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SuperClass.as:20]
    at SubClass$cinit()[Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SubClass.as:6]
    at global$init()
    at Test()[Y:\Development\ActionScript\Projects\VSCode\src\Test.as:11]

Thank you!

ajwfrost commented 2 days ago

Re the line number: when I decompile:

     function Main():*  /* disp_id=-1 method_id=1 nameIndex = 1 */
     {
       // local_count=1 max_scope=1 max_stack=1 code_len=12
       // method position=903 code position=1049
       0    getlocal0       
       1    pushscope       
       2    debugfile       "I:\Adobe\AIR\bugs\Github3334\src;;Main.as"  //stringIndex = 10
       4    getlocal0       
       5    constructsuper  (0)
       7    getlex          SubClass //nameIndex = 3
       9    debugline       14
       11   returnvoid      
     }

So, the debugline is coming after the getlex call which is triggering all this...

Similar thing if we just have a constructor with line trace("Test");:

       7    getlex          trace //nameIndex = 3
       9    getglobalscope  
       10   debugline       13
       12   pushstring      "Test"  //stringIndex = 53
       14   call            (1)

Will have to look further at the compiler here, to try to ensure the debugline instruction gets pushed out earlier.

ajwfrost commented 2 days ago

Regarding the getDefinitionByName failure:

     11     newclass        SubClass
     13     popscope        
     14     popscope        
     15     initproperty    SubClass //nameIndex = 3

The subclass is being created on line 11, but not set into the script slot until line 15. Which is why, if we try finding it by name before that point (i.e. within the static initializers that are called within that newclass function) we won't then find it.

The other usage of it is done within its own class initializer:

     static function SubClass$cinit():* /* disp_id=0 method_id=9 nameIndex = 0 */
     { ...
       0    getlocal0       
       1    pushscope       
       16   getlex          SuperClass::test //nameIndex = 9
...
       25   getlocal0       
       28   callpropvoid    SuperClass::test2 (1) //nameIndex = 10
     }

in other words, the call to test2(SubClass) isn't finding the SubClass class definition using its name, it just knows that it's the same as this i.e. local0 and so it passing that in for the argument.

I still think that we should be able to pick up the scope that contains this SubClass class definition, within that test function, but currently we're struggling to find how to access/traverse down to the appropriate scope...!

ajwfrost commented 2 days ago

Okay -> a bit more investigation, and it sounds like we wouldn't be able to pick this up after all, without some additional hacks / caching of values within the runtime (i.e. within the newclass handler, we would need to store a reference to the class definition, so that we can check for it specifically and return it from the getDefinitionByName call..)

But .. I'm not so sure this is a good idea. If you look at what "works", i.e. that use of test2(SubClass), it doesn't actually pick up the class when it's initialised. So you run the risk of odd behaviour.

For example: If your SubClass file contained:

        static private var _i : uint;
        static public function get i() : uint { return _i };

        // calling static method test at static constructor
        {
            trace("SubClass static initializer");
            test;
            test2(SubClass);
            _i = 5;
        }

Then in your SuperClass method, you had:

        protected static function test2(input:Class):*
        {
            trace("input");
            trace(input); // [class SubClass]
            trace("i = " + input.i);
        }

it gives you '0', because that _i value has not yet been initialised. That's just a trivial example, but it would be possible to get into a real problem with this.

You mentioned earlier that this capability would allow you to create enums via:

package
{
    public class PhoneOS extends Enum
    {
        {enum}      
        public static var ANDROID:PhoneOS, IOS:PhoneOS;
    }
}

I guess it depends a little on what exactly you're doing/how, but I would have considered enums to be better as a set of integer values rather than object instances.. I need to go back and review all the info / requests about enum though, but it's something we could perhaps look at implementing a different way?

ylazy commented 1 day ago

Sorry for late response.

I guess it depends a little on what exactly you're doing/how, but I would have considered enums to be better as a set of integer values rather than object instances.. I need to go back and review all the info / requests about enum though, but it's something we could perhaps look at implementing a different way?

I think an Enum should be a class instance, not a Number/Int value, because in real uses, we need Type Safety, and we need Enum instances that can expanding to work as a normal class instance.

For example:

class ControlSize.as

package x.controls.base
{
    public class ControlSize extends Enum
    {

        public static const FREE    :ControlSize    =   new ControlSize();

        /**
         * eXtra eXtra Small
         */
        public static const XXS     :ControlSize    =   new ControlSize();

        /**
         * eXtra Small
         */
        public static const XS      :ControlSize    =   new ControlSize();

        /**
         * Small
         */
        public static const S       :ControlSize    =   new ControlSize();

        /**
         * Medium - default
         */
        public static const M       :ControlSize    =   new ControlSize();

        /**
         * Large
         */
        public static const L       :ControlSize    =   new ControlSize();

        /**
         * eXtra Large
         */
        public static const XL      :ControlSize    =   new ControlSize();

        /**
         * eXtra eXtra Large
         */
        public static const XXL     :ControlSize    =   new ControlSize();

        /**
         * eXtra eXtra eXtra Large
         */
        public static const XXXL    :ControlSize    =   new ControlSize();
    }
}

class ControlSizeRange.as

package x.controls.base
{
    public class ControlSizeRange extends Enum
    {
        {enum(ControlSizeRange)}

        public static const S2L     :ControlSizeRange   =   new ControlSizeRange(ControlSize.S, ControlSize.L);
        public static const S2XL    :ControlSizeRange   =   new ControlSizeRange(ControlSize.S, ControlSize.XL);
        public static const XS2XL   :ControlSizeRange   =   new ControlSizeRange(ControlSize.XS, ControlSize.XL);
        public static const M       :ControlSizeRange   =   new ControlSizeRange(ControlSize.M, ControlSize.M);
        public static const FREE    :ControlSizeRange   =   new ControlSizeRange(ControlSize.FREE, ControlSize.FREE);

        private var _minSize        :ControlSize;
        private var _maxSize        :ControlSize;

        public function ControlSizeRange(minSize:ControlSize, maxSize:ControlSize):void
        {
            _minSize = minSize;
            _maxSize = maxSize;

            super();
        }

        public function get minSize():ControlSize
        {
            return _minSize;
        }

        public function get maxSize():ControlSize
        {
            return _maxSize;
        }

        public function has(size:ControlSize = null):Boolean
        {
            if (size == null) return false;
            if (this == FREE) return true;

            return _minSize <= size && size <= _maxSize;
        }
    }
}

Usage:

class Component.as

package x.controls
{
    public class Component extends Container implements IComponent, IOptions
    {
        public function Component(size:ControlSize = null, supportedSizes:ControlSizeRange = null, visible:Boolean = true):void
        {
            initializeControlSize(size, supportedSizes);

            super(visible);
        }

        protected function initializeControlSize(size:ControlSize = null, supportedSizes:ControlSizeRange = null):void
        {
            if (size == null) size = ControlSize.M;
            if (supportedSizes == null) supportedSizes = ControlSizeRange.FREE;

            if (!supportedSizes.has(size))
            {
                throw new Error("Unsupported size: " + size.name + " (" + size.index + ")" + ". Supported sizes: " + supportedSizes.name + "[" + supportedSizes.minSize.index + ", " + supportedSizes.maxSize.index + "]");
            }

            _controlSize = size;
        }
    }
}

class Control.as

package x.controls
{
    public class Control extends Component implements IControl
    {
        public function Control(size:ControlSize = null, supportedSizes:ControlSizeRange = null, visible:Boolean = true):void
        {
            super(size, supportedSizes, visible);
        }
    }
}

class ActionButton.as

package x.controls
{
    public class ActionButton extends Control
    {
        public function ActionButton(size:ControlSize = null):void
        {
            super(size, ControlSizeRange.XS2XL);
        }
    }
}
var button:ActionButton = new ActionButton(ControlSize.M);

with the Enum class that I've built, each enum can be instantiated automatically:

class ControlTheme.as

package x.controls.base
{
    public class ControlTheme extends Enum
    {
        {enum(ControlTheme)}

        public static var LIGHT     :ControlTheme;
        public static var DARK      :ControlTheme;
        public static var DARKEST   :ControlTheme;

        public static function fromName(name:String):ControlTheme
        {
            return Enum.fromName(name, ControlTheme);
        }
    }

}

Usage:

trace(ControlTheme.LIGHT); // LIGHT
trace(ControlTheme.DARK); // DARK
trace(ControlTheme.DARKEST); // DARKEST
trace(ControlTheme.LIGHT.index); // 0
trace(ControlTheme.DARK.index); // 1
trace(ControlTheme.DARKEST.index); // 2
trace(ControlTheme.LIGHT.name); // LIGHT
trace(ControlTheme.DARK.name); // DARK
trace(ControlTheme.DARKEST.name); // DARKEST
var theme:ControlTheme = ControlTheme.fromName("DARK");

You can customize the name:

package x.net
{
    public class WebHyperlinkTargetWindow extends Enum
    {
        {enum(WebHyperlinkTargetWindow)}

        public static const SELF    :WebHyperlinkTargetWindow       =   new WebHyperlinkTargetWindow("_self");
        public static const BLANK   :WebHyperlinkTargetWindow       =   new WebHyperlinkTargetWindow("_blank");
        public static const PARENT  :WebHyperlinkTargetWindow       =   new WebHyperlinkTargetWindow("_parent");
        public static const TOP     :WebHyperlinkTargetWindow       =   new WebHyperlinkTargetWindow("_top");

        public function WebHyperlinkTargetWindow(customName:String = null):void
        {
            super(NaN, customName);
        }
    }
}
trace(WebHyperlinkTargetWindow.BLANK); // _blank
trace(WebHyperlinkTargetWindow.BLANK.toString()); // _blank
trace(WebHyperlinkTargetWindow.BLANK.name); // _blank

customizing the index:

package
{
    public class Comparison extends Enum
    {
        {enum(Comparison)}

        public static const LESS_THAN       :Comparison     =   new Comparison(-1);
        public static const EQUAL       :Comparison     =   new Comparison(0);
        public static const GREATER_THAN    :Comparison     =   new Comparison(1);

        public function Comparison(index:Number = NaN):void
        {
            super(index);
        }

        public static function fromIndex(index:Number):Comparison
        {
            return Enum.fromIndex(index, Comparison);
        }

        public static function fromName(name:String):Comparison
        {
            return Enum.fromName(name, Comparison);
        }
    }
}

usage:

public function compare(str1:String, str2:String):Comparison
{
    if (str1 == str2)
    {
        return Comparison.EQUAL;
    }
    else if (str1 > str2)
    {
        return Comparison.GREATER_THAN;
    }
    else
    {
        return Comparison.LESS_THAN;
    }
}

trace(Comparison.GREATER_THAN.index); // 1
trace(Comparison.GREATER_THAN.valueOf()); // 1
trace(["c", "a", "b"].sort(compare)); // a,b,c

In fact I've built a full-featured Enum class and I'm using it everyday in official products. If this issue can be resolved, I can shorten the initialization:

package
{
    public class PhoneOS extends Enum
    {
        {enum(PhoneOS)}     
        public static var ANDROID:PhoneOS, IOS:PhoneOS;
    }
}

will be

package
{
    public class PhoneOS extends Enum
    {
        {enum}
        public static var ANDROID:PhoneOS, IOS:PhoneOS;
    }
}
ylazy commented 1 day ago

Another Example:

class UserState.as

package x.controls.states
{
    public class UserState extends Enum
    {
        {enum(UserState)}

        public static var NO_FOCUS_OUT      :UserState;

        public static var NO_FOCUS_OVER     :UserState;

        public static var NO_FOCUS_DOWN     :UserState;

        public static var KEY_FOCUS_OVER    :UserState;

        public static var KEY_FOCUS_DOWN    :UserState;

        public static var PRESS_FOCUS_DOWN  :UserState;

        public static var PRESS_FOCUS_OVER  :UserState;

        public static var PRESS_FOCUS_OUT   :UserState;

        public function toPointerState():PointerState
        {
            if (this == NO_FOCUS_OUT || this == PRESS_FOCUS_OUT)
            {
                return PointerState.OUT;
            }
            else if (this == NO_FOCUS_OVER || this == PRESS_FOCUS_OVER || this == KEY_FOCUS_OVER)
            {
                return PointerState.OVER;
            }
            else if (this == NO_FOCUS_DOWN || this == PRESS_FOCUS_DOWN || this == KEY_FOCUS_DOWN)
            {
                return PointerState.DOWN;
            }

            return null;
        }

        public function toFocusState():FocusState
        {
            if (this == NO_FOCUS_OUT || this == NO_FOCUS_OVER || this == NO_FOCUS_DOWN)
            {
                return FocusState.NO_FOCUS;
            }
            else if (this == PRESS_FOCUS_DOWN || this == PRESS_FOCUS_OVER || this == PRESS_FOCUS_OUT)
            {
                return FocusState.PRESS_FOCUS;
            }
            else if (this == KEY_FOCUS_OVER || this == KEY_FOCUS_DOWN)
            {
                return FocusState.KEY_FOCUS;
            }

            return null;
        }

    }
}

class PointerState.as

package x.controls.states
{
    public class PointerState extends Enum
    {
        {enum(PointerState)}

        public static var OUT   :PointerState;
        public static var OVER  :PointerState;
        public static var DOWN  :PointerState;
    }
}

class FocusState.as

package x.controls.states
{
    public class FocusState extends Enum
    {
        {enum(FocusState)}

        public static var NO_FOCUS  :FocusState;

        public static var PRESS_FOCUS   :FocusState;

        public static var KEY_FOCUS :FocusState;
    }

}

Usage:

var userState       :UserState      =   UserState.PRESS_FOCUS_DOWN;
var pointerState    :PointerState       =   userState.toPointerState();
var focusState      :FocusState     =   userState.toFocusState();

trace(pointerState); // DOWN
trace(focusState); // PRESS_FOCUS
ylazy commented 1 day ago

And an enum.index in my enum class is a Number value (not Integer) because I need to check whether an enum use a custom index. For example:

package
{
    public class Days extends Enum
    {
        {enum(Days)}

        public static const MONDAY  :Days = new Days(2);
        public static const TUESDAY :Days = new Days();
        public static const WEDNESDAY   :Days = new Days();
        public static const THURSDAY    :Days = new Days();
        public static const FRIDAY  :Days = new Days();
        public static const SATURDAY    :Days = new Days();
        public static const SUNDAY  :Days = new Days();

        public function Days(index:Number = NaN):void
        {
            super(index);
        }
    }
}

The indexes of the days will be auto-incremented. So it must be a Number to have a default value (NaN).

// 2 3 4 5 6 7 8
trace(Days.MONDAY.index, Days.TUESDAY.index, Days.WEDNESDAY.index, Days.THURSDAY.index, Days.FRIDAY.index, Days.SATURDAY.index, Days.SUNDAY.index);
ylazy commented 1 day ago

And we can add utility functions:

package
{
    public class Days extends Enum
    {
        {enum(Days)}

        public static const MONDAY      :Days = new Days(2);
        public static const TUESDAY     :Days = new Days();
        public static const WEDNESDAY       :Days = new Days();
        public static const THURSDAY        :Days = new Days();
        public static const FRIDAY      :Days = new Days();
        public static const SATURDAY        :Days = new Days();
        public static const SUNDAY      :Days = new Days();

        public function Days(index:Number = NaN):void
        {
            super(index);
        }

        public static function fromName(name:String):Days
        {
            return Enum.fromName(name, Days);
        }

        public static function fromIndex(index:Number):Days
        {
            return Enum.fromIndex(index, Days);
        }

        // return ES6 Set that contains all enums
        public static function get values():Set
        {
            return Enum.getValues(Days);
        }
    }
}

Usage:

trace(Days.fromIndex(4)); // WEDNESDAY
trace(Days.fromName("FRIDAY").index); // 6
trace(Days.values); // [Set MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
ylazy commented 1 day ago

As you can see in above examples, my Enum class have all capabilities like Java Enums.

Enums are used to create our own data type like classes. The enum data type (also known as Enumerated Data Type) is used to define an enum in Java. Unlike C/C++, enum in Java is more powerful.

Enum improves type safety
Enum can be easily used in switch
Enum can be traversed
Enum can have fields, constructors and methods
Enum may implement many interfaces but cannot extend any class because it internally extends Enum class
ajwfrost commented 21 hours ago

Looks good .. are you using describeType to do some of the initialisation? And just wondering if you can use those Enum members in a switch statement?

Looking at the Java documentation, it does seem like they are using the compiler to create the actual (internal) class definition that it then uses for type safety / code completion etc. The shorthand mechanism for defining an enum is quite nice, for the simple case it just makes it trivial.. but although it's a bit more effort in setting up, the functionality you've created here seems very useful!

thanks

ylazy commented 28 minutes ago

are you using describeType to do some of the initialisation? And just wondering if you can use those Enum members in a switch statement?

Sure, I must use describeType to get the list of variables/constants. It bases on this Enum class with improvements. I'm not sure if I understand clearly about what "can be used in switch" mean. If it's just something like bellow:

var focusState:FocusState = getFocusState();

switch (focusState)
{
    case FocusState.PRESS_FOCUS:
        doSth1();
    break;

    case FocusState.KEY_FOCUS:
        doSth2();
    break;

    case FocusState.NO_FOCUS:
        doSth3();
    break;

    default: break;
}

then it surely can.

But it can't:

var focusState:FocusState = getFocusState();

switch (focusState)
{
    case PRESS_FOCUS:
        doSth1();
    break;

    case KEY_FOCUS:
        doSth2();
    break;

    case NO_FOCUS:
        doSth3();
    break;

    default: break;
}

because the import static is not available. And we can't inline enums in our code. We can't also use the shorthand mechanism as you mentioned. But I think it's not a problem (at least for me). Each enum should be placed inside a class for better management, reusing, or expanding... With the help of snippets (like VSCode Snippets), defining an enum is very quick. If some enums are internal for a class, I will place them at the bottom of the class as internal classes. I also like to write the data type of each enum:

public static var NO_FOCUS:FocusState,  PRESS_FOCUS:FocusState, KEY_FOCUS:FocusState;

It reminds me that each variable/constant is a FocusState.

But yes, if the compiler can help, we can make it more perfect and suitable for everyone.

I'm refactoring some code & will release my lib asap.

But .. I'm not so sure this is a good idea. If you look at what "works", i.e. that use of test2(SubClass), it doesn't actually pick up the class when it's initialised. So you run the risk of odd behaviour.

So what about a new method for the Error class, like bellow:


/*
[
    [class SuperClass, "get test", "Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SuperClass.as", 20],
    [class SubClass, "cinit", "Y:\Development\ActionScript\SDK\Ylazy\Core\Libs\as3\src\ylazy\projects\core\x\topLevel\dataTypes\SubClass.as", 0],
    ["global$", "init"],
    [class Test, "Y:\Development\ActionScript\Projects\VSCode\src\Test.as", 0],
    ["runtime::ContentPlayer", "loadInitialContent"],
    ["runtime::ContentPlayer", "playRawContent"],
    ["runtime::ContentPlayer", "playContent"],
    ["runtime::ContentPlayer", "run"],
    ["ADLAppEntry", "run"]
]
*/
var stacks:Array = new Error().getStacks();
var subClassReference:Class = stacks[1][0];