cs173: Assignment 7
Version 2, 2002-12-04 14:00
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.
Since this task relies heavily on support libraries, 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. We also expect you to email the course staff (email@example.com) with questions, and we will frequently update the FAQ.
Your language should include the following expressions; we will award extra credit for additional interesting primitives.
This expression produces a stream containing the names of all files in the current directory that match the regular expression re.
(lines re filename)
This expression produces a stream containing all lines in the file filename that match the regular expression re.
(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. For example, the following expressions are legal:
(run /usr/bin/yes) (run /bin/ls -l -a) (run "/bin/ps" u) (run finger ,(string-append "db" "tucker"))
(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. In lectures, we saw how to write unhygienic macros using define-macro and how to write hygienic macros using syntax-rules. In fact, we can get the best of both worlds both pattern matching and the ability to break hygiene by using a variation of syntax-rules named syntax-case. We give examples of syntax-case below.
(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))))
Here's the time% macro using syntax-case. It is almost identical to the syntax-rules version; the only two differences are the parameter stx and the keyword syntax in the template.
(define-syntax (time% stx) (syntax-case stx () [(_ expr) (syntax (let ([start (current-milliseconds)]) (let ([result expr]) (let ([end (current-milliseconds)]) (begin (printf "time: ~a~n" (- end start)) result)))))]))
As we mentioned earlier, the benefit of syntax case is that it allows us to break hygiene. The expression (if-it test then else) behaves just like if, except that the variable it is bound to the result of test in both then and else. Here is the macro definition and an example use:
(define-syntax (if-it stx) (syntax-case stx () [(src-if-it test then else) (with-syntax ([it (datum->syntax-object (syntax src-if-it) 'it)]) (syntax (let ([it test]) (if it then else))))])) (if-it (memq 'b '(a b c)) it 'nope) => '(b c)