-
Notifications
You must be signed in to change notification settings - Fork 19
/
macros.jl
381 lines (293 loc) · 10.3 KB
/
macros.jl
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
## convenience macros
"""
@gap <expr>
@gap(<expr>)
Execute <expr> directly in GAP, as if `GAP.evalstr("<expr>")` was called.
This can be used for creating GAP literals directly from Julia.
# Examples
```jldoctest
julia> @gap [1,2,3]
GAP: [ 1, 2, 3 ]
julia> @gap SymmetricGroup(3)
GAP: Sym( [ 1 .. 3 ] )
julia> @gap(SymmetricGroup)(3)
GAP: Sym( [ 1 .. 3 ] )
```
Note that the last two examples have a slight syntactical, and therefore also
a semantical difference. The first one executes the string `SymmetricGroup(3)`
directly inside GAP. The second example returns the function `SymmetricGroup`
via `@gap(SymmetricGroup)`, then calls that function with the argument `3`.
Due to Julia's way of handing over arguments into the code of macros,
not all expressions representing valid GAP code can be processed.
For example, the GAP syntax of permutations consisting of more than one cycle
cause problems, as well as the GAP syntax of non-dense lists.
```
julia> @gap (1,2,3)
GAP: (1,2,3)
julia> @gap (1,2)(3,4)
ERROR: LoadError: Error thrown by GAP: Error, no method found! For debugging hints type ?Recovery from NoMethodFound
[...]
julia> @gap [ 1,, 2 ]
ERROR: syntax: unexpected \",\"
[...]
```
Note also that a string argument gets evaluated with `GAP.evalstr`.
```jldoctest
julia> @gap \"\\\"abc\\\"\"
GAP: \"abc\"
julia> @gap \"[1,,2]\"
GAP: [ 1,, 2 ]
julia> @gap \"(1,2)(3,4)\"
GAP: (1,2)(3,4)
```
"""
macro gap(str)
return evalstr(string(str))
end
export @gap
# Define a plain function that contains the code of the `@g_str` macro.
# Note that errors thrown by macros can apparently not be tested using
# `@test_throw`.
function gap_string_macro_helper(str::String)
# We assume that `str` is the input of the `@g_str` macro;
# more precisely, `str` is what arrives inside the code of the macro.
# Note that here backslashes inside `str` are literally contained in `str`,
# except for backslashes that escape doublequotes.
# In order to get the intended meaning (as stated in the GAP manual section
# "Special Characters"),
# we escape doublequotes and leave the interpretation to `evalstr`.
evl = evalstr("\"" * replace(str, "\"" => "\\\"") * "\"")
evl === nothing && error("failed to convert to GapObj:\n $str")
return evl
end
"""
@g_str
Create a GAP string by typing `g"content"`.
# Examples
```jldoctest
julia> g"foo"
GAP: "foo"
julia> g"ab\\ncd\\\"ef\\\\gh" # special characters are handled as in GAP
GAP: "ab\\ncd\\\"ef\\\\gh"
```
Due to Julia's way of handing over arguments into the code of macros,
not all strings representing valid GAP strings can be processed.
```jldoctest
julia> g"\\\\"
ERROR: LoadError: Error thrown by GAP: Syntax error: String must end with " before end of file in stream:1
[...]
```
Conversely,
there are valid arguments for the macro that are not valid Julia strings.
```jldoctest
julia> g"\\c"
GAP: "\\c"
```
"""
macro g_str(str)
return gap_string_macro_helper(str)
end
export @g_str
import MacroTools
"""
@gapwrap
When applied to a method definition that involves access to entries of
`GAP.Globals`, this macro rewrites the code such that the relevant GAP
globals are cached, and need not be fetched again and again.
# Examples
```jldoctest
julia> @gapwrap isevenint(x) = GAP.Globals.IsEvenInt(x)::Bool;
julia> isevenint(1)
false
julia> isevenint(2)
true
```
"""
macro gapwrap(ex)
# split the method definition
def_dict = try
MacroTools.splitdef(ex)
catch
error("@gapwrap must be applied to a method definition")
end
# take the body of the function
body = def_dict[:body]
# find, record and substitute all occurrences of GAP.Globals.*
symdict = IdDict{Symbol,Symbol}()
body = MacroTools.postwalk(body) do x
MacroTools.@capture(x, GAP.Globals.sym_) || return x
new_sym = get!(() -> gensym(sym), symdict, sym)
return Expr(:ref, new_sym)
end
# modify the function body
def_dict[:body] = Expr(
:block,
# first the location of the macro call
__source__,
# now the list of initializations ...
(quote
global $v
if !isassigned($v)
$v[] = GAP.Globals.$k
end
end for (k, v) in symdict)...,
# ... then the original-with-substitutions body
body,
)
# assemble the method definition again
ex = MacroTools.combinedef(def_dict)
return esc(Expr(
:block,
(:(@eval const $v = Ref{GapObj}()) for (k, v) in symdict)...,
:(Base.@__doc__ $ex),
))
end
export @gapwrap
"""
@gapattribute
This macro is intended to be applied to a method definition
for a unary function called `attr`, say,
where the argument has the type `T`, say,
the code contains exactly one call of the form `GAP.Globals.Something(X)`,
where `Something` is a GAP attribute such as `Centre` or `IsSolvableGroup`,
and `attr` returns the corresponding attribute value for its argument.
The macro defines three functions `attr`, `hasattr`, and `setattr`, where
`attr` takes an argument of type `T` and returns what the given
method definition says,
`hasattr` takes an argument of type `T` and returns the result of
`GAP.Globals.HasSomething(X)` (which is either `true` or `false`),
`setattr` takes an argument of type `T` and an object `obj` and
calls `GAP.Globals.SetSomething(X, obj)`.
In order to avoid runtime access via `GAP.Globals.Something` etc.,
the same modifications are applied in the construction of the three functions
that are applied by [`@gapwrap`](@ref).
The variables that are created by the macro belong to the Julia module
in whose scope the macro is called.
# Examples
```jldoctest
julia> @gapattribute isstrictlysortedlist(obj::GAP.GapObj) = GAP.Globals.IsSSortedList(obj)::Bool;
julia> l = GapObj([ 1, 3, 7 ]);
julia> has_isstrictlysortedlist( l )
false
julia> isstrictlysortedlist( l )
true
julia> has_isstrictlysortedlist( l )
true
julia> l = GapObj([ 1, 3, 7 ]);
julia> has_isstrictlysortedlist( l )
false
julia> set_isstrictlysortedlist( l, true )
julia> has_isstrictlysortedlist( l )
true
julia> isstrictlysortedlist( l )
true
```
"""
macro gapattribute(ex)
def_dict = try
MacroTools.splitdef(ex)
catch
error("@gapattribute must be applied to a method definition")
end
# The method must have exactly one argument.
length(def_dict[:args]) == 1 || error("the method must have exactly one argument")
# take the body of the function
body = def_dict[:body]
# Find the (unique) occurrence of GAP.Globals.<name>(<arg>),
# and record <name> and <arg>.
fun_arg = Set{Tuple{Symbol,Any}}()
MacroTools.postwalk(body) do x
MacroTools.@capture(x, GAP.Globals.sym_(arg_)) || return x
push!(fun_arg, (sym, arg))
return x
end
length(fun_arg) == 1 || error("there must be a unique call to a function in GAP.Globals")
pair = pop!(fun_arg)
gapname = string(pair[1])
gaparg = pair[2]
# Define the function names on the GAP side ...
gaptester = Symbol("Has" * gapname)
gapsetter = Symbol("Set" * gapname)
# ... and on the Julia side.
julianame = string(def_dict[:name])
juliaarg = def_dict[:args][1]
testername = Symbol("has_" * julianame)
settername = Symbol("set_" * julianame)
# assemble everything
result = quote
Base.@__doc__ @gapwrap $ex
"""
$($testername)(x)
Return `true` if the value for `$($julianame)(x)` has already been computed.
"""
@gapwrap $testername($juliaarg) = GAP.Globals.$gaptester($gaparg)::Bool
"""
$($settername)(x, v)
Set the value for `$($julianame)(x)` to `v` if it has't been
set already.
"""
@gapwrap $settername($juliaarg,v) = GAP.Globals.$gapsetter($gaparg,v)::Nothing
end
# ensure correct line numbers are used on all three methods, so that
# e.g. @less, @edit etc. work for them
Meta.replace_sourceloc!(__source__, result)
# we must prevent Julia from applying gensym to all locals, as these
# substitutions do not get applied to the quoted part of the new body,
# leading to trouble if the wrapped function has arguments (as the
# argument names will be replaced, but not their uses in the quoted part
return esc(result)
end
export @gapattribute
"""
@wrap funcdecl
When applied to a function declaration of the form `NAME(a::T)` or
`NAME(a::T)::S`, this macro generates a function which behaves equivalently to
`NAME(a::T) = GAP.Globals.NAME(a)` resp. `NAME(a::T) = GAP.Globals.NAME(a)::S`,
assuming that `GAP.Globals.NAME` references a GAP function. Function declarations
with more than one argument or zero arguments are also supported.
However, the generated function actually caches the GAP object `GAP.Globals.NAME`.
This minimizes the call overhead. So @wrap typically is used to provide an optimized
way to call certain GAP functions.
Another use case for this macro is to improve type stability of code calling into
GAP, via the type annotations for the arguments and return value contained in the
function declaration.
Be advised, though, that if the value of `GAP.Globals.NAME` is changed later on,
the function generated by this macro will not be updated, i.e., it will still
reference the original GAP object.
# Examples
```jldoctest
julia> GAP.@wrap Jacobi(x::GapInt, y::GapInt)::Int
Jacobi (generic function with 1 method)
julia> Jacobi(11,35)
1
```
"""
macro wrap(ex)
if ex.head == :(::)
length(ex.args) == 2 || error("unexpected return type annotation")
retval = ex.args[2]
ex = ex.args[1]
else
retval = :Any
end
ex.head == :call || error("unexpected input for macro @wrap")
name = ex.args[1]
newsym = gensym(name)
fullargs = ex.args[2:length(ex.args)]
# strip type annotation from arguments for use in the call to GAP
args = [x isa Symbol ? x : x.args[1] for x in fullargs]
# the "outer" part of the body
body = quote
global $newsym
if !isassigned($newsym)
$newsym[] = GAP.Globals.$name::GapObj
end
return $newsym[]($(args...))::$retval
end
# insert the correct line number
body.args[1] = __source__
return esc(quote
@eval const $newsym = Ref{GapObj}()
Base.@__doc__ $ex = $body
end)
end