Under Review
over 1 year ago

Static vs non-static vs self vs the type checker

I had assumed that in a static method, "self" would be the corresponding class object, rather than an instance of the class (just like, in a module, self is the module object).

But it turns out that inside a static method, self can also be an instance of the class. And in an instance method, self can be the class object, or even an instance of a completely different class.

so given

class X {
  const K = 0;
  function staticFun() as Number {
    return K;
  }
  function instanceFun() as Number {
    return K;
  }
}

If I call staticFun as "X.staticFun()" it will crash (can't find K - but note that the type checker doesn't report this).

On the other hand if I call it as (new X()).staticFun(), it works! Similarly if I add an instance method to X (or an extension of X), and call it from that as "staticFun()" or "self.staticFun()" it also works. But if I call it from an instance method as "X.staticFun()", it crashes. So:

class Y extends X {
  function initialize() {
    var k1 = staticFun(); // works
    var k2 = self.staticFun(); // works
    var k3 = X.staticFun(); // crashes
  }
}

I get almost the same behavior from instanceFun. If I call it as X.instanceFun() from outside any method it will crash trying to find K. In fact the only difference in behavior between an instance method and a static method seems to be that when called as X.instanceFun() from another instance method, instanceFun is invoked "non-statically":

class Z extends X {
  function initialize() {
    var k1 = instanceFun(); // works
    var k2 = self.instanceFun(); // works
    var k3 = X.instanceFun(); // works(!)
  }
}

But this leads to an even stranger possibility. If you call X.instanceFun() from an instance method in an unrelated class, it will invoke instanceFun with self as the instance of the other class. So:

class W {
  const K = 42;
  function initialize() {
    var x = X.instanceFun();
    System.println("X.instanceFun returned " + x);
  }
}

Since instanceFun is invoked on an instance of W, the println outputs "X.instanceFun returned 42". And note that if W doesn't define K, then X.instanceFun crashes.

To summarize the issues here:

I would expect static methods to always behave statically; but instead they can be invoked just like instance methods. Surprisingly, the type checker (and the compiler2 optimizer) both assume they behave as if they were invoked as instance methods - so its easy to write code that will crash at runtime but that passes the type checker; worse, whether it crashes or not can depend on optimization level. I'm not sure what can be done about this. Ideally staticness would be enforced, and the type checker would understand the difference between static and non-static methods. Enforcing staticness at runtime could break existing code, so probably isn't an option. But the type checker could be tightened to match the static behavior without breaking existing code. Code that currently type checks could start failing; but thats actually a good thing - it would point out an existing problem. Worst case the fix would be to remove the static designation, since if the code actually works at runtime, the method isn't being invoked statically.

I definitely expect instance methods to be invoked on instances of the same class - or at least, a related class. ie it should be ok to call X.instanceFun() from a method of Y or Z (since they extend X), for example. But it should not be ok to do so from W. That should either be an error, or it should be invoked statically. And again, ideally instance methods should be guaranteed to have an instance object to work with.

The current situation makes it impossible for the type checker to meaningfully type check methods - since it can't even know the type of self!

  • I'll just note that as of 4.1.7 the type checker does enforce the staticness of static methods (great!). ie it reports that the "K" in staticFun is an undefined variable. As I said above, I think this is the right thing to do, even though you *can* invoke staticFun in such a way that it works (but in that case, you're using it a as a non-static function, so it shouldn't be marked static in the first place).

    It still doesn't give any indication that invoking a non-static method without an object (or with the wrong kind of object) is going to cause problems. It would be great if the type checker could give errors for these issues too...