Given the evaluator, we have in our hands a description (expressed in Lisp) (expressed in JavaScript) of the process by which Lisp expressions JavaScript statements and expressions are evaluated. One advantage of expressing the evaluator as a program is that we can run the program. This gives us, running within Lisp, JavaScript, a working model of how Lisp JavaScript itself evaluates expressions. This can serve as a framework for experimenting with evaluation rules, as we shall do later in this chapter.
Our evaluator program reduces expressions ultimately to the application of primitive procedures. functions. Therefore, all that we need to run the evaluator is to create a mechanism that calls on the underlying Lisp JavaScript system to model the application of primitive procedures. functions.
There must be a binding for each primitive procedure function name and operator, so that when eval evaluate evaluates the operator function expression of an application of a primitive, it will find an object to pass to apply. We thus set up a global environment that associates unique objects with the names of the primitive procedures functions and operators that can appear in the expressions we will be evaluating.
Original | JavaScript | |
The global environment also includes bindings for the symbols true and false, | The global environment also includes bindings for undefined and other names, |
Original | JavaScript |
(define (setup-environment) (let ((initial-env (extend-environment (primitive-procedure-names) (primitive-procedure-objects) the-empty-environment))) (define-variable! 'true true initial-env) (define-variable! 'false false initial-env) initial-env)) | function setup_environment() { return extend_environment(append(primitive_function_symbols, primitive_constant_symbols), append(primitive_function_objects, primitive_constant_values), the_empty_environment); } |
Original | JavaScript |
(define the-global-environment (setup-environment)) | const the_global_environment = setup_environment(); |
Setup-environment The function setup_environment will get the primitive names and implementation procedures functions from a list:[1]
Original | JavaScript |
(define primitive-procedures (list (list 'car car) (list 'cdr cdr) (list 'cons cons) (list 'null? null?) (list 'display display) (list 'read read) (list '+ +) (list '- -) (list '* *) ;; more primitives )) (define (primitive-procedure-names) (map car primitive-procedures)) (define (primitive-procedure-objects) (map (lambda (proc) (list 'primitive (cadr proc))) primitive-procedures)) | const primitive_functions = list(list("head", head ), list("tail", tail ), list("pair", pair ), list("is_null", is_null ), list("+", (x, y) => x + y ), $\langle{}$more primitive functions$\rangle$ ); const primitive_function_symbols = map(f => head(f), primitive_functions); const primitive_function_objects = map(f => list("primitive", head(tail(f))), primitive_functions); |
To apply a primitive procedure, primitive function, we simply apply the implementation procedure function to the arguments, using the underlying Lisp JavaScript system:[2]
Original | JavaScript |
(define (apply-primitive-procedure proc args) (apply-in-underlying-scheme (primitive-implementation proc) args)) | function apply_primitive_function(fun, arglist) { return apply_in_underlying_javascript( primitive_implementation(fun), arglist); } |
Original | JavaScript | |
For convenience in running the metacircular evaluator, we provide a driver loop that models the read-eval-print loop of the underlying Lisp system. It prints a prompt, reads an input expression, evaluates this expression in the global environment, and prints the result. We precede each printed result by an output prompt so as to distinguish the value of the expression from other output that may be printed.[3] (define input-prompt ";;; M-Eval input:\n") (define output-prompt ";;; M-Eval value:\n") (define (driver-loop) (prompt-for-input input-prompt) (let ((input (read))) (if (null? input) 'EVALUATOR-TERMINATED (let ((output (eval input the-global-environment))) (announce-output output-prompt) (user-print output) (driver-loop))))) (define (prompt-for-input string) (newline) (display string)) (define (announce-output string) (newline) (display string)) | For convenience in running the metacircular evaluator, we provide a driver loop that models the read-evaluate-print loop of the underlying JavaScript system. It prints a prompt and reads an input program as a string. It transforms the program string into a tagged-list representation of the statement as described in section 4.1.2—a process called parsing and accomplished by the primitive function parse. We precede each printed result by an output prompt so as to distinguish the value of the program from other output that may be printed. The driver loop gets the program environment of the previous program as argument. As described at the end of section 3.2.4, the driver loop treats the program as if it were in a block: It scans out the declarations, extends the given environment by a frame containing a binding of each name to "*unassigned*", and evaluates the program with respect to the extended environment, which is then passed as argument to the next iteration of the driver loop. const input_prompt = "M-evaluate input: "; const output_prompt = "M-evaluate value: "; function driver_loop(env) { const input = user_read(input_prompt); if (is_null(input)) { display("evaluator terminated"); } else { const program = parse(input); const locals = scan_out_declarations(program); const unassigneds = list_of_unassigned(locals); const program_env = extend_environment(locals, unassigneds, env); const output = evaluate(program, program_env); user_print(output_prompt, output); return driver_loop(program_env); } } |
Original | JavaScript | |
We use JavaScript's prompt function to request and read the input string from the user: function user_read(prompt_string) { return prompt(prompt_string); } |
Original | JavaScript |
(define (user-print object) (if (compound-procedure? object) (display (list 'compound-procedure (procedure-parameters object) (procedure-body object) '<-procedure-env->)) (display object))) | function user_print(string, object) { function prepare(object) { return is_compound_function(object) ? "< compound-function >" : is_primitive_function(object) ? "< primitive-function >" : is_pair(object) ? pair(prepare(head(object)), prepare(tail(object))) : object; } display(string + " " + stringify(prepare(object))); } |
Now all we need to do to run the evaluator is to initialize the global environment and start the driver loop. Here is a sample interaction:
Original | JavaScript |
(define the-global-environment (setup-environment)) (driver-loop) | const the_global_environment = setup_environment(); driver_loop(the_global_environment); |
Original | JavaScript |
;;; M-Eval input: (define (append x y) (if (null? x) y (cons (car x) (append (cdr x) y)))) ;;; M-Eval value: ok | M-evaluate input: function append(xs, ys) { return is_null(xs) ? ys : pair(head(xs), append(tail(xs), ys)); } M-evaluate value: undefined |
Original | JavaScript |
;;; M-Eval input: (append '(a b c) '(d e f)) ;;; M-Eval value: (a b c d e f) | M-evaluate input: append(list("a", "b", "c"), list("d", "e", "f")); M-evaluate value: ["a", ["b", ["c", ["d", ["e", ["f", null]]]]]] |
Original | JavaScript | |
Apply-in-underlying-scheme is the apply procedure we have used in earlier chapters. The metacircular evaluator's apply procedure (section 4.1.1) models the working of this primitive. Having two different things called apply leads to a technical problem in running the metacircular evaluator, because defining the metacircular evaluator's apply will mask the definition of the primitive. One way around this is to rename the metacircular apply to avoid conflict with the name of the primitive procedure. We have assumed instead that we have saved a reference to the underlying apply by doing (define apply-in-underlying-scheme apply) before defining the metacircular apply. This allows us to access the original version of apply under a different name. |
JavaScript's apply method
expects the function arguments in a vector. (Vectors
are called arraysin JavaScript.) Thus, the arglist is transformed into a vector—here using a while loop (see exercise 4.11): function apply_in_underlying_javascript(prim, arglist) { const arg_vector = []; // empty vector let i = 0; while (!is_null(arglist)) { arg_vector[i] = head(arglist); // store value at index $\texttt{i}$ i = i + 1; arglist = tail(arglist); } return prim.apply(prim, arg_vector); // $\texttt{apply}$ is accessed via $\texttt{prim}$ } We also made use of apply_in_underlying_javascript to declare the function apply_generic in section 2.4.3. |