unofficial mirror of guile-user@gnu.org 
 help / color / mirror / Atom feed
From: HiPhish <hiphish@posteo.de>
To: Mark H Weaver <mhw@netris.org>
Cc: guile-user@gnu.org
Subject: Re: A macro containing a mini-macro?
Date: Fri, 02 Nov 2018 23:32:10 +0100	[thread overview]
Message-ID: <3896308.xfKRN2Y66f@aleksandar-ixtreme-m5740> (raw)
In-Reply-To: <87tvm95eq2.fsf@netris.org>

Thank you very much for the in-depth explanation, and sorry for answering only 
now. I haven't been able to get around to it until now, and this is exactly 
what I was looking for. One more question if you feel like answering: where 
can I learn properly about Scheme macros? Metaprogramming is one of the most 
powerful features in Lisp, but I haven't come across a really good explanation 
of all its facets.

On Saturday, 29 September 2018 02:28:37 CET Mark H Weaver wrote:
> Hi,
> 
> HiPhish <hiphish@posteo.de> writes:
> > Hello Schemers,
> > 
> > I have written a small macro for writing test specifications:
> >     (define-syntax test-cases
> >     
> >       (syntax-rules ()
> >       
> >         ((_ title
> >         
> >            (given (byte byte* ...))
> >            ...)
> >          
> >          (begin
> >          
> >            (test-begin title)
> >            (call-with-values (λ () (open-bytevector-output-port))
> >            
> >              (λ (out get-bv)
> >              
> >                (pack given out)
> >                (let ((received (get-bv))
> >                
> >                      (expected (u8-list->bytevector '(byte byte* ...))))
> >                  
> >                  (test-assert (bytevector=? received expected)))))
> >            
> >            ...
> >            (test-end title)))))
> > 
> > The idea is that I can specify a series of test cases where each case
> > consists> 
> > of an object and a sequence of bytes which this object is to be serialized 
to:
> >     (test-cases "Single precision floating point numbers"
> >     
> >       (+3.1415927410125732 (#xCA #b01000000 #b01001001 #b00001111
> >       #b11011011))
> >       (-3.1415927410125732 (#xCA #b11000000 #b01001001 #b00001111
> > 
> > #b11011011)))
> > 
> > This works fine, but sometimes there is a sequence of the same bytes and
> > it
> > 
> > would be more readable if I could write something like this:
> >     ((make-vector 16 0) (#xDC (16 #x00)))
> > 
> > instead of writing out 16 times `#x00`. This would require being able to
> > make a distinction in the pattern whether `byte` is of the pattern
> > 
> >     byte
> > 
> > or
> > 
> >     (count byte)
> > 
> > and if it's the latter construct a list of `count` `byte`s (via
> > `(make-list
> > count byte)` for example) and splice it in. This distinction needs to be
> > made for each byte specification because I want to mix actual bytes and
> > these "RLE- encoded" byte specifications.
> > 
> > So I guess what I'm looking for is to have a `syntax-rules` inside a
> > `syntax- rules` in a way. Can this be done?
> 
> It cannot be done with pure 'syntax-rules' macros, but it can certainly
> be done with procedural macros, sometimes called 'syntax-case' macros.
> Procedural macros are quite general, allowing you to write arbitrary
> Scheme code that runs at compile time to inspect the macro operands and
> generate arbitrary code.
> 
> I'll describe how to do this with macros further down, but let me begin
> with the simple approach.
> 
> As rain1 suggested, this can be accomplished most easily by writing a
> normal procedure to convert your compact bytevector notation into a
> bytevector, and then having your macro expand into code that calls that
> procedure.  Here's working code to do that:
> 
> --8<---------------cut here---------------start------------->8---
> (use-modules (ice-9 match)
>              (srfi srfi-1)
>              (rnrs bytevectors))
> 
> (define (compact-bytevector segments)
>   (u8-list->bytevector
>    (append-map (match-lambda
>                  ((count byte)  (make-list count byte))
>                  (byte          (list byte)))
>                segments)))
> 
> (define-syntax test-cases
>   (syntax-rules ()
>     ((_ title
>         (given (seg ...))
>         ...)
>      (begin
>        (test-begin title)
>        (call-with-values (λ () (open-bytevector-output-port))
>          (λ (out get-bv)
>            (pack given out)
>            (let ((received (get-bv))
>                  (expected (compact-bytevector '(seg ...))))
>              (test-assert (bytevector=? received expected)))))
>        ...
>        (test-end title)))))
> 
> 
> scheme@(guile-user)> (compact-bytevector '(#xDC (16 #x00)))
> $2 = #vu8(220 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
> scheme@(guile-user)> ,expand (test-cases "Single precision floating point
> numbers" ((make-vector 16 0) (#xDC (16 #x00)))) $3 = (begin
>        (test-begin
>          "Single precision floating point numbers")
>        (call-with-values
>          (lambda () (open-bytevector-output-port))
>          (lambda (out get-bv)
>            (pack (make-vector 16 0) out)
>            (let ((received (get-bv))
>                  (expected (compact-bytevector '(220 (16 0)))))
>              (test-assert (bytevector=? received expected)))))
>        (test-end
>          "Single precision floating point numbers"))
> scheme@(guile-user)>
> --8<---------------cut here---------------end--------------->8---
> 
> Now, suppose it was important to do more of this work at macro expansion
> time.  For example, if efficiency was a concern, it might not be
> acceptable to postpone the conversion of '(#xDC (16 #x00)) into
> #vu8(220 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0) until run time.
> 
> It turns out that pure 'syntax-rules' macros are turing complete, but
> they are limited in the ways that they can inspect the syntax objects
> given to them as operands.  In particular, they cannot inspect atomic
> expressions, except to compare them with the finite set of literals in
> the first operand to 'syntax-rules'.  This is not sufficient to
> interpret an arbitrary integer literal.  It could only be done with
> 'syntax-rules' macros if the 'count' field were represented using a
> finite set of literals and/or list structure.  E.g. it could be done if
> the count were represented as a list of decimal digits like (1 4 2) for
> 142.
> 
> In cases like this, we would normally turn to procedural macros,
> e.g. 'syntax-case' macros.  Here's a straightforward approach, reusing
> the 'compact-bytevector' procedure given above, but calling it at
> compile time instead of at run time:
> 
> --8<---------------cut here---------------start------------->8---
> (use-modules (ice-9 match)
>              (srfi srfi-1)
>              (rnrs bytevectors))
> 
> (define (compact-bytevector segments)
>   (u8-list->bytevector
>    (append-map (match-lambda
>                  ((count byte)  (make-list count byte))
>                  (byte          (list byte)))
>                segments)))
> 
> (define-syntax compact-bytevector-literal
>   (lambda (stx)
>     (syntax-case stx ()
>       ((_ (seg ...))
>        (compact-bytevector (syntax->datum #'(seg ...)))))))
> 
> (define-syntax test-cases
>   (syntax-rules ()
>     ((_ title
>         (given (seg ...))
>         ...)
>      (begin
>        (test-begin title)
>        (call-with-values (λ () (open-bytevector-output-port))
>          (λ (out get-bv)
>            (pack given out)
>            (let ((received (get-bv))
>                  (expected (compact-bytevector-literal (seg ...))))
>              (test-assert (bytevector=? received expected)))))
>        ...
>        (test-end title)))))
> --8<---------------cut here---------------end--------------->8---
> 
> Here, instead of having 'test-cases' expand into a procedure call to
> 'compact-bytevector', it expands into a *macro* call to
> 'compact-bytevector-literal'.  The latter is a procedural macro, which
> calls 'compact-bytevector' at compile time.
> 
> This approach is sufficient in this case, but I sense in your question a
> desire to be able to perform more general inspection on the macro
> operands and generation of the resulting code.  This particular example
> is not ideally suited for this task, but the following example code
> comes a bit closer:
> 
> --8<---------------cut here---------------start------------->8---
> (define (segment-syntax->u8-list stx)
>   (syntax-case stx ()
>     ((count byte)
>      (every number? (syntax->datum #'(count byte)))  ;optional guard
>      (make-list (syntax->datum #'count)
>                 (syntax->datum #'byte)))
>     (byte
>      (number? (syntax->datum #'byte))  ;optional guard
>      (list (syntax->datum #'byte)))))
> 
> (define (compact-bytevector-syntax->bytevector stx)
>   (syntax-case stx ()
>     ((seg ...)
>      (u8-list->bytevector
>       (append-map segment-syntax->u8-list
>                   #'(seg ...))))))
> 
> (define-syntax compact-bytevector-literal
>   (lambda (stx)
>     (syntax-case stx ()
>       ((_ (seg ...))
>        (compact-bytevector-syntax->bytevector #'(seg ...))))))
> --8<---------------cut here---------------end--------------->8---
> 
> I've omitted the 'test-cases' macro here because it's unchanged from the
> previous example.  Here we have two normal procedures that use
> 'syntax-case', which might be a bit confusing.  These are procedures
> that accept syntax objects as arguments, and return normal data
> structures.
> 
> In contrast to the previous example, which used 'syntax->datum' on the
> entire compact-bytevector-literal, in this example we inspect and
> destruct the syntax object itself using 'syntax-case'.  This would be
> needed in the more general case where identifiers (i.e. variable
> references) might occur in the syntax objects.
> 
> Hopefully this gives you some idea of what can be done, but I don't
> think this is the best example to explore these possibilities, since in
> this case the normal procedural approach in my first code excerpt above
> is simplest and most likely sufficient.
> 
>      Regards,
>        Mark







      parent reply	other threads:[~2018-11-02 22:32 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-09-13 22:04 A macro containing a mini-macro? HiPhish
2018-09-13 22:24 ` rain1
2018-09-15 22:21   ` HiPhish
2018-09-29  0:28 ` Mark H Weaver
2018-09-29  0:58   ` Mark H Weaver
2018-09-29  7:37   ` Arne Babenhauserheide
2018-11-02 22:32   ` HiPhish [this message]

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=3896308.xfKRN2Y66f@aleksandar-ixtreme-m5740 \
    --to=hiphish@posteo.de \
    --cc=guile-user@gnu.org \
    --cc=mhw@netris.org \
    /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).