Principles of Programming Languages www cs bgu ac
Principles of Programming Languages www. cs. bgu. ac. il/~ppl 192 Lesson 12 –Operational Semantics of Scheme
L 1 Semantics L 1 allows primitive operators and primitive values to be recursively combined. In addition, we cam name composite expressions and bind them to variables. For example, the following is a program in L 1: (define x (+ (* 2 3) (* 4 5))) (+ x (* 2 2))
L 1 BNF We present a first version of the Type. Script program which encodes the following BNF in a set of disjoint union types in Type. Script. <program> : : = (L 1 <exp>+) // program(exps: List(exp)) <exp> : : = <define-exp> | <cexp> <define-exp> : : = (define <var-decl> <cexp>) // defexp(var: var-decl, val: cexp) | <bool-exp> <cexp> : : = <num-exp> // // bool-exp(val: Boolean) num-exp(val: Number) | <prim-op> // prim-op(op: String) | <var-ref> // var-ref(var: String) | (<cexp>*) // app-exp(rator: cexp, rands: List(cexp)) <prim-op> : : = + | - | * | / | < | > | = | not <num-exp> : : = a number token <bool-exp> : : = #t | #f <var-ref> : : = an identifier token <var-decl> : : = an identifier token
Primitive Operators We decide to represent Number and Boolean using the corresponding value types in the Type. Script meta-language. We need to decide how to represent primitive operators. E. g. , how do we compute the expression (+) In Scheme, when we compute this expression, we get: > + #<procedure: +> That is, the + expression’s value is a procedure in Scheme. If we implement primitive values as procedures, we need a language that supports first-class procedures (that can be bound to variables).
Primitive Operators In Java. Script (and Type. Script), primitive operators are not variables bound to procedures. Example: In [1]: const plus = (x, y) => x + y; plus Out[1]: [Function: plus] In [2]: + [COMPILE ERROR] Line 1: Expression expected.
Primitive Operators In scheme objects of type [Function] can be variables, but in Java. Script the expression + (where + can be any primitive operator) is not well formed. We represent primitive operators as strings! The interpreter maps each case to the underlying primitive operation in the meta-language. Primitive operators are prim. Op expressions in the AST Prim. Op(op: string). The value of a Prim. Op expression is itself. When we apply a primitive operator to arguments, we perform the calculation using the corresponding meta-language operations.
Variadic Primitives Scheme operators are Variadic and accept any number of arguments (from 0 and up). Since the AST of application forms (app-exp) supports any number of arguments, the syntax of L 1 also supports expressions of the form: (+ 1 2), (+ 1 2 3 4) and even (+ 1) and (+). In Type. Script, we use reduce to apply a procedure to a list of arguments. A potential problem: ‘-’ and ‘/’ are not associative, they require a more delicate handling which we later revise.
L 1 Values We defome L 1 values inductively on the structure of L 1 -AST. Atomic expressions can return: A number (Num. Exp), a boolean (Bool. Exp) , the value of a primitive operator (Prim. Op), the value of a variable reference (Var. Ref) - which can be any value. Define expressions return a void value. Composite expressions return a value returned by the application of a primitive operator (we can prove by induction). We conclude that the set of all possible values computed by L 1 programs is: Value = Number | Boolean | Prim-ops | Void
L 1 Value Type We need to decide what is the return value of define expressions. In Scheme, a define expression does not return any value: > (define x 1) Such expressions are called statements (as they have side-effects). To avoid expression/statement dichotomy, we set define to return the void type Which contains a single value (also called void). In Type. Script, we use the undefined value for this purpose. We thus adopt the value of L 1 programs accordingly: Value = number | boolean | string | Prim. Op | void
Evaluation of compound forms The evaluation of compound forms is recursive, followed by a rule that determines how to combine the resulting values. For special forms, not all the parts of the compound form are always evaluated. In L 1 there is a single special form - define. The order of evaluated is determined by the computation rule. 1. eval(Define. Exp(var, val)) => ; ; var is of type Var. Decl ; ; val is of type Cexp let val: Value = eval(val) add the binding <(var-decl->var var), val> to the global environment return undefined. 2. eval(App. Exp(rator, rands)) => ; ; rator is of type Cexp ; ; rands is of type List(Cexp) let proc = eval(rator) args = [eval(r) for r in rands] return apply. Proc(proc, args)
Global Environment We define the global environment object as a partial mapping from variable references to values. We add two clauses: eval(Var. Ref(var)) => apply. Env(env, var) ; ; Variables are evaluated by looking up their value in the global environment. eval(Define. Exp(var, val)=>. . . add the binding <(var -decl->var var), val> to the global environment. . . The global environment is a partial function as it is not defined on the entire domain. We model environments as an inductive data type: env : : = empty-env | extended-env empty-env // emptyenv() extended-env(variables, values, env) // extendedenv(vars: List(string), vals: List(Value), nextenv: Env) Pre-conditions: length(variables)=length(values)
Global Environment is either an empty environmentor an extended environment on top of an existing environment. We define apply. Env as a value accessor. import { Value } from '. /L 3 -value'; export type Env = Empty. Env | Non. Empty. Env; export interface Empty. Env {tag: "Empty. Env" }; export interface Non. Empty. Env { tag: "Env"; var: string; val: Value; next. Env: Env ; }; export const make. Empty. Env = (): Empty. Env => ({tag: "Empty. Env"}); export const make. Env = (v: string, val: Value, env: Env): Non. Empty. Env => ({tag: "Env", var: v, val: val, next. Env: env}); const is. Empty. Env = (x: any): x is Empty. Env => x. tag === "Empty. Env"; const is. Non. Empty. Env = (x: any): x is Non. Empty. Env => x. tag === "Env"; const is. Env = (x: any): x is Env => is. Empty. Env(x) || is. Non. Empty. Env(x); export const apply. Env = (env: Env, v: string): Value | Error => is. Empty. Env(env) ? Error("var not found " + v) : env. var === v ? env. val : apply. Env(env. next. Env, v);
Global Environment is either an empty environmentor an extended environment on top of an existing environment. We define apply. Env as a value accessor. We lookup a variable v in an environment env recursively: No variable is defined in an empty environment - in this case, we return an error. Else, for an environment made up of the binding (var)->(val) and a next environment next. Env: If var is the same as v return the correspond val. Else continue searching in the embedded environment next. Env.
Examples What happens here? // Lookup of any variable in an empty env fails apply. Env(make. Empty. Env(), "x") instanceof Error // Lookup of a variable defined in an extended env succeeds apply. Env(make. Env("x", 1, make. Empty. Env()), "x") // Lookup of a variable that is not defined in an extended env fails apply. Env(make. Env("x", 1, make. Empty. Env()), "y") instanceof Error
Examples What happens here? // Lookup of a variable that is defined in a deeper env is retrieved // Here we have 2 levels: (<y 2> <x 1> <empty>) apply. Env(make. Env("y", 2, make. Env("x", 1, make. Empty. Env())), "x") // Lookup of a variable that is defined in a deeper env is overridden by a newer bindin g // Here we have 2 levels: (<x 2> <x 1> <empty>) apply. Env(make. Env("x", 2, make. Env("x", 1, make. Empty. Env())), "x") Out[12]: 2
Handling Variable References The evaluation of variable references (var-ref environment. expressions) requires access to the We split the implementation of the eval algorithm in two cases: L 1 eval(exp, env): evaluate a <cexp> AST with reference to a given environment. L 1 eval. Program(program): evaluate a program L 1 eval handles the case of evaluating a variable reference with respect to a given environment. The evaluation rule for Var. Ref expressions is now clarified: L 1 eval(Var. Ref(var)) => apply. Env(env, var) ; ; Variables are evaluated by looking up their value in the global environment.
Handling Variable References The evaluation of variable references (var-ref environment. expressions) requires access to the We split the implementation of the eval algorithm in two cases: L 1 eval(exp, env): evaluate a <cexp> AST with reference to a given environment. L 1 eval. Program(program): evaluate a program L 1 eval handles the case of evaluating a variable reference with respect to a given environment. The evaluation rule for Var. Ref expressions is now clarified: L 1 eval(Var. Ref(var)) => apply. Env(env, var) ; ; Variables are evaluated by looking up their value in the global environment.
Handling Define. Exp We can now address the issue of evaluating a program. Recall that programs are a sequence of expressions which are either define expressions, or cexp expressions. Define. Exp(var, val) => let value = L 1 eval(val, env) if there are more expressions in the program: let new. Env = extend. Env(var, val, env) continue evaluating remaining expressions in new. Env else return void
Handling Cexp => let value = L 1 eval(cexp, env) if there are more expressions in the program continue evaluating remaining expressions in env else return value
L 1 Eval We can finally show the L 1 Eval algorithm. Notice that it follows structural induction. const L 1 eval = (exp: CExp, env: Env): Value => is. Error(exp) ? exp : is. Num. Exp(exp) ? exp. val : is. Bool. Exp(exp) ? exp. val : is. Prim. Op(exp) ? exp : is. Var. Ref(exp) ? apply. Env(env, exp. var) : is. App. Exp(exp) ? L 1 apply. Procedure(exp. rator, map((rand) => L 1 eval(rand, env), exp. rands)) : Error("Bad L 1 AST " + exp)
L 1 Eval The L 1 eval. Program invokes eval. Exps to evaluate the sequence of expressions that appear inside the program with an initially empty environment. The expressions can either be of type Define. Exp or CExp. // Purpose: evaluate a program made up of a sequence of expressions. export const L 1 eval. Program = (program: Program): Value => eval. Exps(program. exps, make. Empty. Env()); // Evaluate a sequence of expressions (in a program) const eval. Exps = (exps: Exp[], env: Env): Value => is. Empty(exps) ? Error("Empty program") : is. Define. Exp(first(exps)) ? eval. Define. Exps(exps, env) : is. Empty(rest(exps)) ? L 1 eval(first(exps), env) : is. Error(L 1 eval(first(exps), env)) ? Error("error") : eval. Exps(rest(exps), env); // Eval a sequence of expressions when the first exp is a Define. // Compute the rhs of the define, extend the env with the new binding // then compute the rest of the exps in the new env. const eval. Define. Exps = (exps: Exp[], env): Value => { let def = first(exps), rhs = L 1 eval(def. val, env), new. Env = make. Env(def. var, rhs, env); return is. Empty(rest(exps)) ? undefined : eval. Exps(rest(exps), new. Env); }
Procedure Calls We still need to handle procedure calls. In L 1 we only have primitive procedures as we have not yet provided a way to define user procedures (we will do this next in L 2). Consider for example the computation of this L 1 -expression: (+ (* 2 3) (- 3 2)) This expression evaluates as follows: is. App. Exp(exp) ? L 1 apply. Procedure(exp. rator, map((rand) => L 1 eval(rand, env), exp. rands)) :
Summary-L 1 valuation The key decisions we made are: 1. Primitive operations are represented as syntactically distinct expressions (Prim. Op) with a specific type for their value (Prim. Op as well). 2. Primitive operations are dispatched to the meta-language (Type. Script) based on their name. 3. We represent the global environment using the env inductive datastructure. 4. Define expressions evaluation updates the environment, and they return type is void. 5. The global environment is a mutable data-structure. 6. When evaluating an application expression (App. Exp), we first compute the arguments (in any order), then apply the procedure to the argument values. The set of computed values for L 1 is: Value = Number | Boolean | Prim-ops | Void
- Slides: 23