You will need to brush up on the ENVIRONMENT MODEL to understand this lecture. ---------------------------------------------------------------------- Metacircular Evaluator - Writing an interpreter for Dylan in Dylan - Disadvantages: not of much value in practice, because you can't run it unless you already have Dylan - Advantages: * Better understanding of how the language works * See that languages are not magic, just another program! * You can change the language (that the interpreter evaluates) -- PS6 - Environment Model in action - Similar to the expression evaluator that we did in PS3, but more complex expressions (most of Dylan) This is an INTERPRETER - You give it a program, and it "walks over" the program and does stuff, running pieces of it. It is not a COMPILER - You give it a program, and it translates it into another program in the machine's native language (machine code). Interpreters are generally: + Easier to build, understand, and get right - *MUCH* slower In PS6, we will build an interpreter based on what is covered in the next few lectures and recitations. ---------------------------------------------------------------------- We've been looking at techniques for implementing computational processes as ABSTRACTIONS: 2 ---> [square] ---> 4 We're going to look at a rather unusual kind of box today, a UNIVERSAL MACHINE -- an evaluator for Dylan, called Brenda. * It can do *anything* that any other machine can. You give it a DESCRIPTION of any computational process, and it acts like that process: +----------------------------------------------------+ | | 2 --->| (method ((x <number>)) (* x x)) ---> [EVALUATOR] | ---> 4 | | +----------------------------------------------------+ Note that the input to the evaluator is a LIST (a DESCRIPTION of a function to compute) not a Dylan procedure! Given a description of squaring -- Or anything else! -- act like that. * This is a pretty strange kind of box: It reconfigures itself so it can act like any other box. * The processor one inside a computer is one -- programmed in the native machine code for the processor. Dylan-expr ---> [Metacircular Evaluator] ---> values It's called a Metacircular Evaluator because: * They like fancy words, * It's a Dylan interpreter written in Dylan. That means there are TWO SEPARATE DYLAN LANGUAGES here! * Dylan, the implementation language * metacircular Dylan, the one we're writing (a subset) We call this one "Brenda" (for all you 90210 fans) WARNING: * They look exactly the same - until we start tinkering with Brenda at least * But don't get them confused! Brenda won't handle all Dylan expressions, some things are too involved. NOTE, there are *many* layers of evaluation/compilation here: The Brenda evaluator is written in Dylan, the Dylan evaluator is written in Java, there is a Java compiler which translates Java code to JVM code, and another compiler (``just in time'') which runs under IE or Netscape and translates JVM code to the machine code for your processor. ---------------------------------------------------------------------- The top level of a Dylan interpreter is a read-eval-print loop which does four things 1. Prints the prompt 2. READS an expression 3. EVALUATES the expression 4. PRINTS the result. (define read-eval-print-loop (method () (bind ((raw (read)) (result (brenda-eval raw *brenda-global-environment*))) (print "Brenda? " raw) (print "Brenda==> " result) (read-eval-print-loop)))) The reader, READ, builds list structures out of characters. You used READ in PS3. If you type an input * it is initially just a sequence of characters, not symbols, lists, etc. * The reader turns these characters into list structure. * There's a whole big technology of readers we don't have time to discuss. - Uses finite state machines (which we did in PS4) Recall from the environment model that top level expressions are evaluated wrt the global environment which here is called *brenda-global-environment* ---------------------------------------------------------------------- What does brenda-eval do? Remember the environment model: * To evaluate a compound expression other than a special form, evaluate the subexpressions and apply the value of the first to the rest. * To evaluate a primitive expression, either look up the variable value or if it is self evaluating return its value. It's a dispatch on the type of expression, implemented using generic functions. Because arguments are always evaluated for regular functions and not evaluated for special forms, we let brenda-apply handle the evaluation of arguments, depending on the kind of application (regular function or special form). But the first position of the combination must be evaluated so brenda-apply will know what it is (regular function, special form, etc). (define-generic-function brenda-eval ((obj <object>) (env <brenda-frame>))) Most kinds of objects evaluate to themselves. (add-method brenda-eval (method ((obj <object>) (env <brenda-frame>)) obj)) Evaluating symbols causes their associated value to be looked up. (add-method brenda-eval (method ((obj <symbol>) (env <brenda-frame>)) (lookup obj env))) Evaluating a compound expression (a list structure) calls brenda-apply (add-method brenda-eval (method ((obj <pair>) (env <brenda-frame>)) (brenda-apply (brenda-eval (head obj) env) (tail obj) env))) It is also often necessary to evaluate a sequence of expressions and then return the value of the last expression, like in the body of a method or a bind, so this is handy. (define eval-sequence (method ((seq <list>) (env <brenda-frame>)) (cond ((null? seq) #f) ((null? (tail seq)) (brenda-eval (head seq) env)) (else: (brenda-eval (head seq) env) (eval-sequence (tail seq) env))))) ------------------------------------------------------------------- What does brenda-apply do? Remember the environment model: * To apply a procedure, evaluate the body of the procedure in a new environment that is built by extending the procedure's environment * To apply a special form, use a particular rule for that special form * To apply a primitive, do the computation for that primitive (define-generic-function brenda-apply ((obj <object>) (args <list>) (env <brenda-frame>))) (add-method brenda-apply (method ((obj <object>) (args <list>) (env <brenda-frame>)) (brenda-error "Apply : First element in combination isn't a function." obj))) For a primitive ---------------- Evaluate the arguments in the environment in which the primitive is being called. Apply the Dylan function that performs the work of the primitive only after verifying the arguments satisfy the type constraints of the parameters. (add-method brenda-apply (method ((obj <brenda-primitive>) (args <list>) (env <brenda-frame>)) (bind (((evaluated-args <list>) (map (method (x) (brenda-eval x env)) args))) (args-params-match? (params obj) evaluated-args) (apply (calls obj) evaluated-args))) CALLS is an accessor of <brenda-primitive> that looks up what Dylan form to apply. For a special form ------------------- Just apply the Dylan function that does the work of the special form without evaluating the arguments. Assume the special form will perform it own type checking. (add-method brenda-apply (method ((obj <brenda-special-form>) (args <list>) (env <brenda-frame>)) ((calls obj) args env))) For regular functions (user-defined methods), ---------------------------------------------- we evaluate the arguments and then evaluate the body in the extended environment (add-method brenda-apply (method ((obj <brenda-user-method>) (args <list>) (e <brenda-frame>)) (bind (((evaluated-args <list>) (map (method (x) (brenda-eval x e)) args))) (args-params-match? (params obj) evaluated-args) (bind ((new-env (expand-environment (params obj) evaluated-args (env obj)))) (eval-sequence (body obj) new-env))))) args-params-match? is a useful function that checks that each evaluated argument is of a matching type to the corresponding parameter's type. ---------------------------------------------------------------------- That's all very nice, but how do we actually represent something like an environment or a procedure object?? * Internal representations in the interpreter! - (Dylan uses is own such internal structures, which are Java data structures; interested folks can check out the source for Dylan) ENVIRONMENT: sequence of frames, each frame is a set of bindings Binding: variable name and value (define-class <brenda-frame> (<object>) (bindings <list>)) (define-class <brenda-local-frame> (<brenda-frame>) (previous <brenda-frame>)) (define-class <brenda-global-frame> (<brenda-frame>)) (define-class <brenda-binding> (<object>) (variable <symbol>) (value <object>)) (define make-brenda-local-frame (method ((prev <brenda-frame>)) (make <brenda-local-frame> bindings: '() previous: prev))) (define make-brenda-global-frame (method () (make <brenda-global-frame> bindings: '()))) (define make-brenda-binding (method ((var <symbol>) (val <object>)) (make <brenda-binding> variable: var value: val))) ---------------------------------------------------------------- The procedure expand-environment, which is used to create a new frame and link it in, is a good illustration of how the environment data structures work: Create a new frame, link it into the current frame, an puts bindings for each variable into that frame p = params a = arguments e = environment (define expand-environment (method ((p <list>) (a <list>) (e <brenda-frame>)) (bind (((e1 <brenda-frame>) (make-brenda-local-frame e))) (set! (bindings e1) (map make-brenda-binding (map variable p) a)) e1))) [Each elt in p has two parts, VARIABLE and TYPE. The accessor VARIABLE extracts out the variable part. Note that p is a list of <brenda-parameter>s so VARIABLE is an accessor of this type. It is defined below.] -------------------------------------------------------------------------- PROCEDURES: Recall that they have three parts: >>> Draw a double-circle procedure object <<< 1. Parameter list 2. Body 3. Environment where created So, do the obvious thing: The class <function> is for all ``runnables'' including special forms (define-class <brenda-function> (<object>)) The class <brenda-method> is for both user-defined and primitive methods, it provides argument list. (define-class <brenda-method> (<brenda-function>) (params <list>)) (define-class <brenda-user-method> (<brenda-method>) (body <list>) (env <brenda-frame>)) This actually creates an internal representation of a procedure object, given an internal representation of the parameter list, the body text and the internal representation of the procedure's environment (it gets called when a method or special form is evaluated). (define make-brenda-user-method (method ((p <list>) (body <list>) (e <brenda-frame>)) (make <brenda-user-method> params: p body: body e: env))) It is used like this: (make-brenda-user-method (convert-params (head args) env) (tail args) env)))) [To see that this makes sense, consider (method ((x <number>) (* x x))) we strip off the METHOD to obtain args = (((x <number>) (* x x))) so (head args) is ((x <number>)) and (tail args) is ((* x x)) as desired.] Where convert-params makes internal parameter representations using the following structure: (define-class <brenda-parameter> (<object>) (variable <symbol>) (type <brenda-class>)) (define make-brenda-parameter (method ((var <symbol>) (t <brenda-class>)) (make <brenda-parameter> variable: var type: t))) ---------------------------------------------------------------------- Let's try it out. Brenda? ((method ((x <number>)) (+ x 3)) 2) 1. The top level REPL calls brenda-eval with this expression and the global environment. 2. In brenda-eval, this is a combination (i.e., a list structure) so the first argument gets evaluated and then brenda-apply gets called brenda-eval gets called with the first elt of the combination, which is (method ((x <number>)) (+ x 3)) This is a list structure so the first argument gets evaluated and then brenda-apply gets called brenda-eval gets called with the first elt of the comb, which is method This is a symbol which gets looked up (it value is defined in the global envt as a special form) brenda-apply gets called with the internal rep of the method special form as its first arg, and with the unevaluated arglist (((x <number>)) (+ x 3)) this call returns an internal representation of a procedure object with a single parameter x and body (+ x 3), by calling make-brenda-user-method 3. brenda-apply gets called with its first argument being the procedure object created above, and its second argument being the list of unevaluated arguments (2) This calls brenda-eval on the body of the procedure, with the environment extended by evaluated argument list 4. Brenda-apply extends the current environment by binding the parameters of the function to the given arguments in this case one parameter (x <number>) and one parameter 2 And evaluates the body in the new envt 5. brenda-eval gets called with (+ x 3). This is a list structure so the first argument gets evaluated and then brenda-apply gets called with the resulting value which is the addition primitive. 6. Brenda-apply applies the addition primitive to the values of the argumetn list (x 3), so brenda-eval gets mapped down this list brenda-eval x -- look up in this extended envt = 2 brenda-eval 3 -- 3 Value of applying the primitive is 5.