www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

test-define-x-expander-use-site-scope-simple.rkt (6172B)


      1 #lang racket
      2 
      3 ;; For a more realistic use-case justifying why this behaviour matters, see the
      4 ;; discussion at https://github.com/jackfirth/generic-syntax-expanders/pull/8
      5 ;; and in particular the test file at
      6 ;;   https://github.com/jsmaniac/generic-syntax-expanders
      7 ;;     /blob/ec43791028715221c678f8536389a39ee760ed98
      8 ;;     /test/test-define-x-expander-use-site-scope.rkt
      9 
     10 (require generic-syntax-expanders
     11          rackunit)
     12 
     13 (define-expander-type foo)
     14 
     15 (define-syntax (expand-foos stx)
     16   (syntax-case stx ()
     17     [(_ body)
     18      (expand-all-foo-expanders #'body)]))
     19 
     20 ;; Without PR #8, the `x` in `bb` is not `bound-identifier=?` to the `x`
     21 ;; defined by `aa`, despite the use of `syntax-local-introduce` to make the
     22 ;; macros unhygienic.
     23 ;;
     24 ;; This happens because `define-foo-expander` added an extra "use-site" scope
     25 ;; to the body of `aa` and a different "use-site" scope to the body of `bb`.
     26 (define-foo-expander aa
     27   (λ (_)
     28     (syntax-local-introduce #'[x #t])))
     29 
     30 (define-foo-expander bb
     31   (λ (_)
     32     (syntax-local-introduce #'x)))
     33 
     34 ;; Due to the way `let` itself adds scopes to its definition and body, this
     35 ;; makes the identifiers `x` from `aa` and `x` from `bb` distinct, and the
     36 ;; latter cannot be used to refer to the former.
     37 ;;
     38 ;; Approximately, the code below expands to
     39 ;;
     40 ;;     (let [(x⁰¹ #t)]
     41 ;;       x⁰²)
     42 ;;
     43 ;; The `let` form then adds a "local" scope to both occurrences of `x`, and an
     44 ;; internal-definition context "intdef" scope to the `x` present in the body of
     45 ;; the `let` (but not to the one present in the bindings). The expanded form
     46 ;; therefore becomes:
     47 ;;
     48 ;;     (let [(x⁰¹³ #t)]
     49 ;;       x⁰²³⁴)
     50 ;;
     51 ;; where:
     52 ;;   ⁰ are the module's scopes
     53 ;;   ¹ is the undesired "use-site" scope added by `define-foo-expander` on `aa`
     54 ;;   ² is the undesired "use-site" scope added by `define-foo-expander` on `bb`
     55 ;;   ³ is the "local" scope added by `let`
     56 ;;   ⁴ is the "intdef" scope added by `let`
     57 ;;
     58 ;; Since {0,2,3,4} is not a subset of {0,1,3}, the `x` inside the `let` is
     59 ;; unbound.
     60 (test-true
     61  "Test that `x` as produced by `(bb)` is correctly bound by the `x`
     62  introduced by `(aa)`.
     63 
     64 This test fails without the PR #8 patch, because the body of `bb` and the body
     65  of `aa` each have a different use-site scope, introduced by accident by
     66  `define-foo-expander`. The occurrence of `x` introduced by `aa` and the
     67  occurrence of `x` introduced by `bb` therefore have different scopes, and the
     68  latter is not bound by the former.
     69 
     70 Without the PR #8 patch, this test case will not compile, and will fail with
     71  the error `x: unbound identifier in module`."
     72  (expand-foos
     73   (let ((aa))
     74     (bb))))
     75 
     76 ;; ----------
     77 
     78 ;; It is worth noting that `define` seems to strip the "use-site" scopes present
     79 ;; on the defined identifier. If the code above is changed so that a `define`
     80 ;; form is used, the problem does not occur:
     81 (define-foo-expander aa-def
     82   (λ (_)
     83     (syntax-local-introduce #'[define y #t])))
     84 
     85 (define-foo-expander bb-def
     86   (λ (_)
     87     (syntax-local-introduce #'y)))
     88 
     89 ;; This is because the code below expands to:
     90 ;;
     91 ;;     (begin (define y⁰ #t)
     92 ;;            (define y-copy y⁰²⁵)
     93 ;;
     94 ;; where:
     95 ;;   ⁰ are the module's scopes
     96 ;;   ¹ is the undesired "use-site" scope added by `define-foo-expander` on `aa`
     97 ;;     and it is stripped by `define` from the first `y`
     98 ;;   ² is the undesired "use-site" scope added by `define-foo-expander` on `bb`
     99 ;;   ⁵ is the "use-site" scope added because it is in an expression position
    100 ;;
    101 ;; Since {0,2,5} is a subset of {0}, the second `y` refers to the first `y`.
    102 (expand-foos
    103  (begin (aa-def)
    104         (define y-copy (bb-def))))
    105 (test-true
    106  "Test that `y` as produced by `(bb-def)` is correctly bound by the `y`
    107  defined by `(aa-def)`.
    108 
    109 This test succeeds without the PR #8 patch, which shows that `define` removes
    110  all use-site scopes on the defined identifier (or at least it removes all the
    111  use-site scopes present in this example). This can be checked in the macro
    112  debugger, and explains why the test case did not fail with a simple `define`,
    113  but does fail with a binding introduced by a `let`."
    114  y-copy)
    115 
    116 ;; ----------
    117 
    118 ;; The code below attempts to remove the extra "use-site" scope with
    119 ;; `syntax-local-identifier-as-binding`. However, that function does
    120 ;; not remove all use-site scopes, unlike the `define` above.
    121 
    122 (define-foo-expander aa-as-binding
    123   (λ (_)
    124     #`[#,(syntax-local-identifier-as-binding (syntax-local-introduce #'z)) #t]))
    125 
    126 (define-foo-expander bb-as-binding
    127   (λ (_)
    128     (syntax-local-introduce #'z)))
    129 
    130 (test-true
    131  "Test that `z` as produced by `(bb-as-binding)` is correctly bound by
    132  the `z` defined by `(aa-as-binding)`.
    133 
    134 This test fails without the PR #8 patch, which shows that that unlike `define`,
    135  the `syntax-local-identifier-as-binding` function does not remove all use-site
    136  scopes.
    137 
    138 Without the PR #8 patch, this test case will not compile, and will fail with
    139  the error `z: unbound identifier in module`."
    140  (expand-foos
    141   (let ((aa-as-binding))
    142     (bb-as-binding))))
    143 
    144 ;; ----------
    145 
    146 ;; The `cc` expander acts either as aa or as bb depending on the keyword passed
    147 ;; to it. Without PR #8, the code below still compiles fine.
    148 ;;
    149 ;;
    150 ;; The fact that it worked without the patch testifies that the extra scope
    151 ;;   was added on the definition of `aa` and `bb`, instead of being a new fresh
    152 ;;   scope added each time the expander is called. Here, we have two calls to
    153 ;;   the `cc` expander successfully communicating via the `w` variable, thanks
    154 ;;   to `syntax-local-introduce` (which makes the macros unhygienic).
    155 (define-foo-expander cc
    156   (λ (stx)
    157     (syntax-case stx ()
    158       [(_ #:aa)
    159        (begin
    160          (syntax-local-introduce #'[w #t]))]
    161       [(_ #:bb)
    162        (begin
    163          (syntax-local-introduce #'w))])))
    164 
    165 (test-true
    166  "Test that `w` as produced by `(cc #:bb)` is correctly bound by the `w`
    167  introduced by `(cc #:aa)`.
    168 
    169 This test succeeds without the PR #8 patch, which shows that the extra
    170  scopes are per-expander and not per-invocation. Expanders can still be
    171  unhygienic using `syntax-local-introduce`, but can communicate only with
    172  themselves."
    173  (expand-foos
    174   (let ((cc #:aa))
    175     (cc #:bb))))