1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
# Does the decoder implement macros?
I've written about the [parser/decoder dualism](reader.html) in a
previous article. Long story short, the parser takes care of syntax
sugar, like turning `#(...)` into `(#HASH ...)`, and the decoder takes
care of turning that into a vector or whatever.
Now, since the job of the decoder seems superficially quite similar to
that of a macro expander, I've been agonizing for the past two days or
so whether it *is* the macro expander.
(Warning: This post is probably going to be very rambly, as I'm trying
to gather my thoughts by writing it.)
On one hand, sure:
(define-syntax #HASH
(syntax-rules ()
(#HASH <element> ...)
(vector '<element> ...)))
Or something like that. You know what I mean? I mean, in Scheme you
can't return a vector from a macro, but in Zisp the idea is that you
can very well do that if you want, because why not.
It's very much possible that I will eventually realize that this is a
bad idea in some way, but we'll see. So far I really like the idea of
a macro just returning objects, like a procedure, rather than having
to return a syntax object that has a binding to that procedure.
This may be similar to John Shutt's "vau calculus" from his language
Kernel. Maybe Zisp will even end up being an implementation of the
vau calculus. But I don't know; I've never fully grokked the vau
calculus, so if I end up implementing it, it will be by accident.
In any case, I want the user to be able to bind transformers to runes,
and doing so feels like it's pretty much the same thing as defining a
macro, so maybe the decoder should also be the macro expander.
But then there's an issue with quoting. Consider the following:
(define stuff '(foo #(0 1 2)))
In Zisp, this would first of all be parsed into:
(define stuff (#QUOTE foo (#HASH 0 1 2)))
Now, if #QUOTE didn't decode its operand, we'd end up seeing #HASH in
the result, never creating the vector we meant to create.
But if #QUOTE calls decode on its operand, and the decoder is also the
macro expander, whoops:
(let-syntax ((foo (syntax-rules () ((_ x) (bar x)))))
'(foo #(0 1 2)))
;; => (bar #(0 1 2))
I mean... MAYBE that should happen, actually?! Probably not, though.
What Scheme does isn't gospel; Zisp isn't Scheme and it will do some
things differently, but we *probably* don't want anything inside a
quoted expression to be macro expanded. Probably.
The thought that I might actually want that to happen sent me down a
whole rabbit whole, and made me question "runes" altogether. If they
just make the decoder invoke a predefined macro, well, why not ditch
runes and have the parser emit macro calls?
So instead of:
#(x y z) -> (#HASH x y z)
(Which is then "decoded" into a vector...) Why not just:
#(x y z) -> (VECTOR x y z)
And then `VECTOR` is, I don't know, a macro in the standard library I
guess. If the decoder is the macro expander, then sure, it will know
about the standard library; it will have a full-blown environment that
it uses to macro expand, to look up macro names.
But no, I think this conflates everything too much. Even just on the
level of comprehensibility of code containing literals, I think it's
good for there to be something that you just know will turn into an
object of some type, no matter what; that's what a literal is.
(In Zisp, it's not the reader that immediately turns the literal into
an object of the correct type, but the decoder still runs before the
evaluator so it's almost the same.)
Then again, maybe this intuition just comes from having worked with
Scheme for such a long time, and maybe it's not good. Perhaps it's
more elegant if everything is a macro. Don't pile feature on top of
feature, remember?
Booleans, by the way, would just be identifier syntax then. Just
`true` and `false` without the hash sign. In Zisp, you can't shadow
identifiers anyway, so now they're like keywords in other languages,
also a bit like `t` and `nil` in CL and Elisp.
IF we are fine with the quote issue described above, then I *think*
everything being a macro would be the right thing to do. Although
I've said the decoder could be used for things other than code, like
for configuration files containing user-defined data types, you could
still do that by defining macros and calling the macro expander on the
config file.
It's just that you would either not be able to have stuff like vectors
in a quoted list (you'd just get a list like `(VECTOR ...)` in it if
you tried), or you'd have to be expanding any macros encountered
within the quoted list. Either both, or neither.
Not getting a choice, you say... That's not very expressive. That
seems like a limitation in the language. Remember: remove the
limitations that make additional features seem necessary.
Next thing we will have two variants of quote: One which quotes for
real, and one that expands macros. Or maybe some mechanism to mark
macros as being meant to be run inside a quote or not, but then we
re-invented runes in a different way.
Which brings me back to runes, and how `#QUOTE` could handle them,
even if the decoder is the macro expander.
Encountering `#QUOTE` could tell the decoder that while decoding the
operand, it should only honor runes, not macros bound to identifiers.
That would probably be a fine way to solve the quote problem, should
the decoder also be the macro expander: Macros are bound to runes or
identifiers, and the rune-bound macros are those that are expanded
even inside a quote.
I think that would be the same as having completely separate decode
and macro-expand phases.
(The reason we would want them merged, by the way, is that it would
presumably prevent duplication of code, since what they do is so
similar.)
It's possible that I'm agonizing for no reason at all because maybe
the decoder cannot be the macro expander anyway.
We will see.
For now, I think it's best to proceed by implementing the decoder, and
once I've come to the macro expander I can see if it makes sense to
merge the two or not.
But I'll probably keep runes one way or another, since they're a nice
way of marking things that should be processed "no matter what" such
that they can function as object literals within code.
|