MuLE/SLam Warm-up Lab[1]

SLam, BNF Grammars, Parsing and Parse Trees[2]

 

 

Objective

After completing this lab, one should feel more comfortable with both the language scheme and the MuLE architecture as well as an understanding of how parsing works in MuLE's languages.

 

 

Introduction

In this lab, we begin to experiment with the design and implementation of various programming languages. The software tool being used is MuLE, a multiple language environment. MuLE provides four interpreters (SLam, SPoc, SOOP, and SLic) that share a common architecture but represent different language paradigms. One objective of this lab is to master the general syntax of the system only once and then use it for all four languages! This lab will introduce the simple functional (applicative) language, SLam. SLam is implemented as an interpreter. When we talk about an interpreter, we say it "acts as an abstract machine that reads a statement, executes the statement, reads the next statement, executes the statement, and so forth ... and so on ... in a cycle that continues until the interpreter is shut down." But, ... it's not quite that simple.

 

 

Goal

SLam has two logical components: a parser and an execute routine. The parser takes an expression and creates a corresponding parse tree. The execute routine traverses the parse tree to evaluate the expression. In doing so, the execute routine uses and updates the environment. The environment is the state of the program, i.e. all of the variable bindings made during interpreter execution. In this lab, our goal is to explore the relationship of SLam's language design to the implementation of the parser and the creation of the parse tree.

 

 

SLam design/grammar

SLaM is designed to be a simple functional (applicative) language that uses a prefix format in its expressions. The SLam grammar is defined using BNF production rules.

 

<slam>

::=

<slam_command>

<slam_command>

::=

clear

<slam_command>

::=

help

<slam_command>

::=

list

<slam_command>

::=

<slam_stmt_list>

<slam_stmt_list>

::=

 

<slam_stmt_list>

::=

<slam_stmt><slam_stmt_list>

<slam_stmt>

::=

(proc <formals> <slam_stmt>)

<slam_stmt>

::=

(assign <id> <slam_stmt>)

<slam_stmt>

::=

(if <boolean_expr> then <slam_stmt> else <slam_stmt>)

<slam_stmt>

::=

(<id> <slam_stmt_list>)

<slam_stmt>

::=

<expr>

<formals>

::=

<formals>

::=

(<id_list>)

<id_list>

::=

 

<id_list>

::=

<id> <id_list>

<expr>

::=

<boolean_expr>

<expr>

::=

<arith_expr>

<expr>

::=

<number>

<expr>

::=

<id>

<boolean_expr>

::=

(<bool_op> <slam_stmt> <slam_stmt>)

<bool_op>

::=

= | > | < | != | and | or | not

<arith_expr>

::=

(<arith_op> <slam_stmt> <slam_stmt>)

<arith_op>

::=

+ | - | * | /

 

 

Part 1. In-lab Exercises (xx pts):

Complete and hand-in before picking up the Lab X assignment sheet.

 

1. (a) Note that the nonterminal <slam_command> may be substituted by any of the following:

ß        list -- instructs the interpreter to print the current global environment

ß        help -- instructs the interpreter to display the SLam grammar

ß        clear -- instructs the interpreter to delete the current global environment

ß        <slam_stmt_list>

 

Are the meta-commands list, help, and clear terminals or nonterminals?

 

 

(b) <slam_stmt_list> is defined "inductively". Explain what this means.

 

 

(c) List the terminal symbols in SLam.

 

 

(d) Two nonterminals are left undefined in the BNF productions given for SLam. What are they and why do you think they were omitted from the table?

 

 

(e) A <slam-stmt> is an acceptable statement/expression in the language. List below the kinds of SLam statements/expressions.

 

 

(f) Show a derivation of the function expression (assign x (+ 2 3))

 

 

(g) Are the parentheses necessary in the function derived in part (f) above? Explain your answer.

 

 

SLam's parser performs its task by using the production rules of the defined grammar. The parser receives a scanned expression. If the expression can be derived from the grammar, the parser returns a parse tree of the expression. For example, we might expect that when the parser receives

(assign x (+ 2 3))

 

the parser might return a data structure representing the tree

                       assign
                        /  \
                       /    \
                      /      \
                     x       +
                            / \
                           /   \
                          /     \
                         2       3

Before we take a look at how the parser performs such a task, it would be helpful to first consider how the data structure for a parse tree is constructed. In SLam, a parse tree is constructed with nested records. These records are similar to Pascal's variant records, C's structs, or objects of a class in C++ or Java. Since SLam is implemented in Scheme, which does not have a pre-defined record data structure, we extend the Scheme language using define-macro.

 

2. (a) From <unique at each installation>, copy the LabX folder to the desktop of your machine. The LabX folder contains a file named utilities.s Start DrScheme and open the utilities.s file. Near the beginning of the file, the documentation indicates that some of the macros in this file are taken from Essentials of Programming Languages by Friedman, Wand and Haynes, MIT Press, 1992. This lab uses some materials from the same book.

 

(b) Use the menu Edit|Find to locate the macro definition that defines a record data structure. It begins

(define-macro define-record
...

Based on the macro definition, what parameters (arguments) do you think are expected when define-record is used to define a record type?

 

 

(c) Scroll to the bottom of the utilities.s file. List the record types defined here.

 

 

(d) Beneath the record types that are already defined, we'll add our own record type for a tree record structure. We assume a binary tree, defined in BNF as

<tree> ::= <number> | (<symbol> <tree> <tree>)

 

Enter the following:

(define-record node (symbol left right))

 

Execute. (Only to confirm an acceptable definition nothing is output to the screen.) When a record structure is defined, the macro works behind the scenes to provide some additional capabilities:

        make-name -- a procedure that takes n arguments and creates a record of type name with n fields.

        name? a predicate that returns #t when passed a record of type name.

        name -> fieldi -- for 1 <= i <=n, a function that returns the value of the indicated field.

 

(e) Now that the node record type has been defined, we can create a tree using node records. Enter the following in the interactions window:

(define atree (make-node 'foo (make-node 'bar 1 2) 3))

 

 

(f) Make a sketch the tree just created.

 

 

(g) Now, test it. In the interactions window, try each of the following and record the results.

(node? atree)

(node->symbol atree)

(node->right atree)

(node->left atree)

(node->symbol (node->left atree))

 

 

3. (a)              In a parse tree, a distinct record type may be used to represent each alternative of a BNF production rule. Looking back at the BNF rule for a tree (see 2 (d) above), it is apparent that we require a second record type to distinguish a tree consisting of a single leaf.

 

(b) Enter in the definitions window (i.e. into your newly modified utilities.s):

(define-record leaf (number))

 

 

(c)We now have trees that are unions of interior node records and leaf records. When traversing a tree, it is convenient to have some way to branch on the type of record and to fetch values from the fields of that kind of record. Fortunately, utilities.s includes a macro named variant-case that allows us to do just that. The variant-case works in a similar as a scheme cond. To illustrate, enter in the definitions window to modify utilities.s the following:


(define leaf-sum
   (lambda (tree)
     (variant-case tree
         (leaf (number) number)
         (node (left right)
             (+ (leaf-sum left) (leaf-sum right)))
         (else (error "leaf-sum: Invalid tree" tree)))))

(d) Now, define in the interactions window three trees using interior nodes and leafs.

(define atree (make-node 'a (make-leaf 2) (make-leaf 3)))

(define btree (make-node 'b (make-leaf -1) atree))

(define ctree (make-node 'c btree (make-leaf 4)))

 

Make a sketch of each tree, properly labeled.

 

 

Execute and then test with the following. Record the results.

(leaf-sum atree)

(leaf-sum btree)

(leaf-sum ctree)

 

 

4. Save your code as utilities-tree.s on a disk or your account.


Part II. Lab X Assignment (xx pts) Due: yy

 

1. Use variant-case to write a function named max-leaf that receives a binary tree of numbers, having at least one interior node, and returns the maximum leaf value. Attach a printout of your defined function and examples of tests with two different trees.

 

 

2. (a) Start DrScheme and open the file named utilities-parse.s (from the LabX folder <installation dependent> ). Execute. This file is a composite of the utilities.s file (used previously) and the SLam parser. Explore the use of the parser by entering the following.

(parse '(assign x (+ 2 3)) )

 

The output should look like this:

#(struct:sbind

x

#(struct:apply

#(struct:prim-proc +)

(#(struct:num 2) #(struct:num 3)))

untyped)

 

The parser has examined the arguments: the expression '(assign x (+ 2 3)) . The expression was found to be acceptable in the grammar of the language, so the parser created and returned a nested record structure representing the parse tree of the expression.

 

#(struct:sbind a record representing the binding of an expression to an identifier

#(struct:apply a record representing the application of a function

#(struct:prim-proc a record containing a primary operator

#(struct:num a leaf record containing a number

 

The tree would look something like this

                    BIND
                    / \
                   /   \
                  x    APPLY
                        / | \
                       /  |  \
                      +   2   3


(b) Enter each of the following parse expressions and record the results. For each, make a sketch of the parse tree.

 

(parse '(assign x 5) )

 

(parse '(help) )

 

(parse '(proc (x y) (< x y)) )

 

 

(c) Using the BNF grammar for SLam, write a call to parse a statement that increments x if x is less than 10 (otherwise, leaves x unchanged). Attach a printout of your parse expression and the parse tree that is returned along with a sketch of the parse tree. [For example, if you were to write a call to parse for a statement that assigns 1 to a, the call would look like (parse '(assign a 1)) .]



[1] This work was partially funded by National Science Foundation CCLI-DUE # 9952398.

[2] This lab was created jointly with Dr. Wanda Dann of Ithaca College.