guile-user
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: A macro containing a mini-macro?


From: HiPhish
Subject: Re: A macro containing a mini-macro?
Date: Fri, 02 Nov 2018 23:32:10 +0100

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 <address@hidden> 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







reply via email to

[Prev in Thread] Current Thread [Next in Thread]