scopes across files

55 views
Skip to first unread message

Eric Griffis

unread,
Jun 3, 2019, 2:52:34 PM6/3/19
to Racket Users

Several times now, I've run into one or another form of the following problem:

Say I want to build primitives to

  1. declare an "interface" as a list of names, and
  2. implement and use those names at run time in a limited scope

Concretely, I want to run the following code:

(interface Speaker say speak)
(implement Speaker displayln (say 'hello) speak)

If I put these lines at the end of a file that implements the two forms, it should (displayln 'hello) when I run it.

Here is the interface macro:

(define-syntax (interface stx)
  (syntax-case stx ()
    [(_ id member-id ...) #'(define-syntax id #'(member-id ...))]))

The implement macro is trickier. A naive implementation looks like this:

(define-syntax (implement stx)
  (syntax-case stx ()
    [(_ class-id def ... expr)
     (with-syntax ([(id ...) (syntax-e (syntax-local-value #'class-id))])
       #'(letrec ([id def] ...) expr))]))

It fails with the following message:

; /tmp/j.rkt:44:30: say: unbound identifier
;   in: say
;   context...:
;    #(570423 use-site) [common scopes]
;   other binding...:
;    local
;    #(570422 macro) [common scopes]
;   common scopes...:
;    #(570223 module) #(570226 module j) #(570426 local) #(570427 intdef)
;    #(570428 local)

Incorporating the debug-scopes package's +scopes macro shows this:

(letrec⁰˙˙¹
 ((say⁰˙˙¹ displayln⁰˙˙³) (speak⁰˙˙¹ (say⁰˙˙³ (quote⁰˙˙³ hello⁰˙˙³))))
 speak⁰˙˙³)ˢˡⁱ⁼²⁺ᵘˢᵉ⁼³
0 module   570512
1 module j 570515
2 macro    570719
3 use-site 570720

For reasons I don't yet fully comprehend, it just works out if I use syntax-local-introduce on the id's, so +scopes gives me this:

(letrec⁰˙˙¹
 ((say⁰˙˙³ displayln⁰˙˙³) (speak⁰˙˙³ (say⁰˙˙³ (quote⁰˙˙³ hello⁰˙˙³))))
 speak⁰˙˙³)ˢˡⁱ⁼²⁺ᵘˢᵉ⁼³
0 module   571247
1 module j 571250
2 macro    571455
3 use-site 571456

But if the interface and implement invocations are not in the same file as their implementations, it breaks with a similar error:

; /tmp/h.rkt:5:30: say: unbound identifier
;   in: say
;   context...:
;    #(572682 module) [common scopes]
;   other binding...:
;    local
;    #(572388 module) #(572391 module j) #(572596 module)
;    #(572599 module j) [common scopes]
;   common scopes...:
;    #(572685 module h) #(572700 local) #(572701 intdef) #(572702 local)

In this situation, +scopes gives:

(letrec⁰˙˙³
 ((say⁴˙˙⁵ displayln⁴˙˙⁶) (speak⁴˙˙⁵ (say⁴˙˙⁶ (quote⁴˙˙⁶ hello⁴˙˙⁶))))
 speak⁴˙˙⁶)ˢˡⁱ⁼⁶⁺ᵘˢᵉ⁼
0 module   576854
1 module j 576857
2 module   577056
3 module j 577059
4 module   577150
5 module h 577153
6 macro    577173

If I move the interface invocation back into the original file and keep the implement invocation in a separate file, I get the same error but +scopes gives something slightly different:

(letrec⁰˙˙³
 ((say⁰˙˙⁴ displayln⁴˙˙⁶) (speak⁰˙˙⁴ (say⁴˙˙⁶ (quote⁴˙˙⁶ hello⁴˙˙⁶))))
 speak⁴˙˙⁶)ˢˡⁱ⁼⁴⁺ᵘˢᵉ⁼
0 module   572388
1 module j 572391
2 module   572596
3 module j 572599
4 macro    572697
5 module   572682
6 module h 572685

What's going on here? How do I fix this?

Eric

Eric Griffis

unread,
Jun 3, 2019, 3:51:47 PM6/3/19
to Racket Users
Apologies to mobile users. The superscripts added by +scopes aren't boxes in the Web client.

To clarify my question: how do I handle the situation where variables bound by one macro and used by another are out of scope when file boundaries are involved? 

The +scopes into shows that the scope sets go from intersecting to disjoint or sharing only a macro scope.

Fastmail

unread,
Jun 6, 2019, 3:19:59 AM6/6/19
to Eric Griffis, Racket Users


On Jun 3, 2019, at 11:52 AM, Eric Griffis <ded...@gmail.com> wrote:

Several times now, I've run into one or another form of the following problem:

Say I want to build primitives to

  1. declare an "interface" as a list of names, and
  2. implement and use those names at run time in a limited scope


The `implement` macro needs to place its identifiers (say, `say`) inside the lexical context of the calling site, so that they bind other code coming in from the calling site (for instance, `(say 'hello)`). Below, the submodules are not essential for the example, but are meant to show that the module (or file) boundaries don't affect the result. 

#lang racket

(module interface racket
  (provide interface)
  (define-syntax (interface stx)
    (syntax-case stx ()
      [(_ id member-id ...) #'(define-syntax id #'(member-id ...))])))

(module implement racket
  (require (for-syntax racket/syntax))
  (provide implement)
  (define-syntax (implement stx)
    (syntax-case stx ()
      [(_ class-id def ... expr)
       (with-syntax ([(id ...) (for/list ([stx (syntax-e (syntax-local-value #'class-id))])
                                 (format-id #'expr "~a" (syntax->datum stx)))])
         #'(letrec ([id def] ...) expr))])))

(require 'interface 'implement)

Eric Griffis

unread,
Jun 11, 2019, 3:47:17 PM6/11/19
to Fastmail, Racket Users
On Thu, Jun 6, 2019 at 12:19 AM Fastmail <m...@mbtype.com> wrote:
>
> The `implement` macro needs to place its identifiers (say, `say`) inside
> the lexical context of the calling site, so that they bind other code
> coming in from the calling site (for instance, `(say 'hello)`).

Thanks! This was incredibly helpful. Swimming in a soup of phase-1 details, I
forgot which way is up -- in this case, toward the caller's context.

It's too easy to lose sight of the forest for the trees at this part of the
journey.

Eric
Reply all
Reply to author
Forward
0 new messages