need some clarification on compile-time type vs. run-time type

Brendan Eich brendan at
Sat Nov 10 21:57:36 PST 2007

On Nov 10, 2007, at 9:03 PM, Yuh-Ruey Chen wrote:

> I see. I wonder if this can somehow be extended so that it works on
> non-structural types as well. For example, to initialize a Map, you
> could use {a: 0, b: 1, c: 2} : Map, instead of Map({a:0, b:1, c:2}).
> Perhaps it would map onto the |meta static function invoke| method.  
> The
> downside of this syntax sugar though, is that it hides the cost of the
> creation of the object literal, fooling the user into thinking that  
> the
> Map is directly initialized from the literal.

Yeah, it doesn't seem good judging by both hidden costs and redundant  
features. Map has a constructor function and a meta static function  
invoke -- which does exactly what you wonder about above:

         meta static function invoke(x : Object!) {
             let d = new Map.<EnumerableId,V>;
             for ( let n in x )
                 if (x.hasOwnProperty(n))
                     d.put(n, x[n]);
             return d;

This means writing

     let map: Map.<string, *> = {word1: val1, ... wordN: valN};

does exactly what everyone wants from a "Dictionary" (see the old  
threads on this list under that name). Bonus implementation points  
for optimizing away the object initialiser's Object instance, but not  
a worry to over-specify.

> Can you elaborate on the "earlier and more certain type checking"? I
> thought you said for |is| checks (along with |instanceof| checks)  
> there
> can be no early type errors, so that leaves |is| to be a purely  
> runtime
> check.

There are questions about exactly what strict mode analyses might be  
mandated as normative if an implementation supports strict mode. This  
may be clearer to Graydon and Cormac than to me, so I'd welcome their  
comments. For example:

     function g(a_not_null: T!) ...

     function f(a: T) {
         if (a !== null)
             g(a);           // no cast required?

Now consider changing the condition from (a !== null) to (a is T!).  
Should strict mode allow the above, generating a runtime check for  
null when g is called? Should it require (a cast T!)? Should it  
figure things out with fancier analysis (this example is trivial, but  
it is of the same kind as much harder-to-analyze ones). We have said  
"cast required" before, but the question came up again recently.

> Anyway, we still have a similar confusion to what we had with
> |instanceof|, except this time it's all in |is|:
> // assume |b| is a class and |a| is an instance of it
> a is type b
> a is b
> Since both work, this can fool the user into thinking that the |type|
> operator is optional. Furthermore, the latter can still lead to newbie
> confusion (thinking that the expr is equivalent to |a == b|). So  
> I'm not
> sure there is a net win.

I think we should separate the Python n00b concern that 'is' might be  
taken for ===. That is not a big potential problem in my view; JS is  
not Python, even with things like generators (we don't have  
GeneratorExit, names differ for the iteration protocol hooks, etc.)  
in the mix. You have to be *this* tall; you have to pay attention to  
the basics. False cognates in different parts of Europe can cause  
embarrassment or even legal troubles ;-).

The two-edged nature of the type/value expression split means, as you  
say, someone's naive expectation that there is only one expression  
grammar in the language will be shattered, some of the time. This is  
the problem to fix, or mitigate as best we can.

>> For user-defined functions that instanceof takes as right operands,
>> there's no need to upgrade beyond how things work as in ES3. But the
>> guarantees with classes are gone: if one changes F.prototype after x
>> = new F, (x instanceof F) could change from true to false.
> Well, I was just suggesting adding additional functionality to
> |instanceof|, not changing the original behavior of it, so I'm not  
> sure
> where the compatibility issue is coming into play.

Yes, you're upgrading it, but now it makes stronger guarantees for  
the upgrade-case inputs than for the old ones. That seems problematic.

>> But since (x is I) and (x is J) work fine for x = new C where class C
>> implements I, J {...}, perhaps for interface types we could just make
>> instanceof do what 'is' does. Comments?
> That works for me.

Besides changing the guarantees, it mixes domains: prototype chain  
and (possibly mutable) function.prototype vs. class/interface  
supertype vs. structural supertype.

> How about |obj instanceof type {a: int}|?

Sure, that follows, but it's unnecessary for backward compatibility  
as I noted. It might be good for clarity, until everyone learned it  
and got tired of typing it too much ;-).

> This makes me wonder: what exactly does |type x| in a value expr  
> resolve
> to? If it "resolves to a type", that what does that exactly mean?

The RI alas has this in eval.sml:

and evalTypeExpr (regs:Mach.REGS)
     : Mach.VAL =
     case te of
         Ast.SpecialType st => Mach.Null (* FIXME *)
       | Ast.UnionType ut => Mach.Null (* FIXME *)
       | Ast.ArrayType at => Mach.Null (* FIXME *)
       | Ast.TypeName tn => evalExpr regs (Ast.LexicalRef { ident=tn,  
loc=NONE })
       | Ast.FunctionType ft => Mach.Null (* FIXME *)
       | Ast.ObjectType ot => Mach.Null (* FIXME *)
       | Ast.NullableType { expr, nullable } => Mach.Null (* FIXME *)
       | Ast.InstanceType { ty, ... } => Mach.Null (* FIXME *)

but I believe the intention is to evaluate to the appropriate type  

> For
> type expr nested within a value expr, that implies to me that it
> resolves to a runtime representation of that type.

Right -- type meta-objects are discussed here:

See also:

> And that implies that
> every type must have a runtime representation, including structural
> types, e.g. |type {a: int}| would resolve to some runtime  
> representation
> of the structural type defined by |{a: int}|.

Indeed :-).

> But from what I've heard and read up to now, |type x| isn't as generic
> as I'm implying above, so I'm confused.

If I understand your point, it's that if we allow

   (a is T)

given a type name T bound by a type, class, or interface definition;  
but we disallow

   let (t = T) (a is t)

and insist on

   let (t = T) (a is type t)

where (type t) evaluates at runtime to the meta-object for T and then  
'is' proceeds to test whether a's dynamic type is a subtype of T,  
then what good does our restriction on 'is' (that its right operand  
must be a type fixture) actually *do*?

If I'm following myself, then all I can say is: I hear you! I'm with  

> In any case, one advantage of having a runtime representation for  
> every
> type is that structural types can now be more easily used in a ES3- 
> style
> without the usage of type parameters and also allow type bindings  
> to be
> computed at runtime, e.g.
> function foo(x, t) {
>     if (!(x instanceof t))
>        throw some_type_error;
>     print(x);
> }
> ...
> let t;
> if (cond)   // note how t can only be determined at runtime here
>     t = type {a: int};
> else
>     t = type like {a: double};
> ...
> foo(obj, t);

We went down this road before including type parameters, hoping to  
avoid them while saving decidability. Yes, you can write code like  
the above, but it is not the same when foo uses t in a type  
annotation (say x:t in the parameter list). There, t has to be a  
distinguished type parameter, so the checker can see instantiations  
and make sure they pass fixed types too. There's also alpha renaming  
to consider: type params should not collide unhygienically.

> You're right, ES4 does fix some gotchas in ES3.

I certainly hope so -- I'll dance a jig at the end of the day if this  
is clear to everyone.

> I'm going to try to order my thoughts by listing the possible type
> testing functionalities and how they map to operators.
> (a) object ISA class or interface (O(1), early test)
> (b) object's class is a superset of a structural type (O(1), early  
> test)
> (c) object is compatible with a structural type (O(n) where n=#  
> props in
> structural type, late test)
> (d) object's prototype chain includes the prototype of another object
> (O(n) where n=length of chain, late test)
> (e) object ISA x, where x is determined at runtime to be a class or
> interface (O(1), late test)
> (f) object's class is a superset of x, where x is determined at  
> runtime
> to be a structural type (O(1), late test)
> (g) object is compatible with x, where x is determined at runtime  
> to be
> a structural type (same as (c))
> |is| does (a), (b)


> |is like| does (c)

Nit: your definition for (c) is not using "compatible with structural  
type" the way we define the type compatibility relation (written "~:"  
instead of "<:", see 
id=spec:type_relations). We want soundness by some definition, so (a  
is T) must mean that a cannot mutate a split-second later to violate  
T. But let's say your item (b) covers that.

> |instanceof| does (d)
> |instanceof| does (e) for classes
> nothing does (e) for interfaces

The meta-object stuff can be used:

   use namespace reflect

or something like that.

> nothing does (f) and (g) since there's no "runtime structural type"

No, meta-objects again.

> The asymptotic times don't really matter - only whether the test  
> can be
> performed early does. If something can be tested early, that implies
> that it has to be a type expr. Late testing implies value exprs.
> However, although |is| seems to be capable of doing early testing,  
> if no
> type exceptions are thrown early for |is|, then it is in effect a  
> purely
> runtime check, right?

Yes, modulo strict mode analyses TBD (see above).

> Furthermore, the syntax of type exprs and value exprs are incompatible
> (neither can be subset of each other) because of the following issues
> (AFAIK):
> 1) structural type syntax of type expr collides with object and array
> literal syntax of value expr
> 2) union type syntax collides with comma expr syntax of value expr  
> (e.g.
> |(1,2)|)
> 3) type parameter syntax of type expr collides with type argument  
> syntax
> of value expr

You've got it.

> I've been scratching my head for a good proposal that coherently
> distinguishes |is| and |instanceof| and the associated syntax, but  
> I've
> restarted several times already with no solution. Now I've begun to
> wonder why the two operators can't be unified in some way, considering
> that both are runtime checks. If every type had a runtime  
> representation
> then this could work (see example I gave above for structural types).

The issues are (AFAIK):

* Should instanceof do its loosey-goosey ES3 thing for functions,  
which have mutable .prototype, and mix this backward-compatible  
feature into different-in-many-ways subtype tests done by 'is'-as- 

* Should 'is' insist on fixed type name in its right operand, or is  
this inconsistent and pointless, an RI bug even?

I'm going to summon Graydon and stand back now.


More information about the Es4-discuss mailing list