From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!.POSTED!not-for-mail From: Mark H Weaver Newsgroups: gmane.lisp.guile.user Subject: Re: A macro containing a mini-macro? Date: Fri, 28 Sep 2018 20:28:37 -0400 Message-ID: <87tvm95eq2.fsf@netris.org> References: <2093628.1NtSHukUaa@aleksandar-ixtreme-m5740> NNTP-Posting-Host: blaine.gmane.org Mime-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-Trace: blaine.gmane.org 1538180867 18630 195.159.176.226 (29 Sep 2018 00:27:47 GMT) X-Complaints-To: usenet@blaine.gmane.org NNTP-Posting-Date: Sat, 29 Sep 2018 00:27:47 +0000 (UTC) User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/26.1 (gnu/linux) Cc: guile-user@gnu.org To: HiPhish Original-X-From: guile-user-bounces+guile-user=m.gmane.org@gnu.org Sat Sep 29 02:27:42 2018 Return-path: Envelope-to: guile-user@m.gmane.org Original-Received: from lists.gnu.org ([208.118.235.17]) by blaine.gmane.org with esmtp (Exim 4.84_2) (envelope-from ) id 1g636j-0004kh-Ui for guile-user@m.gmane.org; Sat, 29 Sep 2018 02:27:42 +0200 Original-Received: from localhost ([::1]:48265 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1g638q-0002FA-9X for guile-user@m.gmane.org; Fri, 28 Sep 2018 20:29:52 -0400 Original-Received: from eggs.gnu.org ([2001:4830:134:3::10]:48524) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1g638J-0002F2-Pr for guile-user@gnu.org; Fri, 28 Sep 2018 20:29:24 -0400 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1g6388-0004oV-Fv for guile-user@gnu.org; Fri, 28 Sep 2018 20:29:15 -0400 Original-Received: from world.peace.net ([64.112.178.59]:32846) by eggs.gnu.org with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1g6387-0004TJ-HW for guile-user@gnu.org; Fri, 28 Sep 2018 20:29:08 -0400 Original-Received: from mhw by world.peace.net with esmtpsa (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89) (envelope-from ) id 1g637r-0003Oz-2E; Fri, 28 Sep 2018 20:28:51 -0400 In-Reply-To: <2093628.1NtSHukUaa@aleksandar-ixtreme-m5740> (HiPhish's message of "Fri, 14 Sep 2018 00:04:04 +0200") X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] X-Received-From: 64.112.178.59 X-BeenThere: guile-user@gnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: General Guile related discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: guile-user-bounces+guile-user=m.gmane.org@gnu.org Original-Sender: "guile-user" Xref: news.gmane.org gmane.lisp.guile.user:14903 Archived-At: Hi, HiPhish 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 (=CE=BB () (open-bytevector-output-port)) > (=CE=BB (out get-bv) > (pack given out) > (let ((received (get-bv)) > (expected (u8-list->bytevector '(byte byte* ...)))) > (test-assert (bytevector=3D? received expected))))) > ... > (test-end title))))) > > The idea is that I can specify a series of test cases where each case con= sists=20 > of an object and a sequence of bytes which this object is to be serialize= d to: > > (test-cases "Single precision floating point numbers" > (+3.1415927410125732 (#xCA #b01000000 #b01001001 #b00001111 #b11011= 011)) > (-3.1415927410125732 (#xCA #b11000000 #b01001001 #b00001111=20 > #b11011011))) > > This works fine, but sometimes there is a sequence of the same bytes and = it=20 > 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=20 > 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-li= st=20 > count byte)` for example) and splice it in. This distinction needs to be = made=20 > 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 `syn= tax- > 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 (=CE=BB () (open-bytevector-output-port)) (=CE=BB (out get-bv) (pack given out) (let ((received (get-bv)) (expected (compact-bytevector '(seg ...)))) (test-assert (bytevector=3D? received expected))))) ... (test-end title))))) scheme@(guile-user)> (compact-bytevector '(#xDC (16 #x00))) $2 =3D #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 n= umbers" ((make-vector 16 0) (#xDC (16 #x00= )))) $3 =3D (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=3D? received expected))))) (test-end "Single precision floating point numbers")) scheme@(guile-user)>=20 --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 (=CE=BB () (open-bytevector-output-port)) (=CE=BB (out get-bv) (pack given out) (let ((received (get-bv)) (expected (compact-bytevector-literal (seg ...)))) (test-assert (bytevector=3D? 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