summary refs log tree commit diff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2017-07-05 23:28:58 +0200
committerLudovic Courtès <ludo@gnu.org>2017-07-05 23:42:03 +0200
commitc18c53117fa527ea34f8386ad344bb0df0113f67 (patch)
treefc078b9a9866f4002b6b2b85df58ca24f54d57eb
parent425ab478ac99452dff6a71b16caa46ae06d5b550 (diff)
downloadguix-wip-gexp-hygiene.tar.gz
DRAFT gexp: Preserve scope across stages. wip-gexp-hygiene
DRAFT: Needs more tests and more testing.

* guix/gexp.scm (gexp)[lookup-binding, generate-bindings]
[syntax-uid, alpha-rename]: New procedures.
Call 'alpha-rename' before doing anything else.
* tests/gexp.scm ("hygiene, eval", "hygiene, define")
("hygiene, shadowed syntax", "hygiene, quote"): New tests.
-rw-r--r--guix/gexp.scm181
-rw-r--r--tests/gexp.scm58
2 files changed, 237 insertions, 2 deletions
diff --git a/guix/gexp.scm b/guix/gexp.scm
index d9c4cb461e..79b1c5a35f 100644
--- a/guix/gexp.scm
+++ b/guix/gexp.scm
@@ -905,11 +905,188 @@ environment."
                  #,(substitute-references #'exp substs)))
         (x #''x)))
 
+    (define (lookup-binding id env)
+      ;; Lookup ID in ENV.  Return its corresponding generated identifier or
+      ;; #f.
+      (any (match-lambda
+             ((x renamed)
+              (and (bound-identifier=? x id)
+                   renamed)))
+           env))
+
+    (define (generate-bindings lst seed env)
+      ;; Like 'generate-temporaries', but use SEED and ENV as extra data to
+      ;; generate unique identifiers in a deterministic way.
+      (let ((len (length env)))
+        (map (lambda (binding)
+               (datum->syntax
+                binding
+                (string->symbol (format #f "~a-~a-~a"
+                                        (syntax->datum binding)
+                                        (number->string seed 16)
+                                        len))))
+             lst)))
+
+    (define (syntax-uid s)
+      ;; Return a unique numeric identifier for S.
+      (hash s 2147483648))
+
+    (define* (alpha-rename stx env stage
+                           #:optional (quoting 0)
+                           (uid (syntax-uid s)))
+      ;; Perform alpha-renaming of all the identifiers introduced in S, using
+      ;; ENV as the lexical environment.  The goal is to preserve scope across
+      ;; stages, as illustrated by Kiselyov et al. in MetaScheme.  Use UID as
+      ;; a stem when generating unique identifiers.
+      (syntax-case stx (gexp ungexp ungexp-native
+                             ungexp-splicing ungexp-native-splicing
+                             quote quasiquote unquote
+                             lambda let let* letrec define begin)
+        ((proc arg ...)
+         (or (not (identifier? #'proc))
+             (lookup-binding #'proc env))
+         #`(#,(alpha-rename #'proc env stage quoting)
+            #,@(map (lambda (arg)
+                      (alpha-rename arg env stage quoting))
+                    #'(arg ...))))
+        ((quote exp)
+         #'(quote exp))
+        ((quasiquote exp)
+         #`(quasiquote #,(alpha-rename #'exp env stage
+                                       (+ quoting 1))))
+        ((unquote exp)
+         #`(unquote #,(alpha-rename #'exp env stage (- quoting 1))))
+        ;; TODO: 'syntax', 'unsyntax', etc.
+        ((gexp exp rest ...)
+         #`(gexp #,(alpha-rename #'exp env (+ stage 1) quoting)
+                 rest ...))
+        ((ungexp exp rest ...)
+         #`(ungexp #,(alpha-rename #'exp env (- stage 1) quoting)
+                   rest ...))
+        ((ungexp-native exp rest ...)
+         #`(ungexp-native #,(alpha-rename #'exp env (- stage 1) quoting)
+                          rest ...))
+        ((ungexp-splicing exp)
+         #`(ungexp-splicing
+            #,(alpha-rename #'exp env (- stage 1) quoting)))
+        ((ungexp-native-splicing exp)
+         #`(ungexp-native-splicing
+            #,(alpha-rename #'exp env (- stage 1) quoting)))
+        ((lambda (bindings ...) body ...)
+         (with-syntax (((formals ...)
+                        (generate-bindings #'(bindings ...)
+                                           uid env)))
+           #`(lambda (formals ...)
+               #,(alpha-rename #'(begin body ...)
+                               #`((bindings formals) ... #,@env)
+                               stage quoting))))
+        ;; TODO: lambda*, case-lambda
+        ((let ((bindings values) ...) body ...)
+         (with-syntax (((renamed ...)
+                        (generate-bindings #'(bindings ...)
+                                           (syntax-uid #'(values ...))
+                                           env)))
+           #`(let #,(map (lambda (renamed value)
+                           #`(#,renamed #,(alpha-rename value env
+                                                        stage quoting)))
+                         #'(renamed ...)
+                         #'(values ...))
+               #,(alpha-rename #'(begin body ...)
+                               #`((bindings renamed) ... #,@env)
+                               stage quoting))))
+        ;; TODO: named let
+        ((let* ((binding value) rest ...) body ...)
+         (alpha-rename #'(let ((binding value))
+                           (let* (rest ...)
+                             body ...))
+                       env stage quoting))
+        ((let* () body ...)
+         (alpha-rename #'(begin body ...) env stage quoting))
+        ((letrec ((bindings values) ...) body ...)
+         (with-syntax (((renamed ...)
+                        (generate-bindings #'(bindings ...)
+                                           (syntax-uid #'(values ...))
+                                           env)))
+           (let ((env #`((bindings renamed) ... #,@env)))
+             #`(letrec #,(map (lambda (renamed value)
+                                #`(#,renamed #,(alpha-rename value env
+                                                             stage quoting)))
+                              #'(renamed ...)
+                              #'(values ...))
+                 #,(alpha-rename #'(begin body ...) env stage quoting)))))
+        ;; TODO: letrec*
+        ;; TODO: let-syntax, letrec-syntax
+        ((begin exp)
+         (alpha-rename #'exp env stage quoting))
+        ((define (proc formals ...) body ...)     ;top-level
+         (with-syntax (((renamed ...)
+                        (generate-bindings #'(formals ...) uid env)))
+           #`(define (proc renamed ...)
+               #,(alpha-rename #'(begin body ...)
+                               #`((formals renamed) ... #,@env)
+                               stage quoting))))
+        ((define id value)                        ;top-level
+         #`(define id
+             #,(alpha-rename #'value env stage quoting)))
+        ((begin exp ...)
+         (null? env)                              ;top-level
+         #`(begin #,@(map (lambda (exp)
+                            (alpha-rename exp env stage quoting))
+                          #'(exp ...))))
+        ((begin exp ...)                          ;inner 'begin'
+         (with-syntax (((bindings ...)
+                        (filter-map (lambda (exp)
+                                      (syntax-case exp (define)
+                                        ((define (proc _ ...) value)
+                                         #'proc)
+                                        ((define binding value)
+                                         #'binding)
+                                        (_
+                                         #f)))
+                                    #'(exp ...))))
+           (with-syntax (((renamed ...)
+                          (generate-bindings #'(bindings ...)
+                                             uid env)))
+             (let ((env #`((bindings renamed) ... #,@env)))
+               #`(begin
+                   #,@(map (lambda (exp)
+                             (syntax-case exp (define)
+                               ((define (id formals ...) body ...)
+                                (with-syntax ((id (lookup-binding #'id env))
+                                              ((renamed ...)
+                                               (generate-bindings #'(formals ...)
+                                                                  uid env)))
+                                  #`(define (id renamed ...)
+                                      #,(alpha-rename #'(begin body ...)
+                                                      #`((formals renamed) ...
+                                                         #,@env)
+                                                      stage quoting))))
+                               ((define id value)
+                                #`(define #,(lookup-binding #'id env)
+                                    #,(alpha-rename #'value env
+                                                    stage quoting)))
+                               (_
+                                (alpha-rename exp env stage quoting))))
+                           #'(exp ...)))))))
+        ((proc arg ...)
+         #`(#,(alpha-rename #'proc env stage quoting)
+            #,@(map (lambda (arg)
+                      (alpha-rename arg env stage quoting))
+                    #'(arg ...))))
+        (id
+         (identifier? #'id)
+         (if (or (> quoting 0) (< stage 0))
+             #'id
+             (or (lookup-binding #'id env) #'id)))
+        (obj
+         #'obj)))
+
     (syntax-case s (ungexp output)
       ((_ exp)
-       (let* ((escapes (delete-duplicates (collect-escapes #'exp)))
+       (let* ((exp     (alpha-rename #'exp #'() 0))
+              (escapes (delete-duplicates (collect-escapes exp)))
               (formals (generate-temporaries escapes))
-              (sexp    (substitute-references #'exp (zip escapes formals)))
+              (sexp    (substitute-references exp (zip escapes formals)))
               (refs    (map escape->ref escapes)))
          #`(make-gexp (list #,@refs)
                       current-imported-modules
diff --git a/tests/gexp.scm b/tests/gexp.scm
index cf88a9db80..6bdc233170 100644
--- a/tests/gexp.scm
+++ b/tests/gexp.scm
@@ -984,6 +984,64 @@
   '#~(foo #$bar #$baz:out #$(chbouib 42) #$@(list x y z)
           #+foo #+foo:out #+(chbouib 42) #+@(list x y z)))
 
+(test-equal "hygiene, eval"
+  42
+  ;; Test: (1) that 'x' in one gexp does not shadow 'x' from the other 'gexp',
+  ;; and (2) that 'x' in 'ungexp' is not mistakenly renamed.
+  (let* ((inner (lambda (x)
+                  #~(let ((x 40)) (+ x #$x))))
+         (outer  #~(let ((x 2))
+                     #$(inner #~x))))
+    (primitive-eval (gexp->sexp* outer))))
+
+(test-assert "hygiene, define"
+  (match (gexp->sexp* #~(begin
+                          ;; Top-level defines aren't renamed.
+                          (define top0 0)
+                          (define (top1 x) x)
+                          (define (top2 x y)
+                            ;; Internal define is renamed.
+                            (define inner1 (* x x))
+                            (define (inner2 x) (+ x y))
+                            (+ inner y))))
+    (('begin
+       ('define 'top0 0)
+       ('define ('top1 x0) x0)
+       ('define ('top2 x1 y1)
+         ('begin
+           ('define inner1 ('* x1 x1))
+           ('define (inner2 x2) ('+ x2 y1))
+           ('+ inner y1))))
+     (and (not (eq? x0 'x))
+          (not (eq? x1 'x))
+          (not (eq? y1 'y))
+          (not (eq? inner1 'inner1))
+          (not (eq? inner2 'inner2))
+          (not (eq? x2 x1))))))
+
+(test-assert "hygiene, shadowed syntax"
+  (match (gexp->sexp* #~(lambda (lambda x)
+                          (lambda (x) x)))
+    (('lambda (arg x)
+       (arg (x) x))
+     (and (not (eq? arg 'lambda))
+          (not (eq? x 'x))))))
+
+(test-assert "hygiene, quote"
+  (match (gexp->sexp* #~(lambda (x y z)
+                          (list '(x y z)
+                                `(x ,x (,y ,z) z))))
+    (('lambda (x0 y0 z0)
+       ('list ('quote ('x 'y 'z))
+              ('quasiquote
+               ('x ('unquote x0)
+                   (('unquote y0)
+                    ('unquote z0))
+                   'z))))
+     (and (not (eq? x0 'x))
+          (not (eq? y0 'y))
+          (not (eq? z0 'z))))))
+
 (test-end "gexp")
 
 ;; Local Variables: