Merge pull request #8 from jsmaniac/fix-use-site-scope
Remove the extra use-site scope on the body of define-?-expander.
This commit is contained in:
commit
8e501ebeb6
|
@ -7,6 +7,13 @@
|
|||
|
||||
(provide define-expander-type)
|
||||
|
||||
(define-for-syntax (remove-use-site-scope stx)
|
||||
(define bd
|
||||
(syntax-local-identifier-as-binding (syntax-local-introduce #'here)))
|
||||
(define delta
|
||||
(make-syntax-delta-introducer (syntax-local-introduce #'here) bd))
|
||||
(delta stx 'remove))
|
||||
|
||||
(define-syntax define-expander-type
|
||||
(syntax-parser
|
||||
[(_ name:id)
|
||||
|
@ -15,13 +22,16 @@
|
|||
[?-expander? "~a-expander?"]
|
||||
[define-?-expander "define-~a-expander"]
|
||||
[expand-all-?-expanders "expand-all-~a-expanders"])
|
||||
#'(begin
|
||||
#`(begin
|
||||
(define-for-syntax ?-expander-type (make-expander-type))
|
||||
(define-for-syntax (make-?-expander transformer)
|
||||
(expander ?-expander-type transformer))
|
||||
(define-for-syntax (?-expander? v)
|
||||
(expander-of-type? ?-expander-type v))
|
||||
(define-syntax-rule (define-?-expander expander-name transformer)
|
||||
(define-syntax expander-name (make-?-expander transformer)))
|
||||
(define-syntax define-?-expander
|
||||
(syntax-parser
|
||||
[(_ expander-name:id transformer:expr)
|
||||
(remove-use-site-scope
|
||||
#'(define-syntax expander-name (make-?-expander transformer)))]))
|
||||
(define-for-syntax (expand-all-?-expanders stx)
|
||||
(expand-syntax-tree-with-expanders-of-type ?-expander-type stx))))]))
|
||||
|
|
175
test/test-define-x-expander-use-site-scope-simple.rkt
Normal file
175
test/test-define-x-expander-use-site-scope-simple.rkt
Normal file
|
@ -0,0 +1,175 @@
|
|||
#lang racket
|
||||
|
||||
;; For a more realistic use-case justifying why this behaviour matters, see the
|
||||
;; discussion at https://github.com/jackfirth/generic-syntax-expanders/pull/8
|
||||
;; and in particular the test file at
|
||||
;; https://github.com/jsmaniac/generic-syntax-expanders
|
||||
;; /blob/ec43791028715221c678f8536389a39ee760ed98
|
||||
;; /test/test-define-x-expander-use-site-scope.rkt
|
||||
|
||||
(require generic-syntax-expanders
|
||||
rackunit)
|
||||
|
||||
(define-expander-type foo)
|
||||
|
||||
(define-syntax (expand-foos stx)
|
||||
(syntax-case stx ()
|
||||
[(_ body)
|
||||
(expand-all-foo-expanders #'body)]))
|
||||
|
||||
;; Without PR #8, the `x` in `bb` is not `bound-identifier=?` to the `x`
|
||||
;; defined by `aa`, despite the use of `syntax-local-introduce` to make the
|
||||
;; macros unhygienic.
|
||||
;;
|
||||
;; This happens because `define-foo-expander` added an extra "use-site" scope
|
||||
;; to the body of `aa` and a different "use-site" scope to the body of `bb`.
|
||||
(define-foo-expander aa
|
||||
(λ (_)
|
||||
(syntax-local-introduce #'[x #t])))
|
||||
|
||||
(define-foo-expander bb
|
||||
(λ (_)
|
||||
(syntax-local-introduce #'x)))
|
||||
|
||||
;; Due to the way `let` itself adds scopes to its definition and body, this
|
||||
;; makes the identifiers `x` from `aa` and `x` from `bb` distinct, and the
|
||||
;; latter cannot be used to refer to the former.
|
||||
;;
|
||||
;; Approximately, the code below expands to
|
||||
;;
|
||||
;; (let [(x⁰¹ #t)]
|
||||
;; x⁰²)
|
||||
;;
|
||||
;; The `let` form then adds a "local" scope to both occurrences of `x`, and an
|
||||
;; internal-definition context "intdef" scope to the `x` present in the body of
|
||||
;; the `let` (but not to the one present in the bindings). The expanded form
|
||||
;; therefore becomes:
|
||||
;;
|
||||
;; (let [(x⁰¹³ #t)]
|
||||
;; x⁰²³⁴)
|
||||
;;
|
||||
;; where:
|
||||
;; ⁰ are the module's scopes
|
||||
;; ¹ is the undesired "use-site" scope added by `define-foo-expander` on `aa`
|
||||
;; ² is the undesired "use-site" scope added by `define-foo-expander` on `bb`
|
||||
;; ³ is the "local" scope added by `let`
|
||||
;; ⁴ is the "intdef" scope added by `let`
|
||||
;;
|
||||
;; Since {0,2,3,4} is not a subset of {0,1,3}, the `x` inside the `let` is
|
||||
;; unbound.
|
||||
(test-true
|
||||
"Test that `x` as produced by `(bb)` is correctly bound by the `x`
|
||||
introduced by `(aa)`.
|
||||
|
||||
This test fails without the PR #8 patch, because the body of `bb` and the body
|
||||
of `aa` each have a different use-site scope, introduced by accident by
|
||||
`define-foo-expander`. The occurrence of `x` introduced by `aa` and the
|
||||
occurrence of `x` introduced by `bb` therefore have different scopes, and the
|
||||
latter is not bound by the former.
|
||||
|
||||
Without the PR #8 patch, this test case will not compile, and will fail with
|
||||
the error `x: unbound identifier in module`."
|
||||
(expand-foos
|
||||
(let ((aa))
|
||||
(bb))))
|
||||
|
||||
;; ----------
|
||||
|
||||
;; It is worth noting that `define` seems to strip the "use-site" scopes present
|
||||
;; on the defined identifier. If the code above is changed so that a `define`
|
||||
;; form is used, the problem does not occur:
|
||||
(define-foo-expander aa-def
|
||||
(λ (_)
|
||||
(syntax-local-introduce #'[define y #t])))
|
||||
|
||||
(define-foo-expander bb-def
|
||||
(λ (_)
|
||||
(syntax-local-introduce #'y)))
|
||||
|
||||
;; This is because the code below expands to:
|
||||
;;
|
||||
;; (begin (define y⁰ #t)
|
||||
;; (define y-copy y⁰²⁵)
|
||||
;;
|
||||
;; where:
|
||||
;; ⁰ are the module's scopes
|
||||
;; ¹ is the undesired "use-site" scope added by `define-foo-expander` on `aa`
|
||||
;; and it is stripped by `define` from the first `y`
|
||||
;; ² is the undesired "use-site" scope added by `define-foo-expander` on `bb`
|
||||
;; ⁵ is the "use-site" scope added because it is in an expression position
|
||||
;;
|
||||
;; Since {0,2,5} is a subset of {0}, the second `y` refers to the first `y`.
|
||||
(expand-foos
|
||||
(begin (aa-def)
|
||||
(define y-copy (bb-def))))
|
||||
(test-true
|
||||
"Test that `y` as produced by `(bb-def)` is correctly bound by the `y`
|
||||
defined by `(aa-def)`.
|
||||
|
||||
This test succeeds without the PR #8 patch, which shows that `define` removes
|
||||
all use-site scopes on the defined identifier (or at least it removes all the
|
||||
use-site scopes present in this example). This can be checked in the macro
|
||||
debugger, and explains why the test case did not fail with a simple `define`,
|
||||
but does fail with a binding introduced by a `let`."
|
||||
y-copy)
|
||||
|
||||
;; ----------
|
||||
|
||||
;; The code below attempts to remove the extra "use-site" scope with
|
||||
;; `syntax-local-identifier-as-binding`. However, that function does
|
||||
;; not remove all use-site scopes, unlike the `define` above.
|
||||
|
||||
(define-foo-expander aa-as-binding
|
||||
(λ (_)
|
||||
#`[#,(syntax-local-identifier-as-binding (syntax-local-introduce #'z)) #t]))
|
||||
|
||||
(define-foo-expander bb-as-binding
|
||||
(λ (_)
|
||||
(syntax-local-introduce #'z)))
|
||||
|
||||
(test-true
|
||||
"Test that `z` as produced by `(bb-as-binding)` is correctly bound by
|
||||
the `z` defined by `(aa-as-binding)`.
|
||||
|
||||
This test fails without the PR #8 patch, which shows that that unlike `define`,
|
||||
the `syntax-local-identifier-as-binding` function does not remove all use-site
|
||||
scopes.
|
||||
|
||||
Without the PR #8 patch, this test case will not compile, and will fail with
|
||||
the error `z: unbound identifier in module`."
|
||||
(expand-foos
|
||||
(let ((aa-as-binding))
|
||||
(bb-as-binding))))
|
||||
|
||||
;; ----------
|
||||
|
||||
;; The `cc` expander acts either as aa or as bb depending on the keyword passed
|
||||
;; to it. Without PR #8, the code below still compiles fine.
|
||||
;;
|
||||
;;
|
||||
;; The fact that it worked without the patch testifies that the extra scope
|
||||
;; was added on the definition of `aa` and `bb`, instead of being a new fresh
|
||||
;; scope added each time the expander is called. Here, we have two calls to
|
||||
;; the `cc` expander successfully communicating via the `w` variable, thanks
|
||||
;; to `syntax-local-introduce` (which makes the macros unhygienic).
|
||||
(define-foo-expander cc
|
||||
(λ (stx)
|
||||
(syntax-case stx ()
|
||||
[(_ #:aa)
|
||||
(begin
|
||||
(syntax-local-introduce #'[w #t]))]
|
||||
[(_ #:bb)
|
||||
(begin
|
||||
(syntax-local-introduce #'w))])))
|
||||
|
||||
(test-true
|
||||
"Test that `w` as produced by `(cc #:bb)` is correctly bound by the `w`
|
||||
introduced by `(cc #:aa)`.
|
||||
|
||||
This test succeeds without the PR #8 patch, which shows that the extra
|
||||
scopes are per-expander and not per-invocation. Expanders can still be
|
||||
unhygienic using `syntax-local-introduce`, but can communicate only with
|
||||
themselves."
|
||||
(expand-foos
|
||||
(let ((cc #:aa))
|
||||
(cc #:bb))))
|
Loading…
Reference in New Issue
Block a user