April 2002 Draft
JavaScript 2.0
Rationale
Execution Model
previousupnext

Wednesday, December 19, 2001

This page is somewhat out of date.

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.

class WindoidGuide; // forward declaration

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

  global class BobWindoid extends HyperWindoid {
    private var x;
    var g:WindoidGuide;

    private function speck() {...};
    public function zoom(a:WindoidGuide, uncle:HyperWindoid = null):WindoidGuide {...};
  }
} else {
  // emulation class BobWindoid'
  global class BobWindoid {
    private var i:Integer, j:Integer;
    var g:WindoidGuide;

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

class WindoidGuide {
  var currentWindoid:BobWindoid;

  function 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) {
  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.

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 = int32 rather than var a = int32 to define an alias a for a type name int32.

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.

Compiler Blocks

Another alternative execution model briefly considered but rejected is the idea of allowing compiler blocks. A compiler block has the syntax:

   compile { Statement ... Statement }

The compile attribute is a hint that the block may be (but does not have to be) evaluated early. The statements inside this block should depend only on each other, on the results of earlier compiler blocks, and on properties of the environment that are designated as being available early. Other than perhaps being evaluated early, compiler blocks respect all of the scope rules and semantics of the enclosing program. Any definitions introduced by a compiler block are saved and reintroduced at normal evaluation time. On the other hand, side effects may or may not be reintroduced at normal evaluation time, so compiler blocks should not rely on side effects.

compile is an attribute, so it may also be applied to individual definitions without enclosing them in a block.

As an example, after defining

compile var x = 2;

function f1() {
  compile {
    var y = 5;
    var x = 1;
    while (y) x *= y--;
  }
  return ++x;
}

function f2() {
  compile {
    var y = x;
  }
  return x+y;
}

the value of global x will still be 2, calling f1() will always return 121, and calling f2() will return 4. If the statement x=5 is then evaluated at the global level, f1() will still return 121 because it uses its own local x. On the other hand, calling f2() may return either 7 or 10 at the implementation’s discretion — 7 if the implementation evaluated the compile block early and saved the value of y or 10 if it didn’t. As this example illustrates, it is poor technique to define variables inside compiler blocks; constants are usually better.

A fully dynamic implementation of JavaScript 2.0 may choose to ignore the compile attribute and evaluate all compiler blocks at normal evaluation time. A fully static implementation may require that all user-defined types and attributes be defined inside compiler blocks.

Should const definitions with simple constant expressions such as const four = 2+2 be treated as though they were implicitly compiler definitions (compile const four = 2+2)?


Waldemar Horwat
Last modified Wednesday, December 19, 2001
previousupnext