Ticket Created
over 2 years ago

CIQQA-1133

Inconsistent type checker errors

With strict type checking:

import Toybox.Application;
enum Foo {
    Zero,
    One,
    Two,
    Three,
}
function foo(x as Foo) as Void {}
function bar() as Void {
    foo(0);           // Fails, as expected
    foo(Zero);        // Works, as expected
    foo(Zero as Foo); // Works, as expected
    foo(0 as Foo);    // Works, as expected
    Storage.setValue("foo", Zero);          // Works
    Storage.setValue("foo", 0);             // Works
    Storage.setValue("bar", Zero as Foo);   // Fails (why?)
    Storage.setValue("baz", 0 as Foo);      // Fails (why?)
}

The error from Storage.setValue is "Invalid '$.Foo' passed as parameter 2 of type 'PolyType<Null or $.Toybox.Application.PropertyKeyType or $.Toybox.Lang.Array<$.Toybox.Application.PropertyValueType> or $.Toybox.Lang.Dictionary<$.Toybox.Application.PropertyKeyType,$.Toybox.Application.PropertyValueType> or $.Toybox.WatchUi.BitmapResource>'."

The first four lines of bar show that "Foo" is its own type, and that both Numbers and Foos can be passed when cast to Foo. Storage.setValue is happy to accept a Zero, so it seems like it accepts type Foo. But it *won't* accept 'Zero as Foo', or '0 as Foo'. Whats going on? It should either reject Zero (and require "Zero as Number" instead), or accept Zero, "Zero as Foo" and "0 as Foo".

  • One more thing, slightly off the topic of my original post. As you correctly point out, enums can have string values. But the documetation says:

    > Explicitly assigning a value other than an integer to an enumeration will cause an error

  • One more comment

    the type of Zero is actually Number

    No, it isn't. If it was, the call foo(Zero) would fail in the same way that foo(0) does (taking the foo from my example). It's clearly its own type. And the only sensible type for it to be is the same type as (0 as Foo). But thats not what the type checker does.

    You're arguing that the type checker differentiates numeric Foos from non-numeric Foos based on their actual values, which is ok (if a little weird); but (0 as Foo) is just as good a "numeric Foo" as Zero is.

  • Ok. But my enum *doesn't* have any strings in it.

    But the real point is this: suppose I want to mechanically replace every occurrence of Zero in my program with its actual value, so `0` in this case, but I want to keep things type-correct so the type checker will still report issues (in particular, it will report issues that I've just introduced).

    If I replace Zero with 0, then calls to foo fail. If I replace it with (0 as Foo), then calls to Storage.setValue fail. Of course, I could also analyze the context, and figure out which one is needed - but that means writing my own type checker, and mimicking all the weirdness in yours (and yes, this really is weirdness). And don't forget it could have been:

    var x = Zero;
    ...
    foo(x);
    Storage.setValue("foo", x);

    Now what do I replace x's initializer with? `0` will result in an error from foo, and `0 as Foo` will result in an error from Storage.setValue.

    But your whole point seems forced. If the compiler "knows that Zero is really a number", why does it not "know that 0 as Foo is really a number"? Also, why is the typecheker varying its behavior based on the value of an object, rather than its type?

  • Your test case isn't comparing apples-to-apples. Your function foo takes a Foo. Storage.setValue() accepts a second parameter that can be of type Number. I think you should expect different behaviors when passing different types to functions that take different types.

    That said, the issue that you seem to be bumping into is that enum in MonkeyC is different than you are expecting; an enum can have a non-integer value. In your example, the type of Zero is actually Number. The compiler knows the type of each of the enumerators and it is able to see that Zero is a Number and allows you to pass it in a place where a type check is expecting a Number. But if you change the value of Zero to "ABC", the type check will not pass.

    import Toybox.Application;
    
    enum Foo {
        Zero = "ABC",
        One = 1,
    }
    
    // type of Foo is something like Type_List< Foo<String>, Foo<Number> >
    // type of Zero is Foo<String>
    // type of One is Foo<Number>
    
    // accepts
    //   Type_List< Foo<String>, Foo<Number> >
    // accepts any types within that type list
    //   Foo<String>
    //   Foo<Number>
    // accepts any types convertible from those types
    //   String
    //   Number
    function foo(x as Foo) as Void {}
    
    // accepts only String or Number
    function bar(x as String or Number) as Void {}
    
    function baz() as Void {
        foo(Zero);  // legal Zero is Foo<String>
        foo(One);   // legal One is Foo<Number>
    
        // legal; you are telling the compiler the parameter is Type_List< Foo<String>, Foo<Number> >
        // which may be a problem at runtime if you are a liar
        foo(Zero as Foo);
        foo(1 as Foo);
    
        foo("ABC"); // not legal
        foo(1);     // not legal
    
        bar(Zero);  // not legal
        bar(One);   // not legal
    
        bar("ABC"); // legal; parameter is String
        bar(1);     // legal; parameter is string
    }