I think you've correctly identified the problem. It has to do with the relationship between the reading and expansion stages, and the order of expansion.
The $nfx$
macros are inserted into the user syntax at the read stage, before we know anything yet about the meaning of the program.
At the expansion stage, the way I look at it, syntax is transformed in relation to unspecified meaning. That is, expressions are transformed according to rules of the form, "this syntax means what that syntax means." The ultimate meaning of expressions is finally determined when the expressions are evaluated after being expanded and compiled. Yet, the transformations during expansion are aware of meaning, at least in the sense that, unlike reading, expansion rules are context sensitive (cf. this great article by Shriram).
At this expansion stage, to take context into account, macros are expanded "outside in." The outermost macros get to define the meaning of the contained syntax, delegating to any additional macros as needed by expanding to them. So even though the $nfx$
is independently inserted at the reading stage in both places --- the outer one and the nested one --- when it gets to expansion, these are not expanded independently. Instead, the outer $nfx$
expands first to generate the Qi expression. This expression is then expanded by Qi (specifically by the flow
macro which specifies the Qi language). And finally, the inner $nfx$
macro is expanded.
During the Qi expansion stage, if we don't tell Qi about $nfx$
, it just assumes it is a function and uses it as a function (for example, it might try to compose
it with other functions in the expansion). This, as you also mentioned encountering elsewhere, usually leads to a syntax error.
By using define-qi-foreign-syntaxes
, we tell Qi to treat $nfx$
as a "foreign" macro, that is, a form that isn't actually a function but has the same syntax as a function and is typically used as if it were a function. Racket's and
is a good example of this (so much so, that people are usually confused when they find they can't use and
in higher-order functions like foldl
!).
So at this point, having been informed about $nfx$
, Qi first transforms it for use as a flow. To do this, it rewrites the $nfx$
expression by adding a wrapping lambda taking a single v
argument that is then passed to the original macro --- basically just wrapping the macro so it can be used as a function.
And finally, expansion is now delegated to the inner $nfx$
macro. But of course, at this stage, the expression has already been rewritten in a way that can't then be parsed correctly by the $nfx$
macro. That is,
($nfx$ string-split ~> rest ...)
has become:
(lambda (v)
($nfx$ v string-split ~> rest ~> ...))
… as you observed!
The issue is that, unlike and
, but more like let
, $nfx$
is a Racket macro that doesn't have the same syntax as function application and doesn't behave like a function. So it can't be treated as a flow by using define-qi-foreign-syntaxes
.
Regarding a solution, one option is to wrap the inner infix expression with (esc ...)
. This tells Qi that what follows is a Racket expression, and so it will not attempt to expand it.
Another way could be to write a $nfx$
Qi macro using define-qi-syntax-rule
, which expands to an appropriate use of the $nfx$
Racket macro, so that, in a way, at the point where Qi is about to rewrite the $nfx$
macro, you get to take back control of expansion at that point. In fact, the previous solution is also similar, where instead of taking control back yourself, you give control to Racket, which is good because $nfx$
is a Racket macro, after all.
Possibly one simple way to write this Qi macro is just:
(define-qi-syntax-rule ($nfx$ e ...)
(esc ($nfx$ e ...)))
I suspect this will work precisely because the $nfx$
was inserted at the reading stage in a context-free way, so that it doesn't refer to a specific binding (e.g., the Racket macro $nfx$
), and the same $nfx$
syntax could refer to either the Qi macro or the Racket macro when the bindings are resolved at expansion time. I could be wrong, though!
If this approach works out, then it could be worth making an adapter library for curly infix syntax and Qi, which defines the necessary adapter macros like $nfx$
as Qi macros.
If you have any other ideas or thoughts (or if we need to debug further), you're welcome to stop by at the next (or any) Qi meetup, which happens on Fridays