unofficial mirror of guile-user@gnu.org 
 help / color / mirror / Atom feed
From: Maxime Devos via General Guile related discussions <guile-user@gnu.org>
To: Vivien Kraus <vivien@planete-kraus.eu>, Guile User <guile-user@gnu.org>
Subject: RE: Control the order of expansion of syntax-case macros
Date: Fri, 20 Dec 2024 10:15:28 +0100	[thread overview]
Message-ID: <20241220101531.qxFW2D00E2kJuzj01xFXPe@xavier.telenet-ops.be> (raw)
In-Reply-To: <1bfa042ed3719a10006789205e954bb53c83cf45.camel@planete-kraus.eu>

>syntax-case macros can have cool side effects at expansion time.
>However, they are still draped in a veil of mystery to me.

> Consider this code:

(define-syntax hello
  (lambda (stx)
    (syntax-case stx ()
      (_
       (begin
         (format (current-error-port) "Hello!\n")
         (datum->syntax #f "hello"))))))

You can simplify this to:

(define-syntax hello
  (lambda (stx)
     (format (current-error-port) "Hello!\n")
     (datum->syntax #f "hello")))

I think this simplified version makes it a little easier to think about how Scheme macros work.

(define-syntax world
  (lambda (stx)
    (syntax-case stx ()
      (_
       (begin
         (format (current-error-port) "World!\n")
         (datum->syntax #f "world"))))))

(define-syntax hello-world
  (lambda (stx)
    (syntax-case stx ()
      (_
       #`(string-append #,hello " " #,world)))))

(Likewise here)

,expand hello-world


>Running it gives me:
>
>Hello!
>World!
>$1 = (string-append "hello" " " "world")

I wonder, did you copy-paste all four blocks (both the define-syntax forms and the ,expand) at the same time, or one-by-one? I don’t have a Guile at hand to test, but IIUC if you paste the following in the REPL (without any ,expand!)

(define-syntax hello-world
  (lambda (stx)
    (syntax-case stx ()
      (_
       #`(string-append #,hello " " #,world)))))

you’ll get “Hello!<newline>World!” in the REPL, and  ‘,expand hello-world’ will only give you the ‘$1 = […]’ (_without_ the current-error-port output).

What happens here, is that Guile sees your ‘hello-world’ definition and expands it (not the macro that it defines, but rather the ‘lambda’ and its interior) (macros are usually defined with other macros – syntax-case is a macro!). But, it’s not just ‘hello-world’ that’s a macro, hello and world are macros too. Since they aren’t quoted, they are expanded.  So, during the expansion of the ‘define-syntax’ form above, you will see “Hello!” and “World!”. Can we choose which order ‘hello’ and ‘world’ are expanded, while keeping everything else the same? I don’t know.

(‘hello’ and ‘world’ expand to strings, which can be spliced into syntax. If they expanded to, say, a procedure (not just a lambda form, but rather the evaluated lambda), it is invalid (not just a type error for string-append, but invalid).)

>Cool! Now, I suspect there are no clear rules to guess the order of
expansion (whether Hello! is printed before World! or after). I would
very much like to twist that order and reliably get World! and then
Hello!. How can I achieve that?

(define-syntax hello-world
  (lambda (stx)
    (syntax-case stx ()
      (_
       (let* ((w #'world)
              (h #'hello))
         #`(string-append #,h " " #,w))))))

Wait, you are modifying more here!!! In this particular (artificial) example, it probably doesn’t matter, but in whatever the ‘real’ code will eventually be, it might. When expanding the above ‘define-syntax’ form, Guile doesn’t expand ‘world’ and ‘hello’, since they are quoted. If you paste the above definition in the REPL, you won’t get any “Hello! World!” or “World! Hello” messages. No side-effects happen from the expansion of the above define-syntax form, only once a hello-world form is expanded you’ll get the side-effects.

(Depending on what you’re doing, this change might be better or worse.)

>With this modification, I get the same order:

>Hello!
>World!
>$1 = (string-append "hello" " " "world")

>So my guess is that syntax objects are expanded lazily.

Not lazy. If you splice the same syntax into multiple locations, it will be expanded per-location. (I would call it ‘delayed’ if it weren’t for the ‘delay’ macro and ’force’ procedure that implement laziness.)

;; This one is a workaholic! Lots of World! World! World! …
(define-syntax a
  (lambda (stx)
    (let ((a #'world)) #`(list #,a #,a #,a #,a))))

For testing, consider replacing it by

(define-syntax hello-world
  (lambda (stx)
    (syntax-case stx ()
      (_
       (let* ((w #'world)
              (h #'hello))
         (pk 'Post-Quote w h)
         #`(string-append #,h " " #,w))))))
;; no information on stderr yet on stderr
,expand hello-world
;; now we see (post-quote #<syntax world …> #<syntax hello …>) Hello! World!

If you simply write #'world, then no expansion happens yet (except maybe in the sense that `(proc) ‘expands too’ (list 'proc) instead of running the procedure ‘proc’).

During #`(string-append #,h “ “ #,w), no expansion happens yet either. You are simply making a larger piece of code from some smaller pieces. Rather, it’s when you write ‘hello-world’ in some piece of code that Guile applies the lambda (lambda (stx) stuff) to #’hello-world (in realistic settings it would be #'(hello-world arg arg2)), and replaces the code fragment ‘hello-world’ by the output of the lambda. E.g., if the lambda returns #'(not-implemented), then ‘(stuff hello-world more-stuff)’ becomes ‘(stuff (not-implemented) more-stuff).

I think the best way to think of syntax, is as code with some lexical context and line numbers, and ‘quote’ (') is the equivalent of ‘syntax-quote’ (#') (or was it quote-syntax?).

>Is there something I can do to get #'world expanded before #'hello?

Rewritten question:  “Is there something I can do to get ‘world’ expanded before ‘hello’?”

I imagine the following would do it:

(define-syntax hello-world
  (lambda (stx)
    ;; let / let* wouldn’t make a difference, since it’s expansion order we’re concerned
    ;; with, not evaluation order, and the former is equally as unspecified(?) with ‘let’ as
    ;; with ‘let*’
    (let ((w world) (h hello))
      #`(string-append #,hello " " #,world)))))

(but keep in mind the possibly unintended time of expansion).

Perhaps another option would be to use the module reflection API and macro-binding or macro-transformer to extract the (lambda (stx) …) and now use that lambda, or syntax-local-bindings and do the same.

For (well-defined) methods of controlling the evaluation order,  I imagine let-syntax might be able to accomplish something, but I’m not sure.

Another option is to just let ‘hello’ and ‘world’ be procedure (that happen to accept syntax and produce syntax), and simply invoke them in hello-world (and use #, to insert them in the produced code). In this particular case, they could just be thunks. (If you do this, and also use the hello-world form in the same module as where it is defined (not just mentioned somewhere in another macro, but rather when the non-define-syntax forms are expanded, then need the transformer of the hello-world form to run), then make sure to add some eval-when where appropriate.)

Best regards,
Maxime Devos


  reply	other threads:[~2024-12-20  9:15 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-12-17 16:33 Control the order of expansion of syntax-case macros Vivien Kraus
2024-12-20  9:15 ` Maxime Devos via General Guile related discussions [this message]
2024-12-20 17:01   ` Vivien Kraus
2024-12-20 10:38 ` Nala Ginrut
2024-12-20 20:12 ` Zelphir Kaltstahl

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.gnu.org/software/guile/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20241220101531.qxFW2D00E2kJuzj01xFXPe@xavier.telenet-ops.be \
    --to=guile-user@gnu.org \
    --cc=maximedevos@telenet.be \
    --cc=vivien@planete-kraus.eu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).