Ok. Let's see if I can explain away all mysteries by carefully following the syntax and expansion model. Someone please read it through and poke holes. In the process, if I'm not mistaken, we are going to discover an error in the Scopes section of the docs.
Step aside everyone, I'm putting my Matthew hat on. Here goes nothing!
We are trying to answer the following questions about this piece of code (I annotated some identifiers with indexes [in sq brackets]):
(define[1] (foo-impl[1] op a b) (op a b))
(begin
(define[2] (foo-impl[2] formal ...) body ...)
(define-syntax (name stx)
.....
(foo-impl[3] . args)
.....)))
(define-foo (bar op a b) (op a b))[4]
(define-foo (baz op a b) (op a b))[5]
(bar + 1 2)
Q1: My original question was why the two call sites [4] and [5] do not complain about redefinition of /foo-impl/. After all every time the transformer is invoked it generates a definition of the same identifier /foo-impl/, which I can easily see in macro-expansion at the relevant call site. However, if I were to type /foo-impl/ definitions by hand at top-level or module level Racket would yell at me. Why the two cases look about the same (i.e. end up producing visually the same code) but invoke different reaction from the compiler?
Suppose the transformer at [4] did its job and we are now evaluating the code it produced, that is the binding [2] it introduced and that use of /foo-impl/ at [3]. Generated [2] will have at least two scopes: one from the macro definition site i.e. /define-foo/, the other is the fresh macro-introduction scope. When the reference to /foo-impl/ at [3] gets resolved we'll be looking for bindings of /foo-impl/ whose scope sets are subsets of the reference, that is of the identifier at [3]. In fact we find two such bindings: [1] and [2]. Which one do we choose? We choose the one whose scope set is the superset of any other binding we discovered. Here [2] has at least one extra scope (macro-intro scope) compared to [1], so we use [2].
Now, why is there are no ambiguity as to which /foo-impl/ to use when we expand and eval [5]? Well, we'll go through the same motions, but there will be one extra /foo-impl/ binding generated at [4], so we'll have to choose from the total of 3 bindings when resolving any /foo-impl/ ref at [5]. And again we choose the [2] that is the result of expansion of [5]. Why? Well, it'll have that fresh macro intro scope passed to the transformer from [5] and it differs from the macro intro scope at [4], so there is no ambiguity between the two generated bindings at [4] and [5]. For any ref to /foo-impl/ generated by [5] we reject the /foo-impl/ binding generated at [4] because its scope set isn't a subset of any ref at [5]. To choose between [1] and generated [2] we use the same logic as in the previous paragraph: [2] wins cause its scope set is bigger.
So, the three bindings of identifier /foo-impl/ that the code above introduces at [1], [4] and [5] (latter two generated from the template at [2]) are not at all the same, at least not in the syntax model of Racket. Identifiers aren't merely compared by name, their scope sets have the final say in how identifiers are resolved.
Q2: My macro introduces a new binding for /foo-impl/ at [2]. How is that identifier different from the /define/ identifier at [2]? That is to ask why the /define/ at [2] is bound as we expect to the /define/ in Racket, while any reference to /foo-impl/ in the subsequent template code refers to the binding at [2].
The part about any /foo-impl/ ref in the template to the binding at [2] we already answered in Q1. Other bindings e.g. /define/ at [2] are again resolved as we discussed in Q1. This particular /define/ would resolve using the macro definition site, one of /define-foo/.
Q3: If we were to remove /foo-impl/ binding at [2] any template code would happily refer to /foo-impl/ binding at [1]. How so?
Again Q1 kinda answers that. Put simply there are fewer /foo-impl/ bindings to choose from, and the one at [1] happens to have the scope set that is a subset of any use in the template e.g. at [3].
Q4: If instead of [5] I were to just copy paste [4], would Racket yell about attempt to redefine /bar/? Why? (this is inspired by Greg's comment about defining /blerg/).
Yes it would. /bar/ id in both expansions of /define-foo/ would have the exact same scope-set (from the macro use site) and won't have a macro intro scope to disambiguate. That's cause we passed /bar/ id to the macro as syntax object (technically, each /bar/ would get a fresh macro intro scope on the way "into" the transformer, but it gets removed on the way "out" i.e. in the code generated by the transformer; in reverse any identifier in the macro template would start with no macro-intro scope but end up with one in the generated code - the process referred to in the docs as "flipping" the macro intro scopes).
In conclusion. Racket macro system requires some careful thought, looking at macro expansion will only take you so far. It took a lot out of me to think things through and put em in writing. Ben was right. Indeed, Racket macros are hygienic but I guess saying that it'll "gensym" for you is basically waving a lot of details away. Because instead of renaming we use these scope sets (glorified tokens or tags, really) and syntax objects carry those sets with them and can borrow, erase, lend them, I guess Racket can encode really bizarre scoping rules when you so desire (or you don't and just screwed up). It is certainly rich and expressive. But I wonder if there are shortcuts one could take to quickly reason in situations like these? Ones that would give the right answer 99% of time without deliberating about scope sets n all. Maybe it just gets easier every time you do it.
An identifier refers to a particular binding when the reference’s symbol and the identifier’s symbol are the same, and when the reference’sscope set is a subset of the binding’s scope set.
Should probably read:
An identifier refers to a particular binding when the reference’s symbol and the identifier’s symbol are the same, and when the reference’s scope set is a superset of the binding’s scope set.
or perhaps equivalently
An identifier refers to a particular binding when the reference’s symbol and the identifier’s symbol are the same, and when the binding’s scope set is a subset of the reference’s scope set.
Unless I'm mistaken that much should be obvious from the examples and amounts to the fact that as we go deeper (in nesting) into the code tree the number of scopes attached to identifiers can only grow, therefore it follows that any reference to something "previously" defined would have "more" scopes not fewer compared to its potential bindings. (caveat: strictly this may not be always true cause a macro transformer could get fancy and borrow another identifier's scope for whatever it generates, but whatever).
Also the next sentence about ref resolution kinda hints at the correct wording:
For a given identifier, multiple bindings may have scope sets that are subsets of the identifier’s; in that case, the identifier refers to the binding whose set is a superset of all others; if no such binding exists, the reference is ambiguous
Was that convincing? Do I misunderstand how scope sets work after all? Do I need to PR?
Thanks