In addition to defining the representation of components, the evaluator implementation must also define the data structures that the evaluator manipulates internally, as part of the execution of a program, such as the representation of functions and environments and the representation of true and false.
In order to limit the predicate expressions of conditionals to proper predicates (expressions that evaluate to a boolean value) as we do throughout this book, we insist here that the function is_truthy gets applied only to boolean values, and we accept only the boolean value true to be truthy. The opposite of is_truthy is called is_falsy.[1]
function is_truthy(x) {
return is_boolean(x)
? x
: error(x, "boolean expected, received");
}
function is_falsy(x) { return ! is_truthy(x); }
To handle primitives, we assume that we have available the following functions:
Compound functions are constructed from parameters, function bodies, and environments using the constructor make_function:
function make_function(parameters, body, env) {
return list("compound_function", parameters, body, env);
}
function is_compound_function(f) {
return is_tagged_list(f, "compound_function");
}
function function_parameters(f) { return list_ref(f, 1); }
function function_body(f) { return list_ref(f, 2); }
function function_environment(f) { return list_ref(f, 3); }
We saw in section 4.1.1 that the evaluation of a sequence terminates when a return statement is encountered, and that the evaluation of a function application needs to return the value undefined if the evaluation of the function body does not encounter a return statement. In order to recognize that a value resulted from a return statement, we introduce return values as evaluator data structures.
function make_return_value(content) {
return list("return_value", content);
}
function is_return_value(value) {
return is_tagged_list(value, "return_value");
}
function return_value_content(value) {
return head(tail(value));
}
The evaluator needs operations for manipulating environments. As explained in section 3.2, an environment is a sequence of frames, where each frame is a table of bindings that associate symbols with their corresponding values. We use the following operations for manipulating environments:
To implement these operations we represent an environment as a list of frames. The enclosing environment of an environment is the tail of the list. The empty environment is simply the empty list.
function enclosing_environment(env) { return tail(env); }
function first_frame(env) { return head(env); }
const the_empty_environment = null;
function make_frame(symbols, values) { return pair(symbols, values); }
function frame_symbols(frame) { return head(frame); }
function frame_values(frame) { return tail(frame); }
To extend an environment by a new frame that associates symbols with values, we make a frame consisting of the list of symbols and the list of values, and we adjoin this to the environment. We signal an error if the number of symbols does not match the number of values.
function extend_environment(symbols, vals, base_env) {
return length(symbols) === length(vals)
? pair(make_frame(symbols, vals), base_env)
: error(pair(symbols, vals),
length(symbols) < length(vals)
? "too many arguments supplied"
: "too few arguments supplied");
}
To look up a symbol in an environment, we scan the list of symbols in the first frame. If we find the desired symbol, we return the corresponding element in the list of values. If we do not find the symbol in the current frame, we search the enclosing environment, and so on. If we reach the empty environment, we signal an "unbound name" error.
function lookup_symbol_value(symbol, env) {
function env_loop(env) {
function scan(symbols, vals) {
return is_null(symbols)
? env_loop(enclosing_environment(env))
: symbol === head(symbols)
? head(vals)
: scan(tail(symbols), tail(vals));
}
if (env === the_empty_environment) {
error(symbol, "unbound name");
} else {
const frame = first_frame(env);
return scan(frame_symbols(frame), frame_values(frame));
}
}
return env_loop(env);
}
To assign a new value to a symbol in a specified environment, we scan for the symbol, just as in lookup_symbol_value, and change the corresponding value when we find it.
function assign_symbol_value(symbol, val, env) {
function env_loop(env) {
function scan(symbols, vals) {
return is_null(symbols)
? env_loop(enclosing_environment(env))
: symbol === head(symbols)
? set_head(vals, val)
: scan(tail(symbols), tail(vals));
}
if (env === the_empty_environment) {
error(symbol, "unbound name -- assignment");
} else {
const frame = first_frame(env);
return scan(frame_symbols(frame), frame_values(frame));
}
}
return env_loop(env);
}
The method described here is only one of many plausible ways to represent environments. Since we used data abstraction to isolate the rest of the evaluator from the detailed choice of representation, we could change the environment representation if we wanted to. (See exercise 4.9.) In a production-quality JavaScript system, the speed of the evaluator's environment operations—especially that of symbol lookup—has a major impact on the performance of the system. The representation described here, although conceptually simple, is not efficient and would not ordinarily be used in a production system.[3]
predicateexpression. JavaScript's notion of truthiness and falsiness is captured by the following variants of is_truthy and is_falsy:
function is_truthy(x) { return ! is_falsy(x); }
function is_falsy(x) {
return (is_boolean(x) && !x ) ||
(is_number(x) && (x === 0 || x !== x )) ||
(is_string(x) && x === "") ||
is_null(x) ||
is_undefined(x);
}
Not a Number), which is considered to be a falsy number (also not a typo), along with 0. The numerical value NaN is the result of certain arithmetic border cases such as 0 / 0.