;;> The Slug extension to Sloth ;;; ================================================================== ;;; Syntax #| The BNF: ::= | | { bind {{ } ... } } | { with-stx { { ... } { } ...} } | { fun { ... } } | { if } | { ... } |# ;; A matching abstract syntax tree datatype: ;; (note: no WithStx constructor -- it is preprocessed away) (define-type SLUG [Num (n number?)] [Str (s string?)] [Id (name symbol?)] [Bind (names unique-names?) (exprs (list-of SLUG?)) (body SLUG?)] [Fun (names unique-names?) (body SLUG?)] [Call (fun-expr SLUG?) (arg-exprs (list-of SLUG?))] [If (cond-expr SLUG?) (then-expr SLUG?) (else-expr SLUG?)]) ;; unique-list? : List -> Boolean ;; Tests whether a list is unique, used to make `unique-names?' below. (define (unique-list? xs) (or (null? xs) (and (not (member (first xs) (rest xs))) (unique-list? (rest xs))))) ;; unique-names? : Any -> Boolean ;; A predicate that is used to specify a type of unique symbol lists. (define unique-names? (intersection-of (list-of symbol?) unique-list?)) ;; This built-in is used in the following code: ;; make-transformer : (Listof Symbol) (Listof (List Sexpr Sexpr)) ;; -> (Sexpr -> Sexpr) ;; consumes a list of pattern pairs, and creates a transformer ;; procedure (transforms an s-expression into an s-expression) ;; parse-sexpr : Sexpr (Listof (List Symbol (Syntax -> Syntax))) ;; -> SLUG ;; parses *and* macro-expands an s-expression; the second argument is ;; the association list of transformers at this point. (define (parse-sexpr sexpr transformers) ;; convenient function for common cases where we recurse with the ;; same transformers (define (parse* sexpr) (parse-sexpr sexpr transformers)) (let ([transformer (and (pair? sexpr) (assq (car sexpr) transformers))]) (if transformer ;; if there is a transformer by this name, apply it and ;; continue with the result (parse* ((second transformer) sexpr)) (match sexpr ;; if we see `with-stx', then recursively parse with an ;; additional transformer [(cons 'with-stx more) (match sexpr [(list 'with-stx (list (symbol: name) (list (symbol: keywords) ...) more ...) body) (parse-sexpr body (cons (list name (make-transformer keywords more)) transformers))] [else (error 'parse-sexpr "bad `with-stx' syntax in ~s" sexpr)])] [(number: n) (Num n)] [(symbol: name) (Id name)] [(string: s) (Str s)] [(cons 'bind more) (match sexpr [(list 'bind (list (list (symbol: names) nameds) ...) body) (Bind names (map parse* nameds) (parse* body))] [else (error 'parse-sexpr "bad `bind' syntax in ~s" sexpr)])] [(cons 'fun more) (match sexpr [(list 'fun (list (symbol: names) ...) body) (Fun names (parse* body))] [else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])] [(cons 'if more) (match sexpr [(list 'if cond then else) (If (parse* cond) (parse* then) (parse* else))] [else (error 'parse-sexpr "bad `if' syntax in ~s" sexpr)])] [(list fun args ...) ; other lists are applications (Call (parse* fun) (map parse* args))] [else (error 'parse-sexpr "bad syntax in ~s" sexpr)])))) ;; parse : String -> Slug ;; Parses a string containing an SLUG expression to a SLUG AST. (define (parse str) (parse-sexpr (string->sexpr str) null)) ;;; ================================================================== ;;; Values and environments (define-type ENV [EmptyEnv] [FrameEnv (frame frame?) (rest ENV?)]) (define-type VAL [ScmV (x any?)] [IOV (x IO?)] [FunV (names unique-names?) (body SLUG?) (env ENV?)] [ExprV (expr SLUG?) (env ENV?) (cache (box-of (union-of false? VAL?)))] [PrimV (prim procedure?)]) ;; I/O descriptions (define-type IO [Print (string VAL?)] [ReadLine (receiver VAL?)] [Begin2 (l VAL?) (r VAL?)]) ;; a frame is an association list of names and values. (define frame? (list-of (lambda (x) (and (list? x) (= 2 (length x)) (symbol? (first x)) (VAL? (second x)))))) ;; extend : (Listof Symbol) (Listof VAL) ENV -> ENV ;; extends an environment with a new frame. (define (extend names values env) (if (= (length names) (length values)) (FrameEnv (map list names values) env) (error 'extend "arity mismatch for names: ~s" names))) ;; lookup : Symbol ENV -> VAL ;; looks a name in an environment, searching through each frame. (define (lookup name env) (cases env [(EmptyEnv) (error 'lookup "no binding for ~s" name)] [(FrameEnv frame rest) (let ([cell (assq name frame)]) (if cell (second cell) (lookup name rest)))])) ;; scheme-func->prim : (Any ... -> Any) (Any -> VAL) Boolean -> VAL ;; Converts a scheme function to a primitive evaluator function which ;; is a PrimV holding a ((Listof VAL) -> VAL) procedure. The ;; `wrapper' argument is used to convert the result to a VAL. (The ;; result procedure doesn't check for types and arity.) (define (scheme-func->prim scheme-func wrapper strict?) (PrimV (lambda (args) (let* ([args (if strict? (map (lambda (a) (let ([v (strict a)]) (cases v [(ScmV x) x] [else (error 'scheme-func "bad input: ~s" a)]))) args) args)] [result (apply scheme-func args)]) ;; Because there are non-strict constructors, primitives ;; like `car' might be returning promises which are ;; already VAL objects. (if (VAL? result) result (wrapper result)))))) ;; scheme-func->prim-val : (Any ... -> Any) Boolean -> VAL ;; Uses `scheme-func->prim' to return a ScmV value (define (scheme-func->prim-val scheme-func strict?) (scheme-func->prim scheme-func ScmV strict?)) ;; scheme-func->prim-io : (Any ... -> Any) Boolean -> VAL ;; Uses `scheme-func->prim' to return an IOV value (define (scheme-func->prim-io scheme-func strict?) (scheme-func->prim scheme-func IOV strict?)) ;;; ================================================================== ;;; The global environment (define global-environment (FrameEnv (list (list '+ (scheme-func->prim-val + #t)) (list '- (scheme-func->prim-val - #t)) (list '* (scheme-func->prim-val * #t)) (list '/ (scheme-func->prim-val / #t)) (list '< (scheme-func->prim-val < #t)) (list '> (scheme-func->prim-val > #t)) (list '= (scheme-func->prim-val = #t)) (list 'number->string (scheme-func->prim-val number->string #t)) ;; Note flags: (list 'cons (scheme-func->prim-val cons* #f)) (list 'list (scheme-func->prim-val list #f)) (list 'car (scheme-func->prim-val car #t)) (list 'cdr (scheme-func->prim-val cdr #t)) (list 'null? (scheme-func->prim-val null? #f)) ;; IO constructors -- all are non-strict (list 'print (scheme-func->prim-io Print #f)) (list 'read (scheme-func->prim-io ReadLine #f)) (list 'begin2 (scheme-func->prim-io Begin2 #f)) ;; Values (list 'true (ScmV #t)) (list 'false (ScmV #f)) (list 'null (ScmV null))) (EmptyEnv))) ;;; ================================================================== ;;; Evaluation ;; strict : VAL -> VAL which is not an ExprV ;; forces a (possibly nested) ExprV promise (define (strict v) (cases v [(ExprV expr env cache) (or (unbox cache) (let ([val (strict (eval expr env))]) (set-box! cache val) val))] [else v])) ;; eval-promise : SLUG env -> VAL (the ExprV variant) ;; used instead of `eval' to create an evaluation promise (define (eval-promise expr env) (ExprV expr env (box #f))) ;; eval : SLUG env -> VAL ;; evaluates SLUG expressions. (define (eval expr env) (cases expr [(Num n) (ScmV n)] [(Str s) (ScmV s)] [(Id name) (lookup name env)] [(Bind names exprs bound-body) (eval bound-body (extend names (map (lambda (e) (eval-promise e env)) exprs) env))] [(Fun names bound-body) (FunV names bound-body env)] [(Call fun-expr arg-exprs) (let ([fval (strict (eval fun-expr env))] [arg-vals (map (lambda (e) (eval-promise e env)) arg-exprs)]) (cases fval [(PrimV proc) (proc arg-vals)] [(FunV names body fun-env) (eval body (extend names arg-vals fun-env))] [else (error 'eval "function call with a non-function: ~s" fval)]))] [(If cond-expr then-expr else-expr) (eval (if (cases (strict (eval cond-expr env)) [(ScmV v) v] ; Scheme value => use as boolean [else #t]) ; other values are always true then-expr else-expr) env)])) ;;; ================================================================== ;;; I/O execution ;; force-io : IO -> IO ;; forces VAL objects in an IO value. (define (strict-IO v) (cases v [(Print x) (Print (strict x))] [(ReadLine x) (ReadLine (strict x))] [(Begin2 x y) (Begin2 (strict x) (strict y))])) ;; perform-i/o : IO -> void ;; Executes the described I/O operation (define (perform-i/o v) (let ([forced (strict-IO v)]) (cases forced [(Print (ScmV str)) (if (string? str) ??? (error 'perform-i/o "cannot print a non-string value: ~s" str))] [(ReadLine (FunV names body env)) (if (= 1 (length names)) ??? (error 'perform-i/o "expecting a unary function"))] [(Begin2 (IOV io1) (IOV io2)) ???] [else (error 'perform-i/o "bad input: ~s" forced)]))) ;; execute : VAL -> void ;; executes an IOV in a VAL (define (execute val) (let ([val (strict val)]) (cases val [(IOV v) (perform-i/o v)] [else (error 'execute "expecting an IO value: ~s" val)]))) ;;; ================================================================== ;;; Main entry points ;; run : String -> (U Number String) ;; evaluate a SLUG program contained in a string (define (run str) (let ([result (strict (eval (parse str) global-environment))]) (cases result [(ScmV v) v] [else (error 'run "evaluation returned a bad value: ~s" result)]))) ;; run-io : String -> Void ;; evaluate a SLUG program contained in a string, and execute the ;; resulting IOV description (define (run-io str) (execute (eval (parse str) global-environment))) ;;; ================================================================== ;;; Tests (test (run "{{fun {x} {+ x 1}} 4}") => 5) (test (run "{bind {{add3 {fun {x} {+ x 3}}}} {add3 1}}") => 4) (test (run "{bind {{add3 {fun {x} {+ x 3}}} {add1 {fun {x} {+ x 1}}}} {bind {{x 3}} {add1 {add3 x}}}}") => 7) (test (run "{bind {{identity {fun {x} x}} {foo {fun {x} {+ x 1}}}} {{identity foo} 123}}") => 124) (test (run "{bind {{x 3}} {bind {{f {fun {y} {+ x y}}}} {bind {{x 5}} {f 4}}}}") => 7) (test (run "{{{fun {x} {x 1}} {fun {x} {fun {y} {+ x y}}}} 123}") => 124) ;; test laziness (test (run "{bind {{x {/ 1 0}}} {car {cons 1 null}}}") => 1) ;; test two macros (test (run "{with-stx {let {} {{let {{var val} ...} body} {{fun {var ...} body} val ...}}} {with-stx {let* {} {{let* {} body} body} {{let* {{id1 expr1} {id expr} ...} body} {let {{id1 expr1}} {let* {{id expr} ...} body}}}} {let* {{x 1} {y {+ x 1}}} {+ x y}}}}") => 3) ;; test simple IO (run-io "{begin2 {print 'What is your name?\n'} {read {fun {name} {begin2 {print 'Your name is '''} {begin2 {print name} {print '''\n'}}}}}}") ;;; ================================================================== ;; IO with convenient macros -- solution to the last question (run-io "{with-stx {do {<-} {{do {id <- {read}} next more ...} ???} {{do {print str} next more ...} ???} {{do expr} ???}} {do {print 'What is your name?\n'} {name <- {read}} {print 'What is your email?\n'} {email <- {read}} {print 'Your address is '''} {print name} {print ' <'} {print email} {print '>''\n'}}}") ;;; ================================================================== ;; Test mutation (run-io "{bind {{incbox {fun {b} {unbox b {fun {curval} {set-box! b {+ 1 curval}}}}}}} {newbox 0 {fun {i} {begin2 {incbox i} {begin2 {print 'i now holds: '} {unbox i {fun {v} {begin2 {print {number->string v}} {print '\n'}}}}}}}}}") ;; Same as above, but with conveniet syntax (run-io "{with-stx {do {<-} ???} {bind {{incbox {fun {b} {do {curval <- {unbox b}} {set-box! b {+ 1 curval}}}}}} {do {i <- {newbox 0}} {incbox i} {print 'i now holds: '} {v <- {unbox i}} {print {number->string v}} {print '\n'}}}}") ;;; ==================================================================