July 2000 Draft
JavaScript 2.0
Rationale
Execution Model
|
Thursday, November 11, 1999
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.
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.
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.
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:
local
prefix, variable declarations of variables at the global scope
cause the variables to be created at the time the program is entered rather than at the time the declaractions are evaluated.local
prefix, variable declarations of local variables inside a function
cause the variables to be created at the time the function is entered rather than at the time the declaractions are evaluated.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.
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.
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:
(x)(y)
is a function call of function x
or a cast of y
to type
x
.#
symbol. For example, #{var x:int = 3}
defines a compile-time constant x and initializes
it to 3. One can also lift a var
, const
, or function
declaration directly by
preceding it with a #
symbol, so #var x:int = 3;
would accomplish the same
thing.int
in the preceding example is such a TypeExpression.#{#var x:int = 3}
) is evaluated at
compile compile time, and so forth.#
if
(
Expression )
Statements [#
else
if
(
Expression )
Statements] ... [#
else
Statements] #
end
if
#
's can appear anywhere on a line.#if
to conditionally exclude compile time code, etc.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.
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 |