-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add lexically-scoped, non-hygienic macros #239
Conversation
The only thing that I didn't understand is why compiling this file with
|
I'll see about implementing explicitly-renaming macros and syntax-case (then syntax-rules comes for free- I think there is even an implementation of it on top of syntax-case somewhere in STklos :) |
Hygienic macros are not my area of expertise, but please compare this PR with the catalog of existing Scheme macro systems in SRFI 211. In particular, I recall Marc said that "the low-level macro facility of the R4RS" can be used as primitives on top of which every hygienic macro system can be built. My impression is that in Scheme |
Hello @lassik !
Yes, I have read it, I like that SRFI a lot (while researching what options to consider, I also read the HOPL paper by Will Clinger and Mitchell Wand). With what this PR offers, we'd have explicitly renaming macros and syntax-rules easily (I'm sure about that); and also at least a great portion of syntax-case (maybe all of it with some extra changes to the compiler - but I didn't work on that yet, so not completely sure). It may be possible to add the R4RS macro system also, but I didn't want to add anything too disruptive for the moment. As to |
Hello @jpellegrini, I have just added some comment in PR #55, hoping that it will help you for the aspects of loading compiled files. Unfortunately, I will not be able to have a look at this PR before several days. But I'm really looking forward to working on it. |
They are universally visible if you do not compile their definition file. If you compile a file and do not export it, it is not visible. This point is really not terrible, since it introduces a difference between a compiled and a source file. Having a coherent behaviour would be really great.! |
Well, having macros in modules also paves the way for having an R7RS-compliant library system :) |
Hi @egallesio - I'll get these new macro ideas implemented, but it will take a while, because I'm considering different possibilities do as to not be too intrusive in the STklos compiler. So I suppose version 1.70 would be released without this. |
Hi @jpellegrini, I don't really know when I'll release 1.70 (and what it should contain, even, if we have probably enough, or nearly enough, stuff to make it now). Don't worry about that, work at your own pace. After all, we don't have a deadline fixed by the marketing team 😉 |
c1d4896
to
bd4a24e
Compare
I'll try to get back to this now... |
Two mechanisms are added: * `%define-syntax`, which defines global macros that are only visible in the module where they are created; * `%let-syntax`, which defines local macros. This commit doe NOT yet allow one to use internal defines as if they were `%let-syntax`. This would probably require us to change the definition of `%define-syntax` from a macro (it is defined with `define-macro`) to a compiler special form. Not hard, but not yet done. The `compile` function is wrapped in a call to `%%macroexpand`, so macros -- local and global -- are expanded before anything happens. The changes to the compiler were kept to the minimum required. There are some tests included. Below is a more detailed description of the changes. ENVIRONMENT: The representation of the environment by the compiler has been augmented: it used to be a list of lists of symbols, where each inner list represented one level of lexical scope. It is now essentially the same, except that instead of a symbol, we may also store a pair there: - `x` (a symbol) means `x` is bound in the lexical level - `(x . obj)` (a pair) means `x` is bound to a lexically-scoped macro, and `obj` is a `<syntax>` structure. So we make symbol-in-env? return elements, and rename it. Symbol-in-env? (which returned either #t or #f) now is a procedure that returns: - #f - the symbol searched (if it's not a macro) - a pair containing a macro name and its expander A question that could arise is: why not represent all identifiers as pairs? Answering: that would require several changes to the compiler. A less invasive approach was used: wherever (symbol-in-env? ...) was used, the new function works without any further adaptation on the compiler. GLOBAL MACROS A global macro needs to be `DEFINE`d first, so it will have been computed and can be stored in a global variable. So its value will be a `<syntax>` structure, containing the expander. When the compiler finds a global variable of this type, it just expands and compiles the expanded form. Scheme implementation usually do not allow `SET!`-ing a variable that is bound to a macro transformer, but it may be interesting to be able to change the transformer of a macro. When the compiler finds a global macro being used, ``` (define-syntax f ...) ... (f ...) <= f is bound to a <syntax> structure! ``` then it will call `f`'s expander on that form and call itself recursively. The new macros are stored *as ordinary variables*, so they belong to modules, and can be exported as module symbols. LOCAL MACROS A local macro needs to have its identifier inserted in the same stack structure that holds the names of variables, so lexical scope works as intended (identifiers for variables -- used at execution time -- and macros -- expanded at compile time -- may shadow each other). Also, local macros defined in outermost code should be available for use in macro code in innermost code: ``` (define-syntax h ...) (let-syntax ((g ...)) ... (let-syntax ((f ... ,(g ...) ...)) <= f may refer to g and h ``` The local macro name is kept on the environment stack by the compiler as a pair (not a symbol), so that it's clear it's a macro. The pair is `(name macro-obj)`, where macro-obj is an instance of `<syntax>` When the compiler finds a reference to an identifier, - if it is stored as a symbol, compile reference-to-variable - if it is stored as a pair, run the expander (which is stored along with the macro name), then recursively run the compiler. HOW EXPANDERS ARE COMPILED A macro expander should use the environment in which it was defined. So, `compiled-expander` should be compiled as ``` (eval expander module) ``` When `f` makes a reference to a macro not in its local environment, then the global environment should be used. But since we have modules, the global environment is not unique, so the expander is compiled in the module where the macro was defined. References to other local macros are dealt with when expanding: the compiler expands the `car` of a form and starts over, with the same environment. If a global macro has been defined, then it is available as the value of a global variable, _in a module_. So, since a local macro must have access to that global macro, we computed the expander calling `eval` in it, *on that module* (as mentioned before). Caveat: it is possible to change the content of the global macro after the local macro has been defined, but before it has been used. HYGIENE/RENAMING We _DO NOT_ yet deal with those issues here. They can be dealt with separatelty later, maybe with the help of an `ALIAS` compiler macro. REMAINING ISSUES This doesn't seem to work for compiled files. If we compile a file that creates and then uses a macro, STklos fails to expand it.
Hello @jpellegrini, I have worked exclusively on macros for the last three weeks and I have now macros that are no more globally exported. That means that a file can define a macro and export it or not. I had a lot of problems to be able to bootstrap the system. However, it is seems to be OK now. Macros can be compiled and importing a file, will eventually rebuild only the macros of the For the moment, macros are global only, but I can use pieces of your PR to integrate local macros. The problem, is that environments are no more lists, buts structs (I used this to bootstrap the system; this is no more necessary now). Going back to the list representation is possible, and probably suitable, but necessitates another bootstrap (easier this time). As soon as I have something stable, I'll commit it. |
Wow, that's great news! I'm sorry I couldn't help more. I'm still somewhat slow but I plan to get back to contributing to STklos. |
bd4a24e
to
3bfc1e4
Compare
Tests are are an adaptation of the tests from @jpellegrini PR #239
Hi @jpellegrini, I have finally committed anew implementation for macros. Macros can be local to a module and may or may-note exported. We also have local macros, similar to the one you implemented. The implementation is quite different of the one you have to allow the production of In fact, the compiler knows now the module it is compiling with All that stuff seems to be correct for global macros. For local macros, I have still a problem (and I have spent several days on it without success). For instance, (let ((x 'outer))
(%let-syntax ((m (lambda () `x)))
(let ((x 'inner))
(cons x (m))))) produces (let ((x 'outer))
(let-syntax ((m (syntax-rules () ((a) x))))
(let ((x 'inner))
(cons x (m))))) BTW, I have implemented a simple and quite direct |
Hi @egallesio ! |
Hi @egallesio ! stklos> (define define print)
;; define
stklos> define ;; Ok, redefined the symbol
#[closure print]
stklos> (define 1 2 3) ;; Why does this one fail?
**** Error:
error: define: bad definition
(type ",help" for more information)
stklos> (define-macro (define . args) `(print . ,args))
;; define
stklos> (define 1 2 3)
123 ;; Ok in CAR of form (application position)
stklos> define
#[syntax define] ;; Ok, as standalone symbol
stklos> (write define)
#[syntax define]stklos> ;; Ok, as argument to procedure So... Why did |
Ah yes -- the macro transformer should run with the environment in which it was defined if we want to have proper lexical scope (so the |
Hi @egallesio ! This is (define (compile-%let-syntax e env tail?)
(let ((len (length e)))
...
...
...
(let* ((name (car new))
(expander (cadr new))
(new-macro (%make-syntax
name
expander
(eval expander)
#f))) ; #f => non global macro So using I thought that the expander could hold a reference to the scope where it was defined inside the |
Tests are are an adaptation of the tests from @jpellegrini PR egallesio#239
@egallesio as far as I can see you have already implemented lexical scope for macros -- and it's really cool that it works for |
That's OK for me to close it. BTW, I have some ideas to help to circumvent the existent troubles, but I need to work a bit before being able to try. |
I'll open an issue specifically for that... |
Hi @egallesio !
This implements global and local lexically-scoped macros for STklos (the global macros are kept in modules and are used in conjunction with the local ones, it was easier to do this than mix with the already existent macro system).
This does not change
define-macro
. It's a whole new machinery added.Not ready yet - I thought I had fixed loading of files, but it still doesn't work (macros are not expanded when a file is being loaded, don't know why).
I'm sending the PR so you can take a look.
I'll keep working on it.
Two mechanisms are added:
%define-syntax
, which defines global macros that areonly visible in the module where they are created;
%let-syntax
, which defines local macros.This commit doe NOT yet allow one to use internal defines as
if they were
%let-syntax
. This would probably require us tochange the definition of
%define-syntax
from a macro (it isdefined with
define-macro
) to a compiler special form. Nothard, but not yet done.
The
compile
function is wrapped in a call to%%macroexpand
,so macros -- local and global -- are expanded before anything
happens.
The changes to the compiler were kept to the minimum required.
There are some tests included.
Below is a more detailed description of the changes.
ENVIRONMENT:
The representation of the environment by the compiler has been
augmented: it used to be a list of lists of symbols, where each
inner list represented one level of lexical scope.
It is now essentially the same, except that instead of a symbol,
we may also store a pair there:
x
(a symbol) meansx
is bound in the lexical level(x . obj)
(a pair) meansx
is bound to a lexically-scopedmacro, and
obj
is a<syntax>
structure.So we make symbol-in-env? return elements, and rename it.
Symbol-in-env? (which returned either #t or #f) now is a
procedure that returns:
A question that could arise is: why not represent all identifiers as pairs?
Answering: that would require several changes to the compiler. A less invasive
approach was used: wherever (symbol-in-env? ...) was used, the
new function works without any further adaptation on the compiler.
GLOBAL MACROS
A global macro needs to be
DEFINE
d first, so it will have beencomputed and can be stored in a global variable. So its value will
be a
<syntax>
structure, containing the expander.When the compiler finds a global variable of this type, it just
expands and compiles the expanded form.
Scheme implementation usually do not allow
SET!
-ing a variable thatis bound to a macro transformer, but it may be interesting to be
able to change the transformer of a macro.
When the compiler finds a global macro being used,
then it will call
f
's expander on that form and call itselfrecursively.
The new macros are stored as ordinary variables, so they belong to
modules, and can be exported as module symbols.
LOCAL MACROS
A local macro needs to have its identifier inserted in the same
stack structure that holds the names of variables, so lexical
scope works as intended (identifiers for variables -- used at execution
time -- and macros -- expanded at compile time -- may shadow each other).
Also, local macros defined in outermost code should be available for use
in macro code in innermost code:
The local macro name is kept on the environment stack by the compiler
as a pair (not a symbol), so that it's clear it's a macro. The pair
is
(name macro-obj)
, where macro-obj is an instance of<syntax>
When the compiler finds a reference to an identifier,
along with the macro name), then recursively run the compiler.
HOW EXPANDERS ARE COMPILED
A macro expander should use the environment in which it was defined.
So,
compiled-expander
should be compiled asWhen
f
makes a reference to a macro not in its local environment,then the global environment should be used. But since we have modules,
the global environment is not unique, so the expander is compiled in
the module where the macro was defined.
References to other local macros are dealt with when expanding: the
compiler expands the
car
of a form and starts over, with the same environment.If a global macro has been defined, then it is available as the value
of a global variable, in a module. So, since a local macro must
have access to that global macro, we computed the expander calling
eval
in it, on that module (as mentioned before).Caveat: it is possible to change the content of the global macro
after the local macro has been defined, but before it has been
used.
HYGIENE/RENAMING
We DO NOT yet deal with those issues here. They can be dealt with
separatelty later, maybe with the help of an
ALIAS
compiler macro.REMAINING ISSUES
This doesn't seem to work for compiled files. If we compile a
file that creates and then uses a macro, STklos fails to expand it.