Ticket Created
over 3 years ago

CIQQA-1361

Import/Using confusion in the type checker and runtime

Given the following code:

import Toybox.Lang;
import Toybox.Application;

(:typecheck(false))
function noSystem() as Void {
    // Fails at runtime: "Could not find symbol 'System'"
    System.println(0);
}

function noNumber(x as Number or String) as Boolean {
    // Fails at runtime: "Could not find symbol 'Number'"
    return x instanceof Number;
}

function properties() as Void {
    Storage.getValue("What");
}

class TestClass {
    function noSystem() as Void {
        // works!
        System.println(0);
    }
    function noNumber(x as Number or String) as Boolean {
        // works!
        return x instanceof Number;
    }
    static function noNumberStatic(x as Number or String) as Boolean {
        // Fails at runtime: "Could not find symbol 'Number'"
        return x instanceof Number;
    }
    function properties() as Void {
        Storage.getValue("What");
    }
}

function lookupTests() as Void {
    noSystem();
    $.Toybox.System.println(noNumber(1));

    var x = new TestClass();
    x.noSystem();
    $.Toybox.System.println(x.noNumber(1));
    $.Toybox.System.println(TestClass.noNumberStatic(1));
}

There is no import/using of System (in fact, I made sure there is none in the entire project).

Without the (:typecheck(false)) the type checker warns that it can't find System in $.noSystem. Thats good, because it's really not there, and it fails at runtime. So far, so good. I should have trusted the type checker.

In $.noNumber we're trying to use "instanceof Number", rather than the correct "instanceof Lang.Number". The type checker is happy, but it fails at runtime, because Number isn't found (as expected). So the type checker is misleading us.

But inside the class things get interesting.

Both TestClass.noSystem and TestClass.noNumber *work* at runtime (as predicted by the type checker). Why? what makes Number available as a class, and System available as a module, just because we're inside a class method?

And just to confirm the confusion, in noNumberStatic, the type checker is once more satisfied that Number names a class, but it once again fails at runtime.

Finally, the type checker is happy with both versions of "properties". It thinks that 'import Toybox.Application' makes the name Storage available, both inside and outside the class. But unlike "instanceof Number", this one fails both in and out.

So I'm trying to figure out the rules...

  • some (all?, many?) Toybox modules are available inside non-static class methods, even if they haven't been imported. Which ones? The type checker actually seems to know about this rule.
  • importing a toybox module, makes some (all?) of its classes available in non-static methods, but only their module-qualified names available outside of classes. The type checker gets this right inside non-static methods, but wrong everywhere else (probably more accurate: the type checker incorrectly thinks they're available everywhere, so it happens to be right inside non-static class methods).
  • importing a toybox module makes the type checker think that the contained modules are also imported, but they aren't.

So at the least there are a few more type checker bugs here, where it thinks a name can be found, but it will fail at runtime. In addition documenting the expected behavior would be helpful...

Edit:

There must be something special about Number, String etc, because although this works for them, if you replace "Number" with "Menu2" for example, it doesn't work. Without 'import Toybox.WatchUi', the type checker complains. With the import, the type checker is happy, but "instanceof Menu2" crashes at runtime.

On the other hand, I seem to be able to use pretty much any module from within a non-static member. Eg changing the body of TestClass.noSystem to "System.println(Communications.UNKNOWN_ERROR);" works with no imports of Communications anywhere in the program.

Edit2:

It seems that all of the top-level names in Lang, and all of the top level names in Toybox are injected into the scope of any non-static member of any user defined class. That includes the constants, like ENDIAN_BIG and ENDIAN_LITTLE etc, and (as far as I can tell) this only applies to Lang and Toybox. The type checker thinks it applies to every toybox namespace that you've imported though, and fails to report that the resulting code will crash at runtime.

  • After a lot more testing, I think I may finally understand what's going on.

    When you write 'class Foo extends Bar {function foo() {...} }', lookups of symbols inside foo first do the lookup in the context where Foo is defined, and then lookup not just things in Bar's class, but in the entire context in which Bar was defined.

    Lang.Object is the implicit root class, so when you don't extend anything, you implicitly extend Lang.Object. Lang.Object is (obviously) declared within the Lang module, which is declared within the Toybox module. foo has access to the same names that members of Lang.Object would have access to, so naturally, it can access any toplevel name in Lang, and any top level name in Toybox without any qualifications.

    I've done a few experiments, and everything seems to point to this being the case. If I extend WatchUi.InputDelegate, I can access any of the WatchUi constants without any qualifications, for example. It also seems to work with classes I define myself:

    module X {
        const XCONSTANT = 0;
        module Y {
            const YCONSTANT = 1;
            class Base {
            }
        }
        module Z {
            const ZCONSTANT = 2;
        }
    }
    class TestClass extends X.Y.Base {
      function foo() as Number {
        return XCONSTANT + YCONSTANT + Z.ZCONSTANT;
      }
    }

    foo can access things in pretty much the same way it would if it were defined in Base itself.

    So some of the mystery is solved, but it still leaves a number of bugs in the type checker...