A JScript .NET Design Donnybrook
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?
Comments
- Anonymous
June 07, 2004
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? - Anonymous
June 07, 2004
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? - Anonymous
June 07, 2004
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. - Anonymous
June 07, 2004
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. - Anonymous
June 07, 2004
When I said "absense", I of course meant "absence".
Gads, am I totally dependent on that automatic spell checking crutch or what! - Anonymous
June 07, 2004
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. - Anonymous
June 07, 2004
> 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? :-) - Anonymous
June 07, 2004
The comment has been removed - Anonymous
June 07, 2004
The comment has been removed - Anonymous
June 07, 2004
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!) - Anonymous
June 07, 2004
> 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! - Anonymous
June 07, 2004
The comment has been removed - Anonymous
June 07, 2004
The comment has been removed - Anonymous
June 07, 2004
> 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. - Anonymous
June 07, 2004
The comment has been removed - Anonymous
June 07, 2004
I stayed out of this one since I already knew the answer and mostly why it became that way. But I enjoy the quirkyness. Java also lives in this shadow world between the Static and Dynamic models. You can write a very similar piece of code to this and get the same behavior. - Anonymous
June 07, 2004
Yuck. My mind just isn't capable of understanding scripting languages.
BTW, why on earth does every type have a .cctor? I know script is supposed to be inefficient, but isn't this going over the top? ;-) - Anonymous
June 08, 2004
Jeroen: I'm pretty sure that every type has a .cctor (even if it is empty) because it made the code generator much easier. When the compiler / runtime can both compile any valid program into IL OR dynamically interpret it, this becomes very important.
Philip: The reason it checks for foo2 before foo3 is that we assume (ha ha ha) that the developer of foo2.bar only knew about objects of type foo2 when [s]he wrote the code; they were not expecting someone else to come along, subclass their type, and add some other random method to it.
If they expected this kind of tomfoolery, they would have used a type annotation (or so the thinking went).
Basically performance of minimally-tweaked legacy JScript code was REALLY REALLY important for us in ASP .NET (who would upgrade if their pages were slower?) so trade offs like this were made.
Interestingly, this chain of things-JScript-looks-for on a late bound call can get arbitrarily long, and it's still going to be faster than the latebound call in all reasonable cases. isinst is fast compared to InvokeMember ;-) - Anonymous
June 08, 2004
Also, an empty cctor is not inefficient. The jitter will optimize it away.
I suspect that there really was no good reason to always generate the cctor even when it was empty -- perhaps it just worked out easier to write the codegen that way. - Anonymous
June 08, 2004
>>Also, an empty cctor is not inefficient. The jitter will optimize it away.<<
I didn't know that! Thanks for setting me straight and I apologise for the snide remark. - Anonymous
May 26, 2009
Here's some back-and-forth from an email conversation I had with a user a while back. Why should one