Discussion:
macroexpand-1
Catonano
2018-05-21 15:48:41 UTC
Permalink
in the NEWS file, I read:


...
** Removed function: `macroexpand-1'

It is unclear how to implement `macroexpand-1' with syntax-case, though
PLT Scheme does prove that it is possible.




what's the problem with macroexpand-1 and syntax-case ?

Thanks
Mark H Weaver
2018-05-29 15:01:35 UTC
Permalink
Hi,
Post by Catonano
...
** Removed function: `macroexpand-1'
It is unclear how to implement `macroexpand-1' with syntax-case, though
PLT Scheme does prove that it is possible.
what's the problem with macroexpand-1 and syntax-case ?
In Guile 1.x, 'macroexpand-1' performed a single macro expansion step at
the top-level form of an expression, using its old non-hygienic macro
expander. There are several problems with trying to provide such an
interface in a Hygienic macro expander, and especially in the
'syntax-case' expander with its support for 'datum->syntax'. For one
thing, our modern macro expander doesn't even work with the plain
S-expressions which 'macroexpand-1' accepted and produced. It works
with "syntax objects", which effectively annotate every identifier with
extra information needed to determine which binding it references, and
also extra information needed to implement 'datum->syntax'. This in
turn requires detailed knowledge of the lexical environment in which
expansion is taking place, whereas 'macroexpand-1' provides no way for
the user to provide this information.

Mark
Catonano
2018-05-29 19:01:34 UTC
Permalink
Anyway: thank you !!
Mark H Weaver
2018-05-30 01:07:12 UTC
Permalink
Hi,
Post by Mark H Weaver
Post by Catonano
what's the problem with macroexpand-1 and syntax-case ?
In Guile 1.x, 'macroexpand-1' performed a single macro expansion step at
the top-level form of an expression, using its old non-hygienic macro
expander. There are several problems with trying to provide such an
interface in a Hygienic macro expander, and especially in the
'syntax-case' expander with its support for 'datum->syntax'. For one
thing, our modern macro expander doesn't even work with the plain
S-expressions which 'macroexpand-1' accepted and produced. It works
with "syntax objects", which effectively annotate every identifier with
extra information needed to determine which binding it references, and
also extra information needed to implement 'datum->syntax'. This in
turn requires detailed knowledge of the lexical environment in which
expansion is taking place, whereas 'macroexpand-1' provides no way for
the user to provide this information.
Mark
I have been reading this document about the scheme higienic macros
https://www.cs.indiana.edu/~dyb/pubs/bc-syntax-case.pdf
I stopped reading it when I read that the implementation relies on a
previously bootstrapped version of another macro expansion
implementation.
That's not an inherent limitation of the 'syntax-case' design. It's
merely an unfortunate attribute of the psyntax _implementation_ of
'syntax-case', apparently because they didn't care enough about
bootstrapping issues to write psyntax without the benefit of macros.

'syntax-case' could certainly be implemented without using a
pre-existing macro expander.
Post by Mark H Weaver
But Racket has some facilities to step and debug macros, as you can
see here https://docs.racket-lang.org/macro-debugger/index.html
Aren' t Racket macros higienyc ?
Yes, of course, and we could certainly implement similar macro stepping
facilities in Guile. But that's not what you asked about in your
previous message. You asked about 'macroexpand-1', and my answer was
specifically about that. I don't see any procedure similar to
'macroexpand-1' in the document you referenced above.
Post by Mark H Weaver
In this question I've been promptly suggested a quick solution to
perform a single macro expansion step
https://stackoverflow.com/questions/50073207/macro-expansion-in-guile-scheme/50515880#50515880
For posterity, here's the quick solution suggested in the link above:

(define-syntax (expand1 stx)
(syntax-case stx ()
[(_expand1 form)
(syntax-case #'form ()
[(id . more)
(identifier? #'id)
(let ([transformer (syntax-local-value #'id)])
(with-syntax ([expansion (transformer #'form)])
#''expansion))]
[_
#''form])]))

This is just a toy, and not very useful in practice.
Here's the equivalent formulation for Guile:

(use-modules (system syntax)
(srfi srfi-11))

(define (syntax-local-value id)
(let-values (((type value) (syntax-local-binding id)))
value))

(define-syntax expand1
(lambda (stx)
(syntax-case stx ()
[(_expand1 form)
(syntax-case #'form ()
[(id . more)
(identifier? #'id)
(let ([transformer (syntax-local-value #'id)])
(with-syntax ([expansion (transformer #'form)])
#''expansion))]
[_
#''form])])))

(I usually prefer to avoid using square brackets in this way, but for
sake of comparison, I used them in the definition of 'expand1' above.)

Anyway, it works the same way as in Racket for this simple example:

scheme@(guile-user)> (expand1 (or 1 2 3))
$2 = (let ((t 1)) (if t t (or 2 3)))

So, what's the problem? The first problem is that when quoting the
resulting expansion, the binding information associated with identifiers
in the syntax objects are lost, so hygiene is lost. For example:

scheme@(guile-user)> (expand1 (or 1 2 t))
$3 = (let ((t 1)) (if t t (or 2 t)))

Moving on, let's use this to try to investigate how 'define-record-type'
works from SRFI-9 in Guile:

scheme@(guile-user)> ,use (srfi srfi-9)
scheme@(guile-user)> (expand1 (define-record-type <box>
(box value)
box?
(value unbox set-box!)))
$4 = (%define-record-type #f (define-record-type <box> (box value) box? (value unbox set-box!)) <box> (box value) box? (value unbox set-box!))
scheme@(guile-user)> (expand1 (%define-record-type #f (define-record-type <box> (box value) box? (value unbox set-box!)) <box> (box value) box? (value unbox set-box!)))
While compiling expression:
Wrong type to apply: (%define-record-type guile-user)
scheme@(guile-user)>

So what went wrong here? The problem is that '%define-record-type' is a
private macro, used internally within (srfi srfi-9), and therefore not
bound in the (guile-user) module where I'm working. If we had been
working with syntax objects, each identifier within the expression would
have been annotated with the specific binding that it refers to, but as
I noted above, that information has been stripped.

The awkward error message is because this toy implementation doesn't
check if the identifier is a macro or not.

One way we could try to improve this is to write 'expandN', which
performs N macro expansion steps, keeping them as syntax objects during
the intermediate steps:

(use-modules (system syntax)
(srfi srfi-11))

(define (syntax-local-type id)
(let-values (((type value) (syntax-local-binding id)))
type))

(define (syntax-local-value id)
(let-values (((type value) (syntax-local-binding id)))
value))

(define-syntax expandN
(lambda (stx)
(syntax-case stx ()
((_expandN n form)
(let ((n (syntax->datum #'n)))
(and (number? n) (integer? n)))
(let ((n (syntax->datum #'n)))
(if (positive? n)
(syntax-case #'form ()
((id . _)
(and (identifier? #'id)
(eq? 'macro (syntax-local-type #'id)))
(let ((transformer (syntax-local-value #'id)))
(with-syntax ((expansion (transformer #'form))
(n-1 (datum->syntax #'id (- n 1))))
#'(expandN n-1 expansion))))
(_
#''form))
#''form))))))

Unfortunately, this is not quite right, because it fails to add "marks"
to the identifiers introduced by the macro transformers, and thus is not
fully hygienic, and variable capture may occur. However, it is better
than what we had before, and good enough to step further into
'define-record-type':

--8<---------------cut here---------------start------------->8---
scheme@(guile-user)> ,pp (expandN 0 (define-record-type <box>
(box value)
box?
(value unbox set-box!)))
$2 = (define-record-type
<box>
(box value)
box?
(value unbox set-box!))
scheme@(guile-user)> ,pp (expandN 1 (define-record-type <box>
(box value)
box?
(value unbox set-box!)))
$3 = (%define-record-type
#f
(define-record-type
<box>
(box value)
box?
(value unbox set-box!))
<box>
(box value)
box?
(value unbox set-box!))
scheme@(guile-user)> ,pp (expandN 2 (define-record-type <box>
(box value)
box?
(value unbox set-box!)))
$4 = (begin
(define-inlinable
(box value)
(let ((s (allocate-struct <box> 1)))
(struct-set! s 0 value)
s))
(define <box>
(let ((rtd (make-struct/no-tail
record-type-vtable
'pw
default-record-printer
'<box>
'(value))))
(set-struct-vtable-name! rtd '<box>)
(struct-set! rtd (+ 2 vtable-offset-user) box)
rtd))
(define-inlinable
(box? obj)
(and (struct? obj)
(eq? (struct-vtable obj) <box>)))
(define-tagged-inlinable
((%%type <box>)
(%%index 0)
(%%copier %%<box>-set-fields))
(unbox s)
(if (eq? (struct-vtable s) <box>)
(struct-ref s 0)
(throw-bad-struct s 'unbox)))
(define-syntax-rule
(%%<box>-set-fields check? s (getter expr) ...)
(%%set-fields
<box>
(unbox)
check?
s
(getter expr)
...))
(define-inlinable
(set-box! s val)
(if (eq? (struct-vtable s) <box>)
(struct-set! s 0 val)
(throw-bad-struct s 'set-box!))))
scheme@(guile-user)>
--8<---------------cut here---------------end--------------->8---

Unfortunately this is as far as we can go with 'expandN', because it
only expands macros at the top-level of the expression. In this case,
the top-level expression is a 'begin' form, which is a core form. At
this point, a real macro expander descends into the core form and
expands subexpressions, but in order to do this properly, it needs to
understand the meanings of the core forms that it's descending into.

For example, when descending into a 'let' form, it needs to take note of
the variables that are bound by the 'let'. For example:

scheme@(guile-user)> (expandN 0 (or 1 2 3))
$2 = (or 1 2 3)
scheme@(guile-user)> (expandN 1 (or 1 2 3))
$3 = (let ((t 1)) (if t t (or 2 3)))
scheme@(guile-user)> (expandN 2 (or 1 2 3))
$4 = (let ((t 1)) (if t t (or 2 3)))

The last two outputs are the same because I made 'expandN' just smart
enough to notice that 'let' is not a macro, in which case it stops
gracefully without triggering an exception.

Hopefully this illustrates why the old 'macroexpand-1' procedure from
Guile 1.x, which works on plain S-expressions without extra binding
information, and which only expands macros at the top level of the
expression, cannot be usefully implemented on a modern hygienic macro
expander.

However, what certainly *could* be done is some kind of interactive tool
to incrementally step macro expansions, while keeping track of the
syntax objects behind the scenes. To be useful in realistic cases, it
would need to understand most if not all of the core forms in Guile.
Those core forms are the ones defined using 'global-extend' in
psyntax.scm.

Regards,
Mark
Catonano
2018-05-31 08:21:19 UTC
Permalink
Mark,

thank you very much for explaining at lenght, I appreciate that !
Post by Mark H Weaver
Hi,
Post by Mark H Weaver
Post by Catonano
what's the problem with macroexpand-1 and syntax-case ?
In Guile 1.x, 'macroexpand-1' performed a single macro expansion step at
the top-level form of an expression, using its old non-hygienic macro
expander. There are several problems with trying to provide such an
interface in a Hygienic macro expander, and especially in the
'syntax-case' expander with its support for 'datum->syntax'. For one
thing, our modern macro expander doesn't even work with the plain
S-expressions which 'macroexpand-1' accepted and produced. It works
with "syntax objects", which effectively annotate every identifier with
extra information needed to determine which binding it references, and
also extra information needed to implement 'datum->syntax'. This in
turn requires detailed knowledge of the lexical environment in which
expansion is taking place, whereas 'macroexpand-1' provides no way for
the user to provide this information.
Mark
I have been reading this document about the scheme higienic macros
https://www.cs.indiana.edu/~dyb/pubs/bc-syntax-case.pdf
I stopped reading it when I read that the implementation relies on a
previously bootstrapped version of another macro expansion
implementation.
That's not an inherent limitation of the 'syntax-case' design. It's
merely an unfortunate attribute of the psyntax _implementation_ of
'syntax-case', apparently because they didn't care enough about
bootstrapping issues to write psyntax without the benefit of macros.
'syntax-case' could certainly be implemented without using a
pre-existing macro expander.
Post by Mark H Weaver
But Racket has some facilities to step and debug macros, as you can
see here https://docs.racket-lang.org/macro-debugger/index.html
Aren' t Racket macros higienyc ?
Yes, of course, and we could certainly implement similar macro stepping
facilities in Guile. But that's not what you asked about in your
previous message. You asked about 'macroexpand-1', and my answer was
specifically about that. I don't see any procedure similar to
'macroexpand-1' in the document you referenced above.
My bad

I assumed that macroexpand-1 was the building block for Racket macro
stepping and inspecting tools

I' m interested in macro stepping and inspecting facilities, not in
macroexpand-1 per se
Post by Mark H Weaver
Post by Mark H Weaver
In this question I've been promptly suggested a quick solution to
perform a single macro expansion step
https://stackoverflow.com/questions/50073207/macro-
expansion-in-guile-scheme/50515880#50515880
(define-syntax (expand1 stx)
(syntax-case stx ()
[(_expand1 form)
(syntax-case #'form ()
[(id . more)
(identifier? #'id)
(let ([transformer (syntax-local-value #'id)])
(with-syntax ([expansion (transformer #'form)])
#''expansion))]
[_
#''form])]))
This is just a toy, and not very useful in practice.
(use-modules (system syntax)
(srfi srfi-11))
(define (syntax-local-value id)
(let-values (((type value) (syntax-local-binding id)))
value))
(define-syntax expand1
(lambda (stx)
(syntax-case stx ()
[(_expand1 form)
(syntax-case #'form ()
[(id . more)
(identifier? #'id)
(let ([transformer (syntax-local-value #'id)])
(with-syntax ([expansion (transformer #'form)])
#''expansion))]
[_
#''form])])))
(I usually prefer to avoid using square brackets in this way, but for
sake of comparison, I used them in the definition of 'expand1' above.)
$2 = (let ((t 1)) (if t t (or 2 3)))
This is surprising to me

When I saw that example made in Racket for the first time I instantly
identified "syntax-local-value" as problematic

Will Guile have anything equivalent ? I asked myself

Now you show me the "(system syntax)" namespace (or module)

I didn't suspect it existed

Does the manual mention it anywhere ? I didn' t see it

Or maybe does it belong to any scheme standard ?

Do any more (system ....) namespaces exist ?

How would I know ?
Post by Mark H Weaver
So, what's the problem? The first problem is that when quoting the
resulting expansion, the binding information associated with identifiers
$3 = (let ((t 1)) (if t t (or 2 t)))
Moving on, let's use this to try to investigate how 'define-record-type'
(box value)
box?
(value unbox set-box!)))
$4 = (%define-record-type #f (define-record-type <box> (box value) box?
(value unbox set-box!)) <box> (box value) box? (value unbox set-box!))
(define-record-type <box> (box value) box? (value unbox set-box!)) <box>
(box value) box? (value unbox set-box!)))
Wrong type to apply: (%define-record-type guile-user)
So what went wrong here? The problem is that '%define-record-type' is a
private macro, used internally within (srfi srfi-9), and therefore not
bound in the (guile-user) module where I'm working. If we had been
working with syntax objects, each identifier within the expression would
have been annotated with the specific binding that it refers to, but as
I noted above, that information has been stripped.
The awkward error message is because this toy implementation doesn't
check if the identifier is a macro or not.
One way we could try to improve this is to write 'expandN', which
performs N macro expansion steps, keeping them as syntax objects during
(use-modules (system syntax)
(srfi srfi-11))
(define (syntax-local-type id)
(let-values (((type value) (syntax-local-binding id)))
type))
(define (syntax-local-value id)
(let-values (((type value) (syntax-local-binding id)))
value))
(define-syntax expandN
(lambda (stx)
(syntax-case stx ()
((_expandN n form)
(let ((n (syntax->datum #'n)))
(and (number? n) (integer? n)))
(let ((n (syntax->datum #'n)))
(if (positive? n)
(syntax-case #'form ()
((id . _)
(and (identifier? #'id)
(eq? 'macro (syntax-local-type #'id)))
(let ((transformer (syntax-local-value #'id)))
(with-syntax ((expansion (transformer #'form))
(n-1 (datum->syntax #'id (- n 1))))
#'(expandN n-1 expansion))))
(_
#''form))
#''form))))))
Unfortunately, this is not quite right, because it fails to add "marks"
to the identifiers introduced by the macro transformers, and thus is not
fully hygienic, and variable capture may occur. However, it is better
than what we had before, and good enough to step further into
--8<---------------cut here---------------start------------->8---
(box value)
box?
(value unbox set-box!)))
$2 = (define-record-type
<box>
(box value)
box?
(value unbox set-box!))
(box value)
box?
(value unbox set-box!)))
$3 = (%define-record-type
#f
(define-record-type
<box>
(box value)
box?
(value unbox set-box!))
<box>
(box value)
box?
(value unbox set-box!))
(box value)
box?
(value unbox set-box!)))
$4 = (begin
(define-inlinable
(box value)
(let ((s (allocate-struct <box> 1)))
(struct-set! s 0 value)
s))
(define <box>
(let ((rtd (make-struct/no-tail
record-type-vtable
'pw
default-record-printer
'<box>
'(value))))
(set-struct-vtable-name! rtd '<box>)
(struct-set! rtd (+ 2 vtable-offset-user) box)
rtd))
(define-inlinable
(box? obj)
(and (struct? obj)
(eq? (struct-vtable obj) <box>)))
(define-tagged-inlinable
((%%type <box>)
(%%index 0)
(%%copier %%<box>-set-fields))
(unbox s)
(if (eq? (struct-vtable s) <box>)
(struct-ref s 0)
(throw-bad-struct s 'unbox)))
(define-syntax-rule
(%%<box>-set-fields check? s (getter expr) ...)
(%%set-fields
<box>
(unbox)
check?
s
(getter expr)
...))
(define-inlinable
(set-box! s val)
(if (eq? (struct-vtable s) <box>)
(struct-set! s 0 val)
(throw-bad-struct s 'set-box!))))
--8<---------------cut here---------------end--------------->8---
Unfortunately this is as far as we can go with 'expandN', because it
only expands macros at the top-level of the expression. In this case,
the top-level expression is a 'begin' form, which is a core form. At
this point, a real macro expander descends into the core form and
expands subexpressions, but in order to do this properly, it needs to
understand the meanings of the core forms that it's descending into.
For example, when descending into a 'let' form, it needs to take note of
$2 = (or 1 2 3)
$3 = (let ((t 1)) (if t t (or 2 3)))
$4 = (let ((t 1)) (if t t (or 2 3)))
The last two outputs are the same because I made 'expandN' just smart
enough to notice that 'let' is not a macro, in which case it stops
gracefully without triggering an exception.
Hopefully this illustrates why the old 'macroexpand-1' procedure from
Guile 1.x, which works on plain S-expressions without extra binding
information, and which only expands macros at the top level of the
expression, cannot be usefully implemented on a modern hygienic macro
expander.
However, what certainly *could* be done is some kind of interactive tool
to incrementally step macro expansions, while keeping track of the
syntax objects behind the scenes. To be useful in realistic cases, it
would need to understand most if not all of the core forms in Guile.
Those core forms are the ones defined using 'global-extend' in
psyntax.scm.
Regards,
Mark
I had read about those bindings and marks in the paper but I had no clear
idea about what they were

Now I have a way better idea

Also, now I understand what would be required in order to implement some
macro stepping and inspecting facilities similar to those available in
Racket

I don' t know if I will actually try to implement anything, but I will
certainly use these notions to my advantage in working with Guile/Guix

So thanks
Catonano
2018-06-05 07:11:28 UTC
Permalink
Mark,

thanks again for your remarks
You're right, it is problematic, and it's good that you noticed that.
It exposes internal details of Guile's implementation, which is quite
likely to change in the future. Do not use this interface if you can
avoid it, and expect code that uses it to break in future versions of
Guile. That said, it can be useful for writing things like macro
steppers.
Well, if it's gonna change, than a macro stepper that relies on that is on
fragile ground, isn't it ?

Why do you say that it's likely going to change ?

Because the internal implementation is going to change ?

Has it ever changed alrready, in the past ?

I think a macro stepper is a fundamental tool. Macros are a unique feature
(as far as I understand) of lispy languages and there are some notable
libraries implemented as macros

The ability to step through them would allow for tinkerers to explore

I suppose this is exactly the reason why Racket has its macro stepping and
inspecting tools

I'm a bit surprised that such a fundamental functionality seems to be an
afterthought in Guile
Will Guile have anything equivalent ? I asked myself
Post by Catonano
Now you show me the "(system syntax)" namespace (or module)
I didn't suspect it existed
Does the manual mention it anywhere ? I didn' t see it
Do you know how to search the manual or its index? Press 'i' from
either the Emacs or standalone info browsers to search the index, where
you can find 'syntax-local-binding'.
But how would I have known that I should have looed for
"syntax-local-binding" ?

And why not any other name ?

I mean, the manual doesn't even mention macro stepping

You can also search the entire manual text by pressing 's'. You can
find (system syntax) that way.
Ok

But again: how would have I guessed that the namespace containing stuff I
was interested in was named (system syntax)" and not in any oher way ?

Of course I can search through a manual
Post by Catonano
Or maybe does it belong to any scheme standard ?
No, certainly not.
Post by Catonano
Do any more (system ....) namespaces exist ?
How would I know ?
Look in the "module" subdirectory of the Guile source tree for modules
that come with Guile itself, or more generally in the directories of
%load-path after installation. The directory structure mirrors the
module namespaces. The module (foo bar baz) is found in
<DIR>/foo/bar/baz.scm, where <DIR> is a component of %load-path. For
example, (system syntax) is in <DIR>/system/syntax.scm. In the Guile
source tree, it's in module/system/syntax.scm.
Ok, I see those, now

Thanks

Loading...