/
macros.jl
162 lines (136 loc) · 3.86 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
function substitute_underscores!(underscores_to_gensyms, meta_level, other)
other
end
function substitute_underscores!(underscores_to_gensyms, meta_level, maybe_argument::Symbol)
if meta_level < 1 && all(isequal('_'), string(maybe_argument))
if !haskey(underscores_to_gensyms, maybe_argument)
underscores_to_gensyms[maybe_argument] = gensym(maybe_argument)
end
underscores_to_gensyms[maybe_argument]
else
maybe_argument
end
end
function substitute_underscores!(underscores_to_gensyms, meta_level, code::Expr)
# have to do this the old fashioned way because _ has a special meaning in MacroTools
expanded_code = if code.head === :macrocall && length(code.args) === 3
name, location, body = code.args
if name === Symbol("@_")
anonymous(location, body)
elseif name === Symbol("@>")
make_chain(location, body)
else
code
end
else
code
end
head = expanded_code.head
new_meta_level = if head == :quote
meta_level + 1
elseif head == :$
meta_level - 1
else
meta_level
end
Expr(
head,
(
substitute_underscores!(underscores_to_gensyms, new_meta_level, code) for
code in expanded_code.args
)...,
)
end
function anonymous(location, other)
other
end
function anonymous(location, body::Expr)
underscores_to_gensyms = Dict{Symbol,Symbol}()
meta_level = 0
substituted_body = substitute_underscores!(underscores_to_gensyms, meta_level, body)
if length(underscores_to_gensyms) == 0
body
else
Expr(
:function,
Expr(
:call,
gensym("@_"),
Iterators.map(value, sort!(collect(underscores_to_gensyms), by = first))...,
),
Expr(:block, location, substituted_body),
)
end
end
"""
macro _(body)
Terser function syntax. The arguments are inside the `body`; the first argument is `_`, the second argument is `__`, etc. Will `@inline`.
```jldoctest anonymous
julia> using LightQuery
julia> using Test: @inferred
julia> @inferred (@_ _ + 1)(1)
2
julia> @inferred map((@_ __ - _), (1, 2), (2, 1))
(1, -1)
```
If there are no `_` arguments, read as is.
```jldoctest anonymous
julia> (@_ x -> x + 1)(1)
2
```
"""
macro _(body)
anonymous(__source__, body) |> esc
end
export @_
function link(location, object, call::Expr)
underscores_to_gensyms = Dict{Symbol,Symbol}()
meta_level = 0
body = substitute_underscores!(underscores_to_gensyms, meta_level, call)
if length(underscores_to_gensyms) == 0
Expr(:block, location, Expr(:call, call, object))
elseif length(underscores_to_gensyms) == 1 && haskey(underscores_to_gensyms, :_)
Expr(:let, Expr(:(=), underscores_to_gensyms[:_], object), Expr(:block, location, body))
else
throw(ArgumentError("Chain segments must contain single underscores only"))
end
end
function link(location, object, call)
Expr(:call, call, object)
end
function make_chain(location, maybe_chain)
if @capture maybe_chain object_ |> call_
link(location, make_chain(location, object), call)
else
maybe_chain
end
end
"""
macro >(body)
If body is in the form `object_ |> call_`, call [`@_`](@ref) on `call`, and recur on `object`.
```jldoctest chain
julia> using LightQuery
julia> @> 0 |> _ - 1 |> abs |> Base.abs
1
```
You can't include multiple arguments.
```jldoctest chain
julia> @> _ |> _ + __
ERROR: LoadError: ArgumentError: Chain segments must contain single underscores only
[...]
```
You can nest chains:
```jldoctest chain
julia> @> 1 |> (@> _ + 1 |> _ + 1)
3
```
Handles interpolations seamlessly:
```jldoctest chain
julia> @> 1 |> :(_ + \$_)
:(_ + 1)
```
"""
macro >(body)
make_chain(__source__, body) |> esc
end
export @>