March 1999 Draft
JavaScript 2.0
Execution Model
previousupnext

Tuesday, March 23, 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 a function definition such as:

function f(widget x) gadget {
  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 have to 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 slots. 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).

If JavaScript uses the streaming 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.

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

  package class BobWindoid extends HyperWindoid {
    private slot x;
    slot WindoidGuide g;

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

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

class WindoidGuide {
  slot BobWindoid currentWindoid;

  method introduce(BobWindoid arg) 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.x 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 alternatives in between the streaming execution model and the static model that also solve Bob's problem. One of them is described at the end of this chapter.

Streaming Execution Model

JavaScript 2.0 follows the streaming execution model -- the entire program is processed in one pass. Unlike in JavaScript 1.x, declarations take effect only when they are executed. A declaration that is never executed is ignored.

The streaming execution model considerably simplifies the language and allows a JavaScript 2.0 interpreter to treat programs read from a file identically to programs typed in via an interactive console. Also, a JavaScript 2.0 interpreter or just-in-time compiler may start to execute a script even before it has finished downloading all of it. This was not possible in JavaScript 1.x because the interpreter was required to scan the entire program for declarations in one pass before executing any of the code in the following pass. Streaming also simplifies the execution model for web pages that contain multiple JavaScript scripts and permits one to safely coalesce adjacent SCRIPT tags on a page or divide a single SCRIPT tag into several.

One of the most significant advantages of streaming 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 streaming 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(integer a) integer {
  return a+b;
}

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

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

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

if (!featurePresent) {
  package var integer b = 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(integer a) integer {
  return a*2;
}

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

Discussion

Compiling The Streaming Model

Perhaps the easiest way to compile a script under the streaming 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 streaming 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 streaming 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 streaming model), how the lexical scopes of the run time pass would interact with scoping of the compile time pass, etc.

Comparing Streaming 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 Tuesday, March 23, 1999
previousupnext