Macro generating definitional macro

Hi,

(define-syntax-rule (define-define-macro m id)
  (define-syntax-rule (m)
    (define id (lambda (x) x))))
(define-define-macro m1 id1)
(m1)
(id1 2)

Why is id1 unbound while

(define-syntax-rule (m2 id)
    (define id (lambda (x) x)))
(m2 id2)
(id2 2)

id2 is well bound in this case?
I understand how the expander gets this result but why is the first definition not allowed by the expander? How to understand this behaviour?

The inner syntax rule hides id.

(define-syntax-rule (m)

(define id (lambda (x) x))

1 Like

When a (define-syntax-rule pattern template) macro is used, everything in template that does not appear in pattern has a macro introduction scope attached to it. I'll annotate that in the expansion of the example code you gave by adding a superscript to each identifier with the introduction scope. I'll use a for the scope added by the define-define-macro macro, and b for the scope added by the inner define-syntax-rule macro:

(define-define-macro m1 id1) ; next expansion step
(m1)
(id1 2)
=>
(define-syntax-ruleᵃ m1
  (defineᵃ id1 (lambdaᵃ (xᵃ) xᵃ)))
(m1) ; next expansion step
(id1 2)
=>
(define-syntax-ruleᵃ m1
  (defineᵃ id1 (lambdaᵃ (xᵃ) xᵃ)))
(defineᵃᵇ id1ᵇ (lambdaᵃᵇ (xᵃᵇ) xᵃᵇ))
(id1 2) ; next expansion step - error, id1 is unbound

When an identifier is defined with (define id expression), the scopes on id matter. If there's a scope on id, that scope must be present on any other id for it to count as a usage of id. So here's where we hit the problem: in this expansion, the id1 in the (id1 2) expression does not have the b scope that's present in the (defineᵃᵇ id1ᵇ (lambdaᵃᵇ (xᵃᵇ) xᵃᵇ)) definition. So that definition can't be used, leaving the id1 unbound.

More succinctly, if a macro introduces a definition - that is, the template of a macro defines an identifier and that identifier did not come from the macro's input pattern - that definition is invisible at the macro's use site.

You can undo this with syntax-local-introduce if you wish, but that's an advanced tool that's not suitable for most problems. I recommend reading and understanding Bindings as Sets of Scopes to learn more.

2 Likes

Thanks for the detailed explanation.

I actually came up with this question while reading Bindings as Sets of Scopes.

But the expander should be able to detect that id1 actually comes from the top-level definition context. Is there a rationale why the inner definition macro should not affect the top-level definition context?

Everything ultimately comes from the top-level context.

Your argument seems to be that id should not be treated as introduced by m because it was an argument to define-define-macro. But consider this example:

(define-syntax-rule (m0)
  (define id (lambda (x) x))

Then your argument would imply that id should not be treated as introduced by m0, because it was an argument to define-syntax-rule, which is actually a macro that expands into define-syntax and syntax-case** and syntax/loc. But I hope we agree that hygiene requires that id be treated as introduced and thus not accessible at the use site.

If you think those two cases should be treated differently, what rule distinguishes them? And what is the cost in complexity?

Lest I give the wrong impression, your question is a great question, and I remember my frustration when I first discovered that hygienic macros did not define abstractions that were "pure" or "baggage-free" in the way that lambda abstractions are. Other examples of abstractions with "baggage" include function abstraction in languages like C or JavaScript: in both languages, the target of return changes; in JavaScript, you can reflectively access the current function's name and arguments; in C, it changes the lifetime of stack-allocated variables. And so on.

2 Likes