A JScript .NET Design Donnybrook

A JScript .NET Design Donnybrook

  • Comments 22

When you're designing a new programming language, the "main line" cases are easy.  It's the "corner" cases that bedevil language designers -- the little fiddly bits where two language features that make perfect sense on their own interact in some weird way.  Unfortunately, I suspect that any language sufficiently feature-rich to be interesting will have weird corner cases.  Getting them right is one of the more interesting and difficult aspects of language design.

Want to give it a try yourself?  Now's your chance.  Here is a question that sparked a protracted and hilarious debate amongst the JScript .NET design team back in 2000.  I'd be interested to see what your opinions are.  So as to not spoil the fun, figure out what you think this code should do before you run it or decompile it into IL.

class foo {
  function get abc() {
    return 5;
  }
}
class foo2 extends foo {
  function bar(ob) {
    print(ob.abc);
  }
}
class foo3 extends foo2 { 
   hide function get abc()
{
    return 50;
  }
}
var f = new foo3();

Clearly print(f.abc); must print out 50.  But should f.bar(f); print out 50 or 5?  

Justify your answer.

HINT: While you're thinking it over, keep in mind that nowhere in this code are there any type annotations.  The question was originally raised in the context of speculative compiler optimizations which we might perform in the absence of type annotations.  Are there codegen optimizations which we could perform that make your desired behaviour more efficient in common cases?

  • I would guess that it would print '50'. The rationale I would use to defend this position is that f.bar(f) calls the foo2::bar function while passing in f (which is a foo3 type). Calling abc on a foo3 object should print 50. This assumes that passing an object by reference in JScript.NET and then calling a method on it will automatically convert the object to an instance of its most heavily derived type (which I am guessing it does not do).

    Using override in place of hide under the hood likely changes the function pointer for the base abc() to point to the overridden function abc() in foo3.

    Am I on the right track here?
  • That's a reasonable argument, but I'd like to know two things:

    First off, since the argument isn't annotated, it is effectively declared as being of type object -- it's least derived type, not its most derived type. Does that matter?

    Second, what is the meaning of "hides" in this program? If you removed it, would you expect the behaviour to change?
  • I will say 50.

    In the absense of anything to the contrary, I would assume the dynamic late-bound answer rather than the static early-bound one due to the behavior of prototypes in ECMAScript rev. 3.

    If this were C++, I would assume the opposite case.
  • Eric wrote:

    > Second, what is the meaning of "hides" in
    > this program? If you removed it, would you
    > expect the behaviour to change?

    I would expect removal of "hide" to result in a compilation error - to catch the very frequent case of accidental overloading of a base class's method.

  • When I said "absense", I of course meant "absence".

    Gads, am I totally dependent on that automatic spell checking crutch or what!
  • Ah, but remember that I've blogged before that one of the design principles of JScript is "muddle on through". JScript .NET is not like C#, where every design decision you make must be called out with a keyword that shows that you thought it through.
  • > I would assume the dynamic late-bound answer rather than the static early-bound one due to the behavior of prototypes in ECMAScript rev. 3.

    In general, assuming dynamic over static is a good idea when you're talking about JScript.

    However, remember that we added class inheritance to strengthen the type system so that people who wanted a more predictable, static, type-checked experience could have one.

    Thus, it's a bit of a toss-up. On general grounds, you'd be wise to assume dynamic over static, but on design grounds you'd be better off deducing that classes do whatever prototypes _don't_ do.

    Are we having fun yet? :-)
  • >it is effectively declared as being of type object -- it's least derived type, not its most derived type. Does that matter?

    Yes, this does matter and is key to the behavior. If I understood how JScript.NET unpacked the "object" into its truer type, that would likely be the answer to the question.

    >what is the meaning of "hides" in this program?

    Hides will merely override the base's abc() unless the foo3 object was recast as a foo2 or a foo. I believe the absence of hide will be by default the same as override - it would print 50.
  • Okay, before I test it, I'll say this: By looking at it, I'd expect 50. I'd want 50, as well. I'm passing foo2.bar a "foo3", and asking it to call "abc" on whatever I pass it. In foo3's case, that would return 50, right? Now, if it called "this.abc" instead, I'd expect 5.

    Now, it is a bit muddled with the whole "least derived type" thing. Knowing that, I'd say it will probably print 5, but I still hope for 50.

    The issue is that with JS.net, it should just do what I want, right? I guess The Issue is with hides... it seems out of place in JScript. On one hand, JS is fairly loose with the types. On the other, (if this prints 5), I would have to explicitly declare an object as "foo3" to __ever__ call it's abc... right? Wait, if I call foo3::abc on the first line, I have to call it on all the others as well? ... but... but...

    Ouch, my brain exploded.

    ---------------

    Okay I generated it, and it printed out the answer. *warning - spoiler!* A 5. The key is in the code to foo2::bar-- it does something weird. very....

    It first checks too see if the passed in object is a "foo".. [isinst foo]. If it is, it "callvirt foo::gat_abc". However, if it is NOT a foo, it does a [isinst foo3], then, if it is a foo3, [callvirt foo3::get_abc]. Lastly, it does the late-bind call in case it's some random object that has "abc" prop or method.

    Now, why would it do that? Wouldn't an object that is a foo3 always be a foo in this case? Reversing the order would make it work correctly. Keeping it the same seems to be generating dead code.

    It seems as though it will generate that same block [isinst xxx .... callvirt xxx::get_abc()] for *any* xxx that implements get abc(){}. I assume for performance reasons.

    Now about the strange order.... can you tell me why it would do the base classes before the derived ones? It seems like it's generating dead code for no particular reason other than completeness.
  • An addendum to the above post: (I'm not good at explaining myself the first time around in print. That's why I normally post something to my blog without publishing, then re-read it the next day and edit it).

    Here's what I was trying to say when my brain exploded.

    On the first call to foo2::bar ->
    print( f.abc);
    Wouldn't this be passing an "object" to print under the same rules? So this should call the "least derived types abc" just the same. After all, you don't annotate f as a "foo3" object?

    If it doesn't, then calling f.bar(f); should follow the same rules as print(f.abc), shouldn't it?

    It seems to me that either
    a) both should print 50
    b) both should print 5.

    However, the correct answer is none of the above, and I'm confused as to why (as well as being confused as to why the "extra" IL is being generated in foo2::bar). I'm confused a lot. (I bet you're more glad that I'm not on the js.net team than I am!)
  • > Wouldn't this be passing an "object" to print under the same rules? So this should call the "least derived types abc" just the same. After all, you don't annotate f as a "foo3" object?

    It's not passing an object to "print", but I take your point. "f" isn't annotated, so why should it behave any differently than "ob", which is also not annotated?

    The plot thickens!
  • Philip, try comparing the IL when abc is declared hide with the IL when abc is declared override (and compare the execution results). That may lead you to some insight.
  • Eric said:

    > Ah, but remember that I've blogged before
    > that one of the design principles of JScript
    > is "muddle on through".

    True.

    In that case (and after reading the docs on "hide"), I would say 50.

    Why? Because, again due to the philosophy of the previous version of JScript (and script languages in general), I would expect a late-bindy kind of behavior. In the absence of a type specifier (e.g., for an Object), I would *always* perform/expect late-binding of method calls.

    Yes, this results in *much* slower code for the typical case, but it would be in line with my expectations.

    > However, remember that we added class
    > inheritance to strengthen the type system
    > so that people who wanted a more predictable,
    > static, type-checked experience could have one.

    For those cases where I *want* stricter checking (and allow for early binding optimizations), I would tend to add the appropriate annotation. Actually, I would have preferred (would still like) a compiler switch to require strict specification of type. In fact, before Jscript was released I would have argued for it as an effect of –fast. Today, it would be nice as its own switch.

    IMHO, an answer of 5 would make a compiler writer happy (it results in faster early-bound code) and an answer of "50" would make a user happy (it is what I, personally, would expect).

    Yes… I am surprised that the answer is in fact 5 :P

    Anyway, when it comes to the new Jscript.net features, I will now start out suspecting that the compiler will try to do the “fast” thing instead of the “scripty” thing; in this case, use the type in effect at the time of definition and do early binding. Without giving it much thought, I would not be surprised to learn that this follows the model of other CLI languages (none of which I know very well).
  • > For those cases where I *want* stricter checking (and allow for early binding optimizations), I would tend to add the appropriate annotation

    Quite amusing. I said almost exactly the same thing back in 2000. I'll blog more about this tomorrow.




  • I’m not much into JScript, and would expect 5. Because in the languages I have seen “hide” (by any other name, such as “reintroduce” in Delphi, “new” in C#) it breaks the override chain. The foo3::`get abc`() no longer overrides foo::`get abc`, and as such, will not be considered candidate for late binding.

    Of course, the “scripty” thing to do, in absence of knowledge about the type of f, would be to invoke abc by name, and that would probably resolve to foo3::`get abc`. In fact, one might try this:

    class baz { // totally unrelated to foo
    function get abc() {
    return 42;
    }
    }
    f = new foo3();
    b = new baz();
    f.bar(b);

    and expect 42.
Page 1 of 2 (22 items) 12