Thursday, June 28, 2012

Proposal for First-class Types in Dart

Posted by Gilad Bracha


First-class Types in Dart

Currently Dart does not provide any access to types as objects at the base level.  We propose to provide a getter on class Object:

@native Type get type;

which returns a reified representation of the class of an object.  Note that the method type can be overridden in subclasses (for example, to implement a remote proxy that hides its implementation).

The declaration of class Type is

class Type {
 @native String toString();
 String descriptor(){...} // return the simple name of the type
}

In other words, Type does not add any API to Object, other than the ability to get its name.  One can use toString() to obtain a description of the type (such as its name), one can test types for equality or identity, one can hash them (assuming we move forward on making all objects hashable) and that is all. The main use of these reified types is to serve as keys to the mirror system. Instances of Type are canonicalized.

An identifier that denotes a type in Dart may now be used in an expression, and denotes the corresponding reified type object. Examples:

new Object().type === Object; // evaluates to true
new Mirrors.reflect(Map);
Type c = List; // List<Dynamic>
Type t = new List<int>().type;
Type s = List<String>; // this requires adjustment to the grammar
class G<T> { foo() {
 Type t = T;
 InterfaceMirror m1 = new Mirrors.reflect(t);
 InterfaceMirror m2 = new Mirrors.reflect(T);
}

The opposite is not the case: one cannot use an expression in contexts where types or classes are specifically required, even if said expression evaluates to a Type object. Hence, the following are illegal:

new List() is c;
// error: assume c is bound to List as above; still, c denotes a variable, not a type


class G<T> {
  T t = new T();
  // error: T is  a type variable bound to some instance of Type, but not class or interface
  static foo() => 42;
  Type gc = G; // G<Dynamic>
  int bar = gc.foo(); // error: gc is bound to G, but denotes a variable not a type
}


No doubt there will be requests to expand the capabilities of class Type to allow the scenarios shown above.  At this time, the expectation is that mirrors  should be used in such cases; that is, use the reified class as a convenient way to obtain a mirror and use the mirror to obtain the desired functionality, as indicated below:

class G<T> {
  T t = new Mirrors.reflect(T).newInstance([],{});
  static foo() => 42;
  Type gc = G;
  int bar = new Mirrors.reflect(gc).invoke(‘foo’, [], {});
}


Spec Changes


The above does require some changes to the spec. While the addition of Object.type could be construed as a library issue, the changed status of type names to valid expressions requires some adjustments.


3. Overview


Dart is a class-based, single-inheritance, pure object-oriented programming language. Dart is optionally typed and supports reified generics and interfaces.  The runtime type of every object is represented as an instance of class Type which can be obtained by calling the getter  type declared in class Object, the root of the Dart class hierarchy.

Dart programs can be statically checked. The static checker will report some violations of the type rules, but such violations do not abort compilation or preclude execution.

Dart programs may be executed in one of two modes: production mode or checked mode. In production mode, static type annotations have absolutely no effect on execution.  In checked mode, assignments are dynamically checked, and certain violations of the type system raise exceptions at run time.


The coexistence between optional typing and reification is based on the following:

  • Reified type information reflects the types of objects at runtime and may always be queried by dynamic typechecking constructs (the analogs of instanceOf, casts, typecase etc. in other languages). Reified type information includes class and interface declarations, the the runtime type (aka class) of of an object, and type arguments to constructors.
  • Static type annotations determine the types of variables and function declarations (including methods and constructors).
  • Production mode respects optional typing. Static type annotations do not affect runtime behavior.
  • Checked mode utilizes static type annotations and dynamic type information aggressively yet selectively to provide early error detection during development.


Dart programs are organized in a modular fashion into units called libraries. Libraries are units of encapsulation and may be mutually recursive.

However they are not first class.  To get multiple copies of a library running simultaneously, one needs to spawn an isolate.  

10.29 Identifier Reference


An identifier expression consists of a single identifier; it provides access to an object via an unqualified name.

identifier:       IDENTIFIER
  ;


IDENTIFIER_NO_DOLLAR:      IDENTIFIER_START_NO_DOLLAR IDENTIFIER_PART_NO_DOLLAR*
   ;
IDENTIFIER:      IDENTIFIER_START IDENTIFIER_PART*
   ;
   ;

BUILT_IN_IDENTIFIER:      abstract
  | as    | assert
  | Dynamic    | factory    | get    | implements
  | interface    | operator    | set    | static    | typedef    ;


IDENTIFIER_START:      IDENTIFIER_START_NO_DOLLAR
   | '$'
   ;
IDENTIFIER_START_NO_DOLLAR:      LETTER
   | '_'
   ;

IDENTIFIER_PART_NO_DOLLAR:      IDENTIFIER_START_NO_DOLLAR
   | DIGIT
   ;



IDENTIFIER_PART:      IDENTIFIER_START
   | DIGIT
   ;
qualified:      identifier ('.' identifier)?
   ;



A built-in identifier is one of the identifiers produced by the production BUILT_IN_IDENTIFIER. It is a compile-time error if a built-in identifier is used as the declared name of a class, interface, type variable or type alias. It is a compile-time error to use a built-in identifier other than   Dynamic as a type annotation. It is a static warning if a built-in identifier is used as the name of a user-defined variable, function or label.

Built-in identifiers are identifiers that are used as keywords in Dart, but are not reserved words in Javascript. To minimize incompatibilities when porting Javascript code to Dart, we do not make these into reserved words. Ergo, a built-in identifier may not be used to name a class or type.  In other words, they are treated as reserved words when used as types. This eliminates many confusing situations without causing compatibility problems.

Evaluation of an identifier expression e of the form id proceeds as follows:
Let d be the innermost declaration in the enclosing lexical scope whose name is id. If no such declaration exists in the lexical scope, let d be the declaration of the inherited member named id if it exists.

  • If d is a class, interface, or type alias T, the value of e is the unique instance of class Type reifying T.
  • If d is a type parameter T, then the value of e is the value of the actual type argument corresponding to T that was  passed to the generative constructor that created the current binding of this.
  • If d is a library variable then:
    • If d is of one of the forms var v = ei; , T var v = ei; , final v = ei; , final T v = ei; , const v = ei; or const T v = ei;  and no value has yet been stored into v then the initializer expression ei is evaluated. If the evaluation succeeded yielding an object o, let r = o, otherwise let r = null. In any case, r is stored into v. The value of e is r.
    • If d is of one of the forms const v = e; or const T v = e; the result of the getter is the value of the compile time constant e. Otherwise
    • e evaluates to the current binding of id.  This case also applies if d is a library function declaration, as these are equivalent to function-valued variable declarations.
  • If d is a local variable or formal parameter then e evaluates to the current binding of id.  This case also applies if d is a local function declaration, as these are equivalent to function-valued variable declarations.

  • If d is a static method, then e evaluates to the function defined by d.
  • If d is the declaration of a static variable or static getter declared in class C, then e is equivalent to the getter invocation C.id.
  • If d is the declaration of a top level getter, then e is equivalent to the getter invocation id.
  • Otherwise e is equivalent to the property extraction  this.id.

Proposal to eliminate interface declarations from Dart

Posted by Gilad Bracha


Eliminating Interface Declarations from Dart


This document motivates the planned phase-out of interface declarations from the Dart language, and details the required specification changes.

Motivation


In Dart, every class engenders an implicit interface.  Now that this feature is implemented, it is possible to actually eliminate interface declarations from the language. Interface declarations are replaced by purely abstract classes.

Almost every existing interface declaration can already be mapped into an abstract class declaration with no impact whatsoever on other code, by following this formula:

interface I<T> extends J, K  default F<T>{
 set p=(R x);
 R get p;
 U f1;
 final V f2;
 T0 m(T1 a1, ..., Tn An);
 I(S1 p1, ..., Sk pk);
}

maps to

abstract class I’<T> implements J, K {
abstract set p=(R x);
abstract R get p;
abstract set f1=(U _);
abstract U get f1;
abstract V get f2;
abstract T0 m(T1 a1, ..., Tn An);
factory I’(S1 p1, ..., Sk pk){return new F<T>(p1, ..., pk);
}


The implicit interface of I’ is completely equivalent to I in every context.

There are several problems with the above scheme. The most obvious is that it is verbose, mainly due to the extensive use of the abstract modifier. This is easily addressed by eliminating the abstract modifier on methods (respectively getters, setters). If an instance method (getter, setter) has no body, it is deemed to be abstract.

The abstract modifier on the class is unnecessary in this situation as well, so we now have

class I’<T> implements J, K {
set p=(R x);
R get p;
set f1=(U _);
U get f1;
V get f2;
T0 m(T1 a1, ..., Tn An);
factory I(S1 p1, ..., Sk pk){return new F<T>(p1, ..., pk);
}

Beyond the syntax, the following situations are not well supported by the transformation given above:

  1. An interface with a constant constructor cannot be represented.
  2. A generic interface J whose default factory class does not implement J cannot be represented.
  3. Default parameters of the factory constructor need to be repeated, causing a maintenance problem.
  4. The planned support for detecting if a an optional argument was passed by the caller will not serve the factory class’ code.

These are all addressed by the introduction of redirecting factory constructors.



Spec Changes


The changes eliminating the abstract modifier on methods, getters and setters are already in the specification. Various places in the specification need to have any mention of interface declarations removed, but those minor changes are not reflected below.

As always, spec changes (additions and modifications, but not deletions) are highlighted in yellow.



7.6.3 Redirecting Factory Constructors


A redirecting factory constructor specifies a call to a constructor of another class that is to be used whenever the redirecting constructor is called.

redirectingFactoryConstructorSignature:      const? factory  identifier  ('.' identifier)?  formalParameterList `=’ typeName ('.' identifier)?
   ;

Calling a redirecting factory constructor k causes the constructor k’ denoted by typeName (respectively, typeName.identifier) to be called with the actual arguments passed to k, and returns the result of k’ as the result of k.

Note that it is not possible to modify the arguments being passed to  k’. This is a deliberate decision, so that k’ can easily determine what arguments were actually passed by the caller (but we have the same issue with other redirecting constructors, no?).

At first glance, one might think that ordinary factory constructors could simply create instances of other classes and return them, and that redirecting factories are unnecessary. However, redirecting factories have several advantages:
  • An abstract class may provide a constant constructor that utilizes the constant constructor of another class.
  • A factory constructor to which calls are being forwarded can determine whether any user arguments were explicitly passed.
  • A factory constructors avoids the need for forwarders to repeat the default values for formal parameters in their signatures.
  • A generic factory class that aggregates factory constructors for types it does not implement can still have its type arguments passed correctly.

An example of the latter point:

class W<T> implements A<T> { W(w) {...} ...}
class X<T> implements A<T> { X(x) {...} ...}
class Y<T> implements A<T> { Y(y) {...} ...}
class Z<T> implements A<T> { Z(z) {...} ...}


class F<T> { // note that F does not implement A
 static F<T> idw(w) => new  W<T>(w); // illegal - T not in scope in idw
 factory F.idx(x) => new X<T>(x);
 factory F.idy(y) => new Y<T>(y);
 static F idz(z) => new  Z(z); // does not capture the type argument
}

class A<T>{
 factory A.idw(w) => F<T>.idw(w);
// illegal - cannot pass type parameter to static method
 factory A.idx(x) => F<T>.idx(x); // works, but allocates a gratuitous instance of F
 factory A.idy(y) = F<T>.idy; // works
The last two look suspiciously similar;
 factory A.idz(z) => F.idz(z); // wrong - returns Z<Dynamic>; no way to pass type argument
}

It is a compile-time error if k is prefixed with the const modifier but k’ is not a constant constructor.

It is a static warning if the function type of k’ to is not a subtype of the type of k.

This implies that the arguments to k are always legal arguments to k’, and that the resulting object conforms to the interface of the immediately enclosing class of k.

It is a static type warning if any of the type arguments to k’ are not subtypes of the bounds of the corresponding formal type parameters of typeName.



9. Interfaces



An interface defines how one may interact with an object. An interface has methods, getters and setters and a set of superinterfaces.

It is a compile-time error if an interface member m1 overrides an interface member m2 and  m1 has a different number of required parameters than m2. It is a compile-time error if an interface member m1 overrides  an interface member m2 and  m1 does not declare all the named parameters declared by m2 in the same order.

It is a static warning if an interface member m1 overrides an interface member m2 and the type of m1 is not a subtype of the type of m2.  It is a static warning if an interface method m1  overrides an interface method m2,  the signature of m2 explicitly specifies a default value for a formal parameter p and the signature of m1 specifies a different default value for p.


9.1 Superinterfaces

An interface has a set of direct superinterfaces.

An interface J is a superinterface of an interface I iff either J is a direct superinterface of I or J is a superinterface of a direct superinterface of I.

It is a compile-time error if an interface is a superinterface of itself.



Inheritance and Overriding


Let I be the implicit interface of a class C.  I inherits any instance members of its superinterfaces that are not overridden by members declared in C.

However, if there are multiple members m1, …,  mk with the same name n that would be inherited (because identically named members existed in several superinterfaces) then at most one member is inherited. If the static types T1, …,  Tk of the members m1, …,  mk are not identical, then there must be a member mx such that Tx <: Ti, 1 <= x <= k for all  i, 1 <= i <=  k, or a static type warning occurs. The member that is inherited is mx, if it exists; otherwise:
  • If all of m1, …,  mk have the same number r of required parameters and the same set of named parameters s, then I has a method named n, with r required parameters of type Dynamic, named parameters s of type Dynamic and  return type Dynamic.  
  • Otherwise none of the members  m1, …,  mk is inherited.


The only situation where the runtime would be concerned with this would be during reflection if a mirror attempted to obtain the signature of an interface member.

The current solution is a tad complex, but is robust in the face of type annotation changes.  Alternatives: (a) No member is inherited in case of conflict. (b) The first m is selected (based on order of superinterface list) (c) Inherited member chosen at random.  

(a) means that the presence of an inherited member of an interface varies depending on type signatures.  (b) is sensitive to irrelevant details of the declaration and (c) is liable to give unpredictable results between implementations or even between different compilation sessions.

10.10 Instance Creation


Instance creation expressions invoke constructors to produce instances.

It is a compile-time error if any of the type arguments to a constructor of a generic type invoked by a new expression or a constant object expression do not denote types in the enclosing lexical scope. It is a compile-time error if a constructor of a non-generic type invoked by a new expression or a constant object expression is passed any type arguments. It is a compile-time error if a constructor of a generic type with n type parameters invoked by a new expression or a constant object expression is passed m type arguments where m != n.

It is a static type warning if any of the type arguments to a constructor of a generic type G invoked by a new expression or a constant object expression are not subtypes of the bounds of the corresponding formal type parameters of G.



10.10.1 New

The new expression invokes a constructor.

newExpression:
;

Let e be a new expression of the form new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) or the form new T(a1, .., an, xn+1: an+1, …, xn+k: an+k). It is a static warning if T is not a class accessible in the current scope, optionally followed by type arguments.

If e is of the form new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a static warning if T.id is not the name of a constructor declared by the type T. If e of the form new T(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a static warning if the type T does not declare a constructor with the same name as the declaration of T.

If T is a parameterized type S<U1, ,.., Um>, let R = S.  It is a compile time error if S is not a generic type with m type parameters. If T is not a parameterized type, let R = T.
Furthermore, if e is of the form new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) then let  q be the constructor T.id, otherwise let q be the constructor T. Finally, if R is generic but T is not a parameterized type, then for 1 <= i <= m, let Vi = Dynamic, otherwise let Vi = Ui.  

Evaluation of e proceeds as follows:

If T is not a class or interface accessible in the current scope, a dynamic error occurs. Otherwise, if q is not defined, a NoSuchMethodError is thrown.  Otherwise, if q is a generative constructor (regardless of whether q is redirecting or not), then:

Let Ti be the type parameters of R (if any) and let Bi be the bounds of Ti, 1 <= i <= m. It is a dynamic type error if, in checked mode, Vi is not a subtype of  [V1,  ..., Vm/T1,  ..., Tm]Bi, 1 <= i <= m.

A fresh instance, i,  of class R is allocated. For each instance variable f of i,  if the variable declaration of f has an initializer expression ef, then ef is evaluated to an object of and f is bound to of. Otherwise f is bound to null.  

Observe that this is not in scope in ef. Hence, the initialization cannot depend on other properties of the object being instantiated. Do we want to say that this is not in scope, or that using this is illegal?

Next, the argument list (a1, …, an, xn+1: an+1, …, xn+k: an+k) is evaluated. Then, q is executed with this bound to i, the type parameters (if any) of R bound to the actual type arguments V1, ..., Vm and the formal parameters of q bound to the corresponding actual arguments. The result of the evaluation of e is i.


Otherwise, q is a factory constructor. Then:

Let Ti be the type parameters of R (if any) and let Bi be the bounds of Ti, 1 <= i <= m. In checked mode, it is a dynamic type error if Vi is not a subtype of  [V1,  ..., Vm/T1,  ..., Tm]Bi, 1 <= i <= m.
If q is a redirecting factory constructor of the form T(p1, …, pn+k) = c; or of the form  T.id(p1, …, pn+k) = c; then the result of the evaluation of e is equivalent to evaluating the expression [V1,  ..., Vm/T1,  ..., Tm](new c(a1, …, an, xn+1: an+1, …, xn+k: an+k)).

Otherwise, the argument list (a1, …, an, xn+1: an+1, …, xn+k: an+k) is evaluated. Then, the body of q is executed  with respect to the bindings that resulted from the evaluation of the argument list and the type parameters (if any) of q bound to the actual type arguments V1, ,.., Vm resulting in an object i. The result of the evaluation of e is i.


It is a static warning if q is a constructor of an abstract class and q is not a factory constructor.

The above gives precise meaning to the idea that instantiating an abstract class leads to a warning.  A similar clause applies to constant object creation in the next section.

In particular, a factory constructor can be declared in an abstract class and used safely, as it will either produce a valid instance or lead to a warning inside its own declaration.

The static type of a new expression of either the form new T.id(a1, .., an) or the form new T(a1, .., an) is T. It is a static warning if the static type of ai, 1 <= i <= n+ k may not be assigned to the type of the corresponding formal parameter of the constructor T.id (respectively T).



10.10.2 Const


A constant object expression invokes a constant constructor.

constObjectExpression:
;


Let e be a constant object expression of the form const T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) or the form const T(a1, .., an, xn+1: an+1, …, xn+k: an+k). It is a compile-time error if T is not a class accessible in the current scope, optionally followed by type arguments.  It is a compile-time error if T includes any type variables.

If e is of the form const T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a compile-time error if T is not a class accessible in the current scope, optionally followed by type arguments.  It is a compile-time error if T.id is not the name of a constant constructor declared by the type T. If e is of the form const T(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a compile-time error if the type T does not declare a constant constructor with the same name as the declaration of T.

In all of the above cases, it is a compile-time error if ai, 1 < = i <= n + k, is not a compile-time constant expression.

If T is a parameterized type S<U1, ,.., Um>, let R = S; It is a compile time error if S is not a generic type with m type parameters. If T is not a parameterized type, let R = T.
Finally, if R is generic but T is not a parameterized type, then for 1 <= i <= m, let Vi = Dynamic, otherwise let Vi = Ui.  

Evaluation of e proceeds as follows:

First, if e is of the form const T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) then let i be the value of the expression new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k). Otherwise, e must be of the form  const T(a1, .., an, xn+1: an+1, …, xn+k: an+k), in which case let i be the result of evaluating new T(a1, .., an, xn+1: an+1, …, xn+k: an+k) . Then:
  • If during execution of the program, a constant object expression has already evaluated to an instance j of class R with type arguments Vi 1 <= i <= m, then:
    • For each instance variable f of i, let vif be the value of the f in i, and let vjf be the value of the field f in j. If  identical(vif , vjf) for all fields f in i, then the value of e is j, otherwise the value of e is i.
  • Otherwise the value of e is i.

In other words, constant objects are canonicalized.  In order to determine if an object is actually new, one has to compute it; then it can be compared to any cached instances. If an equivalent object exists in the cache, we throw away the newly created object and use the cached one. Objects are equivalent if they have identical fields and identical type arguments. Since the constructor cannot induce any side effects, the execution of the constructor is unobservable.  The constructor need only be executed once per call site, at compile-time.

The static type of a constant object expression of either the form const T.id(a1, .., an) or the form const T(a1, .., an) is T. It is a static warning if the static type of ai, 1 <= i <= n+ k may not be assigned to the type of the corresponding formal parameter of the constructor T.id (respectively T).

It is a compile-time error if evaluation of a constant object results in an uncaught exception being thrown.

To see how such situations might arise, consider the following examples:

class A {
 static final x;
 const A(var p): p = x * 10;
}

const A(“x”); //compile-time error
const A(5); // legal

class IntPair {
 const IntPair(this.x, this.y);
 final int x;
 final int y;
 operator *(v) => new IntPair(x*v, y*v);
}

const A(const IntPair(1, 2)); // compile-time error: illegal in a subtler way

Due to the rules governing constant constructors, evaluating the constructor A() with the argument “x” or the argument const IntPair(1, 2) would cause it to throw an exception, resulting in a compile-time error.

Given an instance creation expression of the form const q(a1, .., an) it is a static warning if T is an  abstract class and q is not a factory constructor.  



12. Libraries and Scripts


A library consists of (a possibly empty) set of imports, and a set of top level declarations. A top level declaration is either a class, a type declaration, a function or a variable declaration.

topLevelDefinition:      classDefinition      | functionTypeAlias    | functionSignature functionBody    | returnType? getOrSet identifier formalParameterList functionBody    | (final | const) type? staticFinalDeclarationList ';'
   |
variableDeclaration ';'
   ;


getOrSet:
 get
| set
;

libraryDefinition:      scriptTag? libraryName import* include* resource* topLevelDefinition*
   ;

scriptTag:
 “#!” (~NEWLINE)* NEWLINE
;

libraryName:
 “#library” “(” stringLiteral “)” “;”
 ;

A library may optionally begin with a script tag, which can be used to identify the interpreter of the script to whatever computing environment the script is embedded in. The script tag must appear before any whitespace or comments.  A script  tag begins with the characters #! and ends at the end of the line. Any characters after #! are ignored by the Dart implementation.

The name of a library can be used for printing and, more generally, reflection. The name may be relevant for further language evolution (such as first class libraries) as well. In the future it may also serve to define a default prefix when importing.

Libraries are units of privacy. A private declaration declared within a library L can only be accessed by code within L. Any attempt to access a private member declaration from outside L will cause a run-time error. Since top level privates are not imported, using them is a compile time error and not an issue here.

The public namespace of library L is the mapping that maps the simple name of each public top level member m of L to m.

The scope of a library L consists of the names introduced of all top level declarations declared in L, and the names added by L's imports.

Libraries may include extralinguistic resources (e.g., audio, video or graphics files)

resource:
 “#resource” “(” stringLiteral “)” “;”
;

It is a compile-time error if the argument x to a library or resource directive is not a compile-time constant, or if x involves string interpolation.


… unchanged …




Interface Types
The implicit interface of class I is a direct supertype of the implicit interface of class J iff:
  • If I is Object, and J has no extends clause.
  • If I is listed in the extends clause of J.
  • If I is listed in the implements clause of J.


A type T is more specific than a type S, written TS,  if one of the following conditions is met:
  1. Reflexivity: T is S.
  2. T is bottom.
  3. S is Dynamic.
  4. Direct supertype: S is a direct supertype of T.
  5. T is a type variable and S is the upper bound of T.
  6. Covariance: T is of the form I<T1, ..., Tn> and S is of the form I<S1, ..., Sn> and Ti  ≪ Si , 1 <= i <= n.
  7. Transitivity: TU and US.

≪ is a partial order on types.
T is a subtype of S, written T <: S, iff [bottom/Dynamic]TS.
Note that <: is not a partial order on types, it is only binary relation on types. This is because <: is not transitive. If it was, the subtype rule would have a cycle. For example:
List <: List<String> and List<int> <: List, but List<int> is not a subtype of List<String>.
Although <: is not a partial order on types, it does contain a partial order, namely ≪. This means that, barring raw types, intuition about classical subtype rules does apply.

S is a supertype of T, written S :> T, iff T is a subtype of S.

The supertypes of an interface are its direct supertypes and their supertypes.

A type T may be assigned to a type S, written  T ⇔ S, iff either T <: S or S <: T.
This rule may surprise readers accustomed to conventional typechecking. The intent of the ⇔ relation is not to ensure that an assignment is correct. Instead, it aims to only flag assignments that are almost certain to be erroneous, without precluding assignments that may work.

For example, assigning a value of static type Object to a variable with static type String, while not guaranteed to be correct, might be fine if the runtime value happens to be a string.