Acknowledged
CIQQA-3490

Compilator wrongly assuming 'Attempting to perform container access on null.'

Hey,

I'm currently developing a Connect IQ app and I think I found a bug in the MonkeyC compilator. With the following code, the second line in the onStart method would be underlined with the error  'Attempting to perform container access on null.'. Trying to compile would result in the same issue. This is performed from the default widget template app with VSCode, SDK version 8.2.3 and trying to compile for device forerunner 955.

import Toybox.Application;
import Toybox.Lang;
import Toybox.WatchUi;
import Toybox.System;

class testApp extends Application.AppBase {
    public static const RESOLUTION_MAP = {
        1   => [:_4K, :_16R9],
        4   => [:_2K7, :_16R9],
        6   => [:_2K7, :_4R3],
        7   => [:_1440, :_16R9],
        9   => [:_1080, :_16R9],
    };

    function initialize() {
        AppBase.initialize();
    }

    // onStart() is called on application start up
    function onStart(state as Dictionary?) as Void {
        var test = 1;
        var result = RESOLUTION_MAP.get(test)[0];
        System.println(result);
    }

    // onStop() is called when your application is exiting
    function onStop(state as Dictionary?) as Void {
    }

    // Return the initial view of your application here
    function getInitialView() as [Views] or [Views, InputDelegates] {
        return [ new testView() ];
    }

}

function getApp() as testApp {
    return Application.getApp() as testApp;
}
I found a workaround by type checking the container access as followed so that I can compile my code, but I still get a compilation warning for the line within the if statement 'Statement is not reachable'. I checked in a test project, and the statement is actually reachable if the key provided exists in the const Dictionary RESOLUTION_MAP.
var resolution = RESOLUTION_MAP.get(setting);
if (resolution instanceof Symbol) {
    System.println(RESOLUTION_LABELS.get(resolution[0]));
}

Don't hesitate to reach out to me if you need any other information to reproduce the issue !
  • not my downvote (i've just upvoted it to cancel it out Slight smile). I agree the compiler does some really weird type inference.

    I've seen the type-checker gives better results when everything of interest is within a function, or class-private -- that's probably because it's only analyzing per-unit (ie that .mc file) - that's a common limitation of many type checkers. 

  • I think the issue you are running into is the compiler cannot infer all possible types of RESOLUTION_MAP.

    Like I said, in that case, it should not be incorrectly inferring a too-specific type for elements in RESOLUTION_MAP, which it does in *some* cases. Better to be too vague than too specific, when it comes to type inferences. The former makes code harder to write, but the latter gives the dev a false sense of security (that it's impossible for their code to crash due to a certain kind of runtime type error).

    The compiler should always infer Object or Null for the element type of a container, *unless* the element is accessed immediately after being assigned to a known type. This is because there's no way for the compiler to know for sure that a given element of RESOLUTION_MAP hasn't changed, unless it was assigned right before the current access of the element.

    Bug 1) I already gave an example where the compiler will infer SOME_TYPE or Null for an element even though the element value has been changed to a different type.

    Bug 2) On the flip side, if you access an element immediately after it's been assigned to value with a certain type (call it NEW_TYPE), the compiler infers NEW_TYPE or Null for that element. But why include Null as a possibility. In this case the compiler should infer NEW_TYPE alone.

    So I think there's real bugs here, and I don't think it's fair for others to tell OP they shouldn't have posted here and to downvote the bug report.

  • I think the issue you are running into is the compiler cannot infer all possible types of RESOLUTION_MAP.
    This seems to be a compiler limitation as it does not know the scope of usage.

    - Vars Inside of a function, the compiler can guarantee scope, and thus is able to infer types (usually correctly)

    *public* infers the Dictionary is accessible outside of the Unit, and therefore, it doesn't *know" if it can support other types.  Declaring as 'private' might help. (bizarrely const declaration does not help)

    - In strongly-typed languages compilers can fully resolve the types for this.  But Monkey C is "Monkey-typed" -- it has no limits on re-typing.

    I find if you want the compiler to *know* the types outside of a function, you often have to *tell* it explicitly to only accept a given type (basically a compiler hint): 

    as Dictionary<Lang.Number, Lang.Array<Lang.Symbol>>;

    e.g.

    public static const RESOLUTION_MAP = {

            1   => [:_4K, :_16R9],
            4   => [:_2K7, :_16R9],
            6   => [:_2K7, :_4R3],
            7   => [:_1440, :_16R9],
            9   => [:_1080, :_16R9],
    } as Dictionary<Lang.Number, Lang.Array<Lang.Symbol>>;
  • maybe it suggests that the compiler should *never* be inferring the original value type for a get of any key for a dictionary?

    Except in the case that the get() happens "immediately" after the put() (i.e. in such a way that the value type could not have changed in between. (i.e. no intervening function calls)

    Anyway, I appreciate that the compiler is trying to be helpful, but I think it goes too far in some cases in inferring a specific value type (or Null) when Object or Null would be more correct. I do understand there's probably a balance between strict correctness and trying to make the type system as useful as possible.

  • I noticed that the compiler tries to be smart here.
    e.g. If I change/add a key with a different value type in a function, then get the same key immediately afterwards in the same function, the compiler will infer the new value type or null.
    While this is nice, since it's actually trying to be smart, why not infer the new value type and not null?
    But of course, if I move the access of key 4 to a different function, the compiler infers the *original* value type.
    While it's obvious why this is happening, maybe it suggests that the compiler should *never* be inferring the original value type for a get of any key for a dictionary?
    It looks like the compiler is:
    - trying to be too smart in some cases: inferring VALUE_TYPE or Null for an arbitrary get() when it should be Object or Null
    - not being smart enough in some cases: inferring VALUE_TYPE or Null for a get() that follows a known put() when it should be VALUE_TYPE. (At least be consistent and infer Object or Null.)
    [4/4]