Project 2: Scheme Interpreter

Similar documents
Scheme. Functional Programming. Lambda Calculus. CSC 4101: Programming Languages 1. Textbook, Sections , 13.7

Project 1: Scheme Pretty-Printer

Why do we need an interpreter? SICP Interpretation part 1. Role of each part of the interpreter. 1. Arithmetic calculator.

CSc 520 Principles of Programming Languages

6.184 Lecture 4. Interpretation. Tweaked by Ben Vandiver Compiled by Mike Phillips Original material by Eric Grimson

Functional Programming. Pure Functional Programming

Scheme in Scheme: The Metacircular Evaluator Eval and Apply

Documentation for LISP in BASIC

Below are example solutions for each of the questions. These are not the only possible answers, but they are the most common ones.

6.037 Lecture 4. Interpretation. What is an interpreter? Why do we need an interpreter? Stages of an interpreter. Role of each part of the interpreter

MORE SCHEME. 1 What Would Scheme Print? COMPUTER SCIENCE MENTORS 61A. October 30 to November 3, Solution: Solutions begin on the following page.

CSSE 304 Assignment #13 (interpreter milestone #1) Updated for Fall, 2018

Fall 2018 Discussion 8: October 24, 2018 Solutions. 1 Introduction. 2 Primitives

COP4020 Programming Assignment 1 - Spring 2011

CSE413: Programming Languages and Implementation Racket structs Implementing languages with interpreters Implementing closures

Lexical vs. Dynamic Scope

CS 275 Name Final Exam Solutions December 16, 2016

Fall Semester, The Metacircular Evaluator. Today we shift perspective from that of a user of computer langugaes to that of a designer of

Introduction to Scheme

Announcements. The current topic: Scheme. Review: BST functions. Review: Representing trees in Scheme. Reminder: Lab 2 is due on Monday at 10:30 am.

Functional Programming. Pure Functional Languages

Building a system for symbolic differentiation

11/6/17. Functional programming. FP Foundations, Scheme (2) LISP Data Types. LISP Data Types. LISP Data Types. Scheme. LISP: John McCarthy 1958 MIT

CS 360 Programming Languages Interpreters

;;; Determines if e is a primitive by looking it up in the primitive environment. ;;; Define indentation and output routines for the output for

Principles of Programming Languages 2017W, Functional Programming

Functional programming with Common Lisp

Lecture 09: Data Abstraction ++ Parsing is the process of translating a sequence of characters (a string) into an abstract syntax tree.

Turtles All The Way Down

Functional Programming. Pure Functional Languages

User-defined Functions. Conditional Expressions in Scheme

Computer Science 21b (Spring Term, 2015) Structure and Interpretation of Computer Programs. Lexical addressing

CS 314 Principles of Programming Languages

TAIL RECURSION, SCOPE, AND PROJECT 4 11

Streams, Delayed Evaluation and a Normal Order Interpreter. CS 550 Programming Languages Jeremy Johnson

Scheme Quick Reference

Fall 2017 Discussion 7: October 25, 2017 Solutions. 1 Introduction. 2 Primitives

From Syntactic Sugar to the Syntactic Meth Lab:

Building a system for symbolic differentiation

An Explicit-Continuation Metacircular Evaluator

Intro. Scheme Basics. scm> 5 5. scm>

Discussion 12 The MCE (solutions)

(scheme-1) has lambda but NOT define

Scheme: Strings Scheme: I/O

Summer 2017 Discussion 10: July 25, Introduction. 2 Primitives and Define

A LISP Interpreter in ML

CSc 520. Principles of Programming Languages 7: Scheme List Processing

CSC 533: Programming Languages. Spring 2015

regsim.scm ~/umb/cs450/ch5.base/ 1 11/11/13

6.001, Spring Semester, 1998, Final Exam Solutions Your Name: 2 Part c: The procedure eval-until: (define (eval-until exp env) (let ((return (eval-seq

CSE341: Programming Languages Lecture 17 Implementing Languages Including Closures. Dan Grossman Autumn 2018

CSE 413 Languages & Implementation. Hal Perkins Winter 2019 Structs, Implementing Languages (credits: Dan Grossman, CSE 341)

SCHEME AND CALCULATOR 5b

Principles of Programming Languages Topic: Scope and Memory Professor Louis Steinberg Fall 2004

CSc 520 Principles of Programming Languages. Examining Lists. Constructing Lists... 7: Scheme List Processing

Syntactic Sugar: Using the Metacircular Evaluator to Implement the Language You Want

Scheme Quick Reference

SCHEME The Scheme Interpreter. 2 Primitives COMPUTER SCIENCE 61A. October 29th, 2012

LECTURE 16. Functional Programming

C311 Lab #3 Representation Independence: Representation Independent Interpreters

Lisp Basic Example Test Questions

CS61A Midterm 2 Review (v1.1)

FP Foundations, Scheme

Programming Languages

Lecture 15 CIS 341: COMPILERS

A Brief Introduction to Scheme (II)

Typical workflow. CSE341: Programming Languages. Lecture 17 Implementing Languages Including Closures. Reality more complicated

Lambda Calculus see notes on Lambda Calculus

;; definition of function, fun, that adds 7 to the input (define fun (lambda (x) (+ x 7)))

Spring 2018 Discussion 7: March 21, Introduction. 2 Primitives

Scheme: Data. CS F331 Programming Languages CSCE A331 Programming Language Concepts Lecture Slides Monday, April 3, Glenn G.

CSE 413 Midterm, May 6, 2011 Sample Solution Page 1 of 8

COP4020 Programming Languages. Functional Programming Prof. Robert van Engelen

A Small Interpreted Language

CS 314 Principles of Programming Languages. Lecture 16

Principles of Programming Languages Topic: Functional Programming Professor L. Thorne McCarty Spring 2003

SCHEME 8. 1 Introduction. 2 Primitives COMPUTER SCIENCE 61A. March 23, 2017

News. Programming Languages. Recap. Recap: Environments. Functions. of functions: Closures. CSE 130 : Fall Lecture 5: Functions and Datatypes

SCHEME 7. 1 Introduction. 2 Primitives COMPUTER SCIENCE 61A. October 29, 2015

An Explicit Continuation Evaluator for Scheme

Organization of Programming Languages CS3200/5200N. Lecture 11

Essentials of Programming Languages Language

Essentials of Programming Languages Language

Type Inference and Type Habitation (5/10/2004)

University of Massachusetts Lowell

Comp 311: Sample Midterm Examination

Sample Final Exam Questions

6.001: Structure and Interpretation of Computer Programs

UMBC CMSC 331 Final Exam

Functional Programming

Interpreters and Tail Calls Fall 2017 Discussion 8: November 1, 2017 Solutions. 1 Calculator. calc> (+ 2 2) 4

CS 314 Principles of Programming Languages

Scheme Tutorial. Introduction. The Structure of Scheme Programs. Syntax

CS61A Summer 2010 George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang

Functions and Recursion. Dr. Philip Cannata 1

CS61A Discussion Notes: Week 11: The Metacircular Evaluator By Greg Krimer, with slight modifications by Phoebus Chen (using notes from Todd Segal)

CSc 520 Principles of Programming Languages

Parsing Scheme (+ (* 2 3) 1) * 1

Symbolic Computation and Common Lisp

ALISP interpreter in Awk

Transcription:

Project 2: Scheme Interpreter CSC 4101, Fall 2017 Due: 12 November 2017 For this project, you will implement a simple Scheme interpreter in C++ or Java. Your interpreter should be able to handle the same subset of the language as the parser for Project 1, as well as some predefined functions. In particular, your interpreter should implement symbols, 32 bit integers, booleans, strings, lists, and closures; the test symbol? for identifying an identifier; the test number? for identifying integers and the binary arithmetic operations b+, b-, b*, b/, b=, b<; the list operations car, cdr, cons, set-car!, set-cdr!, null?, pair?, eq?; the test procedure? for identifying a closure; the I/O functions read, write, display, newline (without the optional port argument); the functions eval, apply, and interaction-environment. Build this interpreter on top of the code you wrote for Project 1. Use the parse trees from Project 1 as the internal data structure for lists. The arithmetic and list operations would then be simply implemented by the appropriate C++ or Java operations on parse trees. The following built-in Scheme functions call parts of your interpreter: read calls the parser and returns a parse tree, write calls the pretty-printer, eval calls your C++ or Java eval() function, apply calls your C++ or Java apply() function, interaction-environment returns a pointer to your interpreter s global environment. All other built-in Scheme functions, such as and, or, not, >, >=, <=, list, cadr, caddr, etc., equal?, map, assq, reverse, etc., can then be implemented as Scheme definitions using only the primitive operations implemented above. Since we have a general mechanism for defining functions with a variable number of arguments, the n-ary versions of the binary arithmetic operators can also be implemented in Scheme. We ll do that in Project 3. 1

Environments For keeping track of the values of variables during evaluation, we need a data structure for storing these values. That s the environment. Scheme is a language with nested scopes. Given the function definition: (define (z x) (+ x y)) (define y 42) (define x 17) When evaluating the body of the function call (z y), we need to look for the values of +, x, and y. For each variable, we first look in the local function scope, then in the outer file scope, then in the scope containing the built-in function definitions. We will find x in the function scope, y in file scope, and + in built-in scope (+ is nothing special, it s a regular variable which has a function as its value). The environment data structure needs to be designed to allow this search. The environment consists of individual frames, tables that contain the values for a single scope. We have one scope for built-in functions, the top-level (file) scope, and one scope for each function call and for each evaluation of a let-expression. Every frame is linked to the frame representing the syntactically enclosing scope. When looking up the value of a variable, we traverse these links to frames for enclosing scopes until we find the variable or until we reach the outermost scope. If a variable name refers to a function, its value is represented as a closure. Unlike function pointers in C, which only point to the code, a closure consists of two pointers: a pointer to the code (a lambda expression) and a pointer to the environment in which the lambda expression was created. As data structure for environments, you could either use a list of association lists or a list of hash tables. The implementation of environments is described below in the form of association lists in Scheme. This is also the implementation used in the Scheme interpreter written in Scheme. I suggest, though, that you use classes to define the environment data structure. An association list is a list of pairs with the first element (the car) of each pair being the key and the second element (the cadr) being the value. The Scheme function assq can be used to look up an entry in an association list. The function assq takes a key as first argument, an association list as second argument, and looks up the key in the association list with the comparison operation eq?. E.g., the call (assq y ((x 17) (z (closure (lambda (x) (+ x y)) nil)))) will result in the list. Each scope in the program can be represented by such an association list. A complete environment is a list of association lists, from the innermost scope to the outermost scope. E.g., evaluating the Scheme definitions (define (z x) (+ x y)) (define y 42) (define x 17) will result in the environment 2

(((x 17) (z (closure (lambda (x) (+ x y))...)))) where... is the environment in which z is defined, i.e., a pointer to the entire environment. Since this results in a circular data structure, we need to use the assignment function set-car! for building it. The circular data structure can be constructed in Scheme using set-car! as follows: ; Define the empty environment: a list containing an empty scope (define env (list ())) ; Add a binding for z to the innermost scope (list z (list closure (lambda (x) (+ x y)) env)) ; Add a binding for y to the innermost scope (list y 42) ; Add a binding for x to the innermost scope (list x 17) These set-car! operations are performed when evaluating the define special forms above. The value associated with a variable can be updated using set-cdr!. For example, the redefinition (define x 15) or the assignment (set! x 15) would have the effect (set-cdr! (assq x (car env)) (list 15)) When calling a function, we create a new (empty) association list for representing the function scope and cons it in front of the environment in which the function was defined (which is taken out of the closure for the function). Then we add variable definitions for the parameters to the new association list. Given the function call (z y), the body of the function z will be evaluated in the environment (((x 42)) ((x 17)) (z (closure (lambda (x) (+ x y))...))) where the first association list, ((x 42)), contains the binding for the parameter x of function z to the value passed as argument. A frame (association list) is added to the environment when entering a function body or when entering a let body. For each define that is encountered inside a function body or inside a let body, one binding is added to the front of the first frame in the environment. The Scheme interpreter written in Scheme is started with the empty global-environment. However, conceptually, the environment also contains everything that was defined in the underlying Scheme 3

system. This way we can rely on the existing implementations for the data structures and primitive functions. When implementing the interpreter in C++ or Java we first need to construct a frame for built-in functions, then add all the built-in function definitions, then we construct the frame for user definitions and start processing the input. A possible implementation in C++ or Java would be to define a class for implementing frames. The sample skeleton implementation is patterned after the Scheme implementation. For a better implementation, you could use a hash table that associates a value with a symbol name. Evaluation For evaluating Scheme programs, you will implement the two mutually recursive functions eval and apply. The main evaluation function, eval, takes two arguments, a Scheme expression and an environment. E.g., the call (eval (z y) env) where env is defined as above, will result in the value 84. The function eval needs to perform the following tasks: extend the environment for variable or function definitions, extend the environment for let expressions, update the environment for set! assignments, look up variables in the environment, handle the special forms quote, lambda, begin, if, and cond (including the else keyword in cond), and recursively call apply for function calls. Note that some of the special forms as well as some of the built-in functions do not return a value. A user-defined function is applied to its arguments using the function apply, which takes two arguments: a function and a list of arguments to the function. The function argument must be a closure. A closure is constructed by eval for a function definition or a lambda expression. In the Scheme interpreter written in Scheme, it is represented as a list consisting of the symbol closure, a lambda expression, and the environment in which the lambda expression was defined. BTW, since environments are circular structures, printing an environment or an entire closure will loop endlessly. In your interpreter written in C++ or Java a closure would be represented as an object. Before calling the function apply, its arguments are evaluated. The first argument must evaluate to a closure. The function apply then needs to perform the following tasks: extract the environment out of the closure; add a new frame to the environment that binds the parameters to the corresponding argument values; recursively call eval for the function body and the new environment. 4

The functions eval and apply as described above, should work correctly for most Scheme programs, including programs containing functions as parameters or functions as return values. What does not work are programs containing special forms (like define, lambda, if, etc.) other than the ones listed above. For distinguishing between built-in functions and functions that were defined in Scheme, we need two different representations for closures. The Scheme interpreter written in Scheme uses lists of the form (closure fun env) for representing user-defined functions. For built-in functions, it uses the closure representation of the underlying Scheme48 implementation. For your interpreter in C++ or Java you need two parse tree node classes, Closure and BuiltIn, for representing these different types of closures. Top-Level Loop Your Scheme interpreter eval should be run with a top-level loop such as the following: (define (Scheme) (display "Scheme 4101> ") (let ((input (read))) (if (not (eof-object? input)) (begin (write (eval input global-environment)) (newline) (Scheme)) (newline)))) This top-level loop could either be implemented directly in C++ or Java or read from a file of Scheme definitions. For this project, it will be easiest to implement this loop directly in your main function. Administrative Stuff You can compare the output of your interpreter with the behavior of scheme48. Extend the Makefile appropriately as needed and submit the program using cs4101_bau/bin/p_copy 2 Make sure you submit a README.txt file. 5