Goals
Here are some design notes for JS2, starting with my goals, shared in large part by ECMA TG1 for ECMA-262 Edition 4:
- Support programming in the large with stronger types and naming.
- Enable bootstrapping, self-hosting, and reflection.
- Backward compatibility apart from a few simplifying changes.
(Goal 2 implies many things beyond what is discussed in these notes.) Non-goals, again shared (mostly!) by ECMA TG1 going back to Waldemar’s Edition 4 drafts:
- To become more like any other language (including Java!).
- To be more easily optimized than the current language.
Types
In JS today, every expression has a type, as specified by ECMA-262 Edition 3
Chapter 8. The visible types, by their spec names, are Undefined, Null, Boolean, Number, String, and Object. These types are disjoint sets of values:
Undefined = {undefined}, Null = {null}, etc.
Int32 and UInt32 are subsets of Number (IEEE-754 double precision) subject to
different operators from Number, and they appear only in the bitwise operators,
Array length
, and a few other special cases.
Edition 3 Chapter 9 defines somewhat ad-hoc, mostly useful conversion rules between types. Chapter 15 contains constructor specifications that may also convert according to the Chapter 9 rules, or ad-hoc variations on those rules.
One oddness to JS1: the so-called primitive types Boolean, Number, and String each have a corresponding Object subtype: Boolean
, Number
, and String
respectively. When a primitive value is used as an object, it is automatically “boxed” or wrapped by a new instance of the corresponding object subtype. When used in an appropriate primitive type context, the box/wrapper converts back to the primitive value.
For JS2 and ECMA-262 Edition 4, we would like to use modern type theory to avoid the pitfalls and contradictions of less formal, ad-hoc approaches. We define a lattice of all type value sets induced by the set contains
subset relation, in order to:
- Define new operators to enable programmers to test and enforce invariants using type annotations (goal 1).
- Let users define their own types by writing class extensions that can do anything the native classes can do (goal 2).
- Eliminate primitive types and boxing, coalescing each Edition 3 primitive type with its object wrapper (simplifying exception from goal 3).
- Provide nullability and rationalize Undefined (goals 1 and 3).
The lattice is as follows, with arcs directed downward by default (arcs directed otherwise have an arrowhead showing direction):
___⊤___ / / Void Object?__________ / / Null<----String? Object__________ / | / | String Number Boolean User... (double) / | / | int uint ...
⊤ is the top type, not named T
in the language. Edition 3’s Undefined is renamed to Void, as in Waldemar’s Edition 4 drafts.
For all object types t, there exists a nullable type t? = t ∪ Null. Only Object? and String? are shown above, but every object subtype is nullable. Note that this is just a specification notation; we have not committed to adding the ? typename suffix for nullability to the language.
The User… type stands for a hedge of user-defined type trees. I’ve left out Array, RegExp, Date, etc., because they can be thought of as
user-defined Object subtypes. Also, not all proposed numeric types are shown (not all are subtypes of IEEE double).
Type operators
Given a value and a type, you can ask whether the value is a member of the type’s set using the is
relational operator:
v is t ≡ v ∈ t's value set ⇒ Boolean
A class defines an object type, and class C extends B {...}
defines a subclass C of base class B. All values of a subclass type are members of its superclass type, so (new C is B)
.
Given a value of unknown type, the as
relational operator coerces (or downcasts) the value to the type, resulting in null
if the value is not a member of the type:
v as t ≡ (v is t ? v : null) ⇒ t?
So, e.g., undefined as Object === null
— this shows how the type of an as t
expression is t?
.
Given a value of arbitrary type, the to
relational operator converts the value to be a member of the nullable extension type, or throws a TypeError
exception.
v to t ≡ (v is t ? v : v converted to t) ⇒ t? or throw TypeError
The to
operator may result in t
rather than t?
, at the discretion of the class implementing t
(e.g., null to Boolean === false
). A class may define its own to
operator using the following syntax:
class C extends B { ... function to C(v) {...} }
We will redefine the type conversions specified variously in Edition 3 Chapters 9 and 15 in terms of the to
operator applied to the native classes.Our current thinking is that to
conversions follow Chapter 9, except for any of (Null ∪ Void) to
(String ∪ Object), which all result in null
, not "null"
, "undefined"
, or a TypeError throw.
Type annotations
Testing and enforcing invariants using these type operators in expressions governing control flow is sometimes useful, but often tedious, error-prone, and bloaty. We wish for typed declarations that enable the language implementation to do the testing and enforcing for us. Therefore for each of the three type operators is
, as
,and to
, there is a corresponding type annotation that may be used with var
, const
, and function
declarations to specify type:
var v is t = x ≡ if (!(x is t)) throw TypeError; var v = x var v as t = x ≡ var v = x as t var v to t = x ≡ var v = x to t
The initializer is optional as usual; if missing, a sane default value for the annotated type is used. For all assignments v = x
following such a type-annotated variable declaration, the production on the right of ≡ above, stripped of var
, is evaluated. Function formal parameters and the function’s return value may be annotated similarly:
function f(a is int, b as Object, c to String) is Number {...}
Type annotations are optional. To support strict options that require every declaration to be annotated, *
may be used for ⊤ (the top type), e.g. var v is *
, which is equivalent to var v
. Note that *
is used differently for E4X, but its meaning as ⊤ is unambiguous in type operator and annotation right operand contexts.
In a nutshell, is t
annotations insist on type t
and defend against null and undefined (no more “foo has no properties” errors; with static analysis, an error that can’t be avoided at runtime can even be reported at compile time). as t
annotations enforce (is t
)-or-null invariance. And to t
annotations convert according to cleaner, class-extensible rules.
Coming soon
In the next update, I’ll list the small number of incompatible changes to Edition 3 that we are considering. In a subsequent item, I will discuss stronger naming mechanisms to support programming in the large.