July 2000 Draft
JavaScript 2.0
Rationale
Execution Model
previousupnext

Thursday, November 11, 1999

Introduction

When does a declaration (of a value, function, type, class, method, pragma, etc.) take effect? When are expressions evaluated? The answers to these questions distinguish among major kinds of programming languages. Let's consider the following function definition in a language with C++ or Java-like syntax:

gadget f(widget x) {
  if ((gizmo)(x) != null)
    return (gizmo)(x);
  return x.owner;
}

In a static language such as Java or C++, all type expressions are evaluated at compile time. Thus, in this example widget and gadget would be evaluated at compile time. If gizmo were a type, then it too would be evaluated at compile time ((gizmo)(x) would become a type cast). Note that we must be able to statically distinguish identifiers used for variables from identifiers used for types so we can decide whether (gizmo)(x) is a one-argument function call (in which case gizmo would be evaluated at run time) or a type cast (in which case gizmo would be evaluated at compile time). In most cases, in a static language a declaration is visible throughout its enclosing scope, although there are exceptions that have been deemed too complicated for a compiler to handle such as the following C++:

typedef int *x;

class foo {
  typedef x *y;
  typedef char *x;
}

Many dynamic languages can construct, evaluate, and manipulate type expressions at run time. Some dynamic languages (such as Common Lisp) distinguish between compile time and run time and provide constructs (eval-when) to evaluate expressions early. The simplest dynamic languages (such as Scheme) process input in a single pass and do not distinguish between compile time and run time. If we evaluated the above function in such a simple language, widget and gadget would be evaluated at the time the function is called.

Challenges

JavaScript is a scripting language. Many programmers wish to write JavaScript scripts embedded in web pages that work in a variety of environments. Some of these environments may provide libraries that a script would like to use, while on other environments the script may have to emulate those libraries. Let's take a look at an example of something one would expect to be able to easily do in a scripting language:

Bob is writing a script for a web page that wants to take advantage of an optional package MacPack that is present on some environments (Macintoshes) but not on others. MacPack provides a class HyperWindoid from which Bob wants to subclass his own class BobWindoid. On other platforms Bob has to define an emulation class BobWindoid' that is implemented differently from BobWindoid -- it has a different set of private methods and fields. There also is a class WindoidGuide in Bob's package; the code and method signatures of classes BobWindoid and BobWindoid' refer to objects of type WindoidGuide, and class WindoidGuide's code refers to objects of type BobWindoid (or BobWindoid' as appropriate).

Were JavaScript to use a dynamic execution model (described below), declarations take effect only when executed, and Bob can implement his package as shown below. The package keyword in front of both definitions of class BobWindoid lifts these definitions from the local if scope to the top level of Bob's package.

class WindoidGuide; // forward declaration

if (onMac()) {
  import "MacPack";

  package class BobWindoid extends HyperWindoid {
    private field x;
    field g:WindoidGuide;

    private method speck() {...};
    public method zoom(a:WindoidGuide, uncle:HyperWindoid = null):WindoidGuide {...};
  }
} else {
  // emulation class BobWindoid'
  package class BobWindoid {
    private field i:integer, j:integer;
    field g:WindoidGuide;

    private method advertise(h:WindoidGuide):WindoidGuide {...};
    private method subscribe(h:WindoidGuide):WindoidGuide {...};
    public method zoom(a:WindoidGuide):WindoidGuide {...};
  }
}

class WindoidGuide {
  field currentWindoid:BobWindoid;

  method introduce(arg:BobWindoid):BobWindoid {...};
}

On the other hand, if the language were static (meaning that types are compile-time expressions), Bob would run into problems. How could he declare the two alternatives for the class BobWindoid?

Bob's first thought was to split his package into three HTML SCRIPT tags (containing BobWindoid, BobWindoid', and WindoidGuide) and turn one of the first two off depending on the platform. Unfortunately this doesn't work because he gets type errors if he separates the definition of class BobWindoid (or BobWindoid') from the definition of WindoidGuide because these classes mutually refer to each other. Furthermore, Bob would like to share the script among many pages, so he'd like to have the entire script in a single BobUtilities.js file.

Note that this problem would be newly introduced by JavaScript 2.0 if it were to evaluate type expressions at compile time. JavaScript 1.5 does not suffer from this problem because it does not have a concept of evaluating an expression at compile time, and it is relatively easy to conditionally define a class (which is merely a function) by declaring a single global variable g and conditionally assigning either one or another anonymous function to it.

There exist other alternatives in between the dynamic execution model and the static model that also solve Bob's problem. One of them is described at the end of this chapter.

Dynamic Execution Model

In a pure dynamic execution model the entire program is processed in one pass. Declarations take effect only when they are executed. A declaration that is never executed is ignored. Scheme follows this model, as did early versions of Visual Basic.

The dynamic execution model considerably simplifies the language and allows an interpreter to treat programs read from a file identically to programs typed in via an interactive console. Also, a dynamic execution model interpreter or just-in-time compiler may start to execute a script even before it has finished downloading all of it.

One of the most significant advantages of the dynamic execution model is that it allows JavaScript 2.0 scripts to turn parts of themselves on and off based on dynamically obtained information. For example, a script or library could define additional functions and classes if it runs on an environment that supports CSS unit arithmetic while still working on environments that do not.

The dynamic execution model requires identifiers naming functions and variables to be defined before they are used. A use occurs when an identifier is read, written, or called, at which point that identifier is resolved to a variable or a function according to the scoping rules. A reference from within a control statement such as if and while located outside a function is resolved only when execution reaches the reference. References from within the body of a function are resolved only after the function is called; for efficiency, an implementation is allowed to resolve all references within a function or method that does not contain eval at the first time the function is called.

According to these rules, the following program is correct and would print 7:

function f(a:integer):integer {
  return a+b;
}

var b:integer = 4;
print(f(3));

Assuming that variable b is predefined by the host if featurePresent is true, this program would also work:

function f(a:integer):integer {
  return a+b;
}

if (!featurePresent) {
  package var b:integer = 4;
}

print(f(3));

On the other hand, the following program would produce an error because f is referenced before it is defined:

print(f(3));

function f(a:integer):integer {
  return a*2;
}

Defining mutually recursive functions is not a problem as long as one defines all of them before calling them.

Hybrid Execution Model

JavaScript 1.5 does not follow the pure dynamic execution model, and, for reasons of compatibility, JavaScript 2.0 strays from that model as well, adopting a hybrid execution model instead. Specifically, JavaScript 2.0 inherits the following static execution model aspects from JavaScript 1.5:

In addition to the above, the evaluation of class declarations has special provisions for delayed evaluation to allow mutually-referencing classes.

The second condition above allows the following program to work in JavaScript 2.0:

const b:string = "Bee";

function square(a:integer):integer {
  b = a;   // Refers to local b defined below, not global b
  return b*a;
  var b:integer;
}

While allowed, using variables ahead of declaring them, such as in the above example, is considered bad style and may generate a warning.

The third condition above makes the last example from the pure execution model section work:

print(f(3));

function f(a:integer):integer {
  return a*2;
}

Again, actually calling a function at the top level before declaring it is considered bad style and may generate a warning. It also will not work with classes.

Discussion

Compiling The Dynamic Execution Model

Perhaps the easiest way to compile a script under the dynamic execution model is to accumulate function definitions unprocessed and compile them only when they are first called. Many JITs do this anyway because this lets them avoid the overhead of compiling functions that are never called. This process does not impose any more of an overhead than the static model would because under the static model the compiler would need to either scan the source code twice or save all of it unprocessed during the first pass for processing in the second pass.

Compiling a dynamic execution model script off-line also does not present special difficulties as long as eval is restricted to not introduce additional declarations that shadow existing ones (if eval is allowed to do this, it would present problems for any execution model, including the static one). Under the dynamic execution model, once the compiler has reached the end of a scope it can assume that that scope is complete; at that point all identifiers inside that scope can be resolved to the same extent that they would be in the static model.

Conditional Compilation Alternative

Bob's problem could also be solved by using conditional compilation similar in spirit to C's preprocessor. If we do this, we have to ask about how expressive the conditional compilation meta-language should be. C's preprocessor is too weak. In JavaScript applications we'd often find that we need the full power of JavaScript so that we can inspect the DOM, the environment, etc. when deciding how to control compilation. Besides, using JavaScript as the meta-language would reduce the number of languages that a programmer would have to learn.

Here's one sketch of how this could be done:

Note that because variable initializers are not evaluated at compile time, one has to use #var a = int rather than var a = int to define an alias a for a type name int.

This sketch does not address many issues that would have to be resolved, such as how typed variables are handled after they are declared but before they are initialized (this problem doesn't arise in the dynamic execution model), how the lexical scopes of the run time pass would interact with scoping of the compile time pass, etc.

Comparing the Dynamic Execution Model with Conditional Compilation

Both approaches solve Bob's problem, but they differ in other areas. In the sequel "conditional compilation" refers to the conditional compilation alternative described above.


Waldemar Horwat
Last modified Thursday, November 11, 1999
previousupnext