CS 536 Homework 1: Implementing a Scripting Language via Macros

Due: September 13, hardcopy in class and electronic via turnin (asgmt name hwk1)


For this assignment, you will implement a small scripting language using Scheme's macro system. Like most Unix shells, we will use streams to represent the output of system processes; below, we provide Scheme primitives for streams. Do this assignment in the Pretty Big language level in DrScheme.

Since this task relies heavily on support libraries (for systems calls, I/O, etc), you will need to learn some new features of PLT Scheme as you go. A good resource is the Help Desk, which contains extensive documentation on all of the libraries.

Your language should include at least the following expressions:

Directory listing

    (files re) 

This expression produces a stream containing the names of all files in the current directory that match the regular expression re. The re argument should be a string, not a Scheme reg-exp object.

File contents

    (lines re filename) 

This expression produces a stream containing all lines in the file filename that match the regular expression re. The filename should be a string naming a file in the current directory.

Command execution

    (run cmd arg1 arg2 ... argn) 

This expression produces a stream containing all lines output by the program cmd with arguments arg1, arg2, ..., argn. The subexpressions (cmd, etc.) can be either symbols or strings, and should be implicitly quasiquoted (which allows them to contain arbitrary Scheme code). For example, the following expressions are legal:

  (run /usr/bin/yes)
  (run /bin/ls -l -a)
  (run "/bin/ps" u)
  (run finger ,(string-append "k" "fisler")) 

Stream iteration

  (for stream-expr with ([var init-expr] ...) do
     body-expr

     then return-expr) 

This expression iterates over the elements in stream-expr, evaluating body-expr each time, and returning return-expr when the stream is empty. The variables var ... are initially bound to init-expr ... and are updated each iteration by the special expression (loop next-expr ...). Also, the identifier it is bound in body-expr to the current stream element.

The for-expression is best illustrated with an example. The following expression prints all logins and the total number at the end:

   (for (run who) with ([n 0]) do
     (begin
       (printf "~a~n" it)
       (loop (+ n 1)))
     then (printf "total: ~a~n" n))
  

There are also two shorter forms of for. The following form is useful when only binding one variable:

  (for stream-expr with (var init-expr) do
     body-expr
     then return-expr)
 

The above example thus could be written as:

   (for (run who) with (n 0) do
     (begin
       (printf "~a~n" it)
       (loop (+ n 1)))
     then (printf "total: ~a~n" n))
 

This form binds no variables:


  (for stream-expr do
     body-expr
     then return-expr)
 

Since the body of the for-expression includes the implicitly bound identifiers loop and it, your macro must produce an expression where these identifier are not automatically renamed; in parlance, you must break hygiene. If you took CS1102 or CS2135, you learned how to write hygienic macros using syntax-rules. Breaking hygiene requires a more sophisticated macro construct called syntax-case. Chapter 36 of the course text discusses hygiene, syntax-rules and syntax-case.

Streams code


    (define-syntax stream-cons
      (lambda (stx)
	(syntax-case stx ()
	  [(_ f r) (syntax (cons f (delay r)))])))

    (define stream-empty
      empty)

    (define (stream-empty? s)
      (empty? s))

    (define (stream-first s)
      (first s))

    (define (stream-rest s)
      (force (rest s)))

    (define (stream-display s)
      (unless (stream-empty? s)
	(display (stream-first s))
	(newline)
	(stream-display (stream-rest s))))
 

FAQ

  1. What is the desired behavior for a for that does not use loop?

    If the evaluation of a for expression's body does not use loop, the value of the body should be returned (i.e. you should not continue iterating over stream elements, and you should not evaluate the then expression).

  2. Should the stream resulting from run only contain output from stdout, or stderr also?

    Just stdout.

  3. How do we invoke system processes from within Scheme?

    Look up the Scheme command subprocess, an example use of which appears below (see the documentation for a description of the inputs and outputs to this function); let-values supports functions with multiple outputs.

        (let-values ([(proc p-out p-in p-err) (subprocess #f #f #f "/usr/bin/finger" "kfisler")])
          ...)
       
  4. Do we have to worry about closing ports?

    No.

  5. What do we do if we are passed an empty stream in for?

    Just evaluate the then clause.

  6. For the run command, can we assume that we are given a full path to the command, or should we attempt to somehow look things up in $PATH?

    You can assume you are given a full path.


Back to the Assignments page