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))))