Thanks Philip and Ryan,
I do need to get more familiar with the macro stepper. :) I tried it out for maybe the first time just now, but I think Philip's screenshot already helped me understand what's going on.
I recall that when Racket invokes a macro, it puts a scope on the inputs first (the macro-introduction scope), and then it flips that scope afterward so that it occurs only on the rest of the macro's output. This keeps variable occurrences that are introduced in the output from binding variables that were already in the input, and vice versa.
What's going on here seems to be that `let-third` uses `x`, but doesn't take `x` as its own input, so this scope-flipping treats it like it's an identifier the `let-third` macro introduces. If I think about the example in terms of currying, then `x` *is* an input -- it just isn't an input to this particular stage of the macro. But this isn't something Racket's hygiene is prepared to treat as an input.
And you know what? I guess I'm convinced. When I write out this far simpler example...
(let-syntax ([let-second
(syntax-parser
[(_ body)
#'(let ([x "second"])
body)])])
(let-second
x)))
...I take one look at it, and I don't expect the `(let ([x "second"]) ...)` to have anything to do with the other two occurrences of `x`. My original macro expands into a macro like this, so it doesn't work either.
I guess my intent clouded my judgment. I must have gone in assuming I could write a macro that created a `let` that shadowed the variable I wanted it to shadow, but that doesn't even work when the variable is hardcoded like this.
Philip's technique with the scope introduction with `syntax-local-introduce` looks good. I even tried `syntax-local-introduce` almost exactly like that myself before posting here, and I think I ran into the exact issue that Philip fixed by moving the `(let ([var "second"]) ...)` around. So there is a workaround to this, which is nice.
I didn't actually have a macro to write; I just wanted a test case to ensure my custom variables obeyed the kind of hygiene Racket's existing variables did. Now that I understand this better, I know that what I'm seeing isn't a broken behavior.
Thanks again,
Nia