Ticket Created
over 3 years ago

Type checker doesn't understand nested classes

Given this code:

import Toybox.Test;
import Toybox.Lang;

module nesting {
    module MB {
        const MBK = 1;
        class Base {
            const BaseK = 2;
        }
    }

    module MC {
        const MCK = 3;
        class X extends MB.Base {
            function initialize() {
                Base.initialize();
            }
            const XK = 4;
            function getXK() as Number {
                return XK;
            }
            function getMCK() as Number {
                return MCK;
            }
            function getBaseK() as Number {
                return BaseK;
            }
            function getMBK() as Number {
                return MBK;
            }
            class Y {
                function getXK() as Number {
                    return XK;
                }
                function getMCK() as Number {
                    return MCK;
                }
                function getBaseK() as Number {
                    return BaseK;
                }
                function getMBK() as Number {
                    return MBK;
                }
            }
        }
    }
}

// all these getXK functions work, and return the
// expected values.
(:test)
function testClassLookup(logger as Logger) as Boolean {
    var x = new nesting.MC.X();
    logger.debug(x.getXK());
    logger.debug(x.getMCK());
    logger.debug(x.getBaseK());
    logger.debug(x.getMBK());
    return true;
}

// all the testNestedLookup* function crash inside the 
// corresponding Y.get* method
(:test)
function testNestedLookupXK(logger as Logger) as Boolean {
    var x = new nesting.MC.X.Y();
    logger.debug(x.getXK());
    return true;
}
(:test)
function testNestedLookupMCK(logger as Logger) as Boolean {
    var x = new nesting.MC.X.Y();
    logger.debug(x.getMCK());
    return true;
}
(:test)
function testNestedLookupBaseK(logger as Logger) as Boolean {
    var x = new nesting.MC.X.Y();
    logger.debug(x.getBaseK());
    return true;
}
(:test)
function testNestedLookupMBK(logger as Logger) as Boolean {
    var x = new nesting.MC.X.Y();
    logger.debug(x.getMBK());
    return true;
}

I get this output:

Executing test testClassLookup...
DEBUG (13:19): 4
DEBUG (13:19): 3
DEBUG (13:19): 2
DEBUG (13:19): 1
PASS
------------------------------------------------------------------------------
Executing test testNestedLookupXK...

Error: Symbol Not Found Error
Details: Could not find symbol 'XK'
Stack: 
  - getXK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:362 0x10001d2f 
  - testNestedLookupXK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:390 0x10000a50 
  - evaluate_test_entries_0_to_11() at UnitTests.mc:72 0x100007d2 
  - runTest() at UnitTests.mc:93 0x100009d8
ERROR
------------------------------------------------------------------------------
Executing test testNestedLookupMCK...

Error: Symbol Not Found Error
Details: Could not find symbol 'MCK'
Stack: 
  - getMCK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:365 0x10001d45 
  - testNestedLookupMCK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:396 0x10000e3c 
  - evaluate_test_entries_0_to_11() at UnitTests.mc:75 0x100007f3 
  - runTest() at UnitTests.mc:93 0x100009d8
ERROR
------------------------------------------------------------------------------
Executing test testNestedLookupBaseK...

Error: Symbol Not Found Error
Details: Could not find symbol 'BaseK'
Stack: 
  - getBaseK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:368 0x10001d3a 
  - testNestedLookupBaseK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:402 0x10000faa 
  - evaluate_test_entries_0_to_11() at UnitTests.mc:78 0x10000814 
  - runTest() at UnitTests.mc:93 0x100009d8
ERROR
------------------------------------------------------------------------------
Executing test testNestedLookupMBK...

Error: Symbol Not Found Error
Details: Could not find symbol 'MBK'
Stack: 
  - getMBK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:371 0x10001d50 
  - testNestedLookupMBK() at /Users/mwilliams/www/git/monkeyc-optimizer/test/OptimizerTests/source/OptimizerTestsTest.mc:408 0x100017ec 
  - evaluate_test_entries_0_to_11() at UnitTests.mc:81 0x10000835 
  - runTest() at UnitTests.mc:93 0x100009d8
ERROR

I had to put one failing test per test function because there's no way to recover when symbol lookup fails, but the test runner runs each test function as a separate invocation of the program.

But basically, the X.get* functions all work (as expected, and as predicted by the type checker), but the corresponding Y.get* functions (for the inner class) all fail, even though the type checker thinks everything is ok.

It appears that a nested class behaves as if it were defined in the global scope, so it doesn't get access to the context of the containing class. There's a hint at this on the monkeyc reference page: "Nested classes in Monkey C do not have access to the members of the enclosing class", but its not entirely clear what that means. I had initially assumed it was talking about not having privileged access to private/protected members. But it seems to mean no access at all, even to the enclosing modules (except for the global module, of course).

I've confirmed that this behavior is the same in both the latest full release, and the 4.1.4 compiler 2 beta.

Edit: Its actually much worse. At -O2, because the new compiler thinks it knows the values for these consts, it replaces the them with their values so that the tests stop crashing. This is not a good thing...

  • Here's a more refined example, that shows a few of the mistakes the optimizer can make when substituting constants (because it thinks it knows the value of the constant, but it looked it up incorrectly). Its similar to the original, but now there are three sets of constants. One set (NESTEDA1-NESTEDA4) defined as before in the class, its super class, and their corresponding modules. Then a similar set NESTEDB1-NESTEDB4, which are also mirrored in the global module, but with different values. Finally NESTEDC1-NESTEDC4 are similar to the Bs, but are also mirrored in the nested module.

    Now, when run with optimization turned off, the tests all pass, except for the Y's NESTEDA* and NESTEDC*, which all crash because there are no such symbols in scope.

    When run with optimization turned on, Y's NESTEDA1-A4 evaluate to the corresponding values: 1-4. This is wrong, because those variables are not in scope. Y's NESTEDC1-C4 are weirder; C1==1, C2==2, C4==4, but C3==2003, meaning it picked up the value from the const in the nested module (which is also wrong, all of these constants were out of scope, as demonstrated by the unoptimized run).

    Also, with optimization turned on, X's NESTEDB3 incorrectly evaluates to 1003, rather than 3, NESTEDC1 evaluates to 1, rather than 2001, and C3 to 2003 rather than 3 (ie the optimizer is looking up the constants incorrectly, finding the wrong ones, and then substituting their values so we get the wrong values at runtime).

    Sorry, its a lot of code, and quite confusing. But hopefully its clear that if we get different numbers with optimization turned on, then something is badly wrong. Similarly, failing to crash, is also wrong, but at least somewhat less of an issue... you could certainly argue (as the C and C++ committees did) that if the code as written is going to crash, it doesn't really matter what the code generator produces. But I think most people agree that that approach was a mistake in practice...

    Ouch. When I tried to insert the code, I got a popup telling me I've been blocked and cant access garmin forums. So here's a link to it instead