Skip to content

Commit ef882fd

Browse files
committed
lecture 07 in progress
1 parent 6e10548 commit ef882fd

File tree

2 files changed

+287
-30
lines changed

2 files changed

+287
-30
lines changed

docs/src/lecture_06/lecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ julia> parsed_fib = Meta.parse(
5959
return b
6060
end)
6161
```
62-
AST is a tree representation of the source code, where the parser has already identified individual code elements function call, argument blocks, etc. The parsed code is represented by Julia objects, therefore it can be read and modified by Julia from Julia at your wish (this is what is called homo-iconicity of a language). Using `TreeView`
62+
AST is a tree representation of the source code, where the parser has already identified individual code elements function call, argument blocks, etc. The parsed code is represented by Julia objects, therefore it can be read and modified by Julia from Julia at your wish (this is what is called homo-iconicity of a language the itself being derived from Greek words *homo*- meaning "the same" and *icon* meaning "representation"). Using `TreeView`
6363
```julia
6464
using TreeView, TikzPictures
6565
g = tikz_representation(walk_tree(parsed_fib))

docs/src/lecture_07/lecture.md

Lines changed: 286 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,117 @@
11
# [Macros](@id macro_lecture)
22
What is macro?
33
In its essence, macro is a function, which
4-
1. takes as an input an expression (or more expression)
4+
1. takes as an input an expression (parsed input)
55
2. modify the expressions in argument
6-
3. evaluate the modified expression exactly once during the compile time. The expression returned from the macro is inserted at the position of the macro and evaluated. After the macro is applied there is no trace of macro being called (from the syntactic point of view).
6+
3. insert the modified expression at the same place as the one that is parsed.
77

8-
Thus, the macro can be thus viewed as a convenience for performing
9-
```
10-
ex = Meta.parse(some_source_code)
11-
eval(replace_sin(ex))
12-
```
8+
Macros are necessary because they execute when code is parsed, therefore, macros allow the programmer to generate and include fragments of customized code before the full program is run. To illustrate the difference, consider the following example:
139

14-
We can instantiate the aboce with the following example
10+
One of the very conveninet ways to write macros is to write functions modifying the `Expr`ession and then call that function in macro as
1511
```julia
1612
replace_sin(x::Symbol) = x == :sin ? :cos : x
1713
replace_sin(e::Expr) = Expr(e.head, map(replace_sin, e.args)...)
1814
replace_sin(u) = u
19-
some_source_code = "sinp1(x) = 1 + sin(x)"
2015

21-
ex = Meta.parse(some_source_code)
22-
eval(replace_sin(ex))
23-
sinp1(1) == 1 + cos(1)
24-
```
25-
which can be equally written by defining macro using keyword `macro`
26-
```
2716
macro replace_sin(ex)
28-
o = replace_sin(ex)
17+
replace_sin(esc(ex))
2918
end
30-
@replace_sin(sinp1(x) = 1 + sin(x))
31-
sinp1(1) == 1 + cos(1)
19+
20+
@replace_sin(cosp1(x) = 1 + sin(x))
21+
cosp1(1) == 1 + cos(1)
3222
```
3323
notice the following
3424
- the definition of the macro is similar to the definition of the function with the exception that instead of the keyword `function` we use keyword `macro`
3525
- when calling the macro, we signal to the compiler our intention by prepending the name of the macro with `@`.
36-
- the macro receives the expression(s) as the argument instead of the evaluated argument
37-
- when you are invoking the macro, you should be aware that the code you are entering can be arbitrarily modified and you can receive something completely different (this of course does not make sense from the functional perspective).
26+
- the macro receives the expression(s) as the argument instead of the evaluated argument and also returns an expression that is placed on the position where the macro has been called
27+
- when you are invoking the macro, you should be aware that the code you are entering can be arbitrarily modified and you can receive something completely different. This meanst that `@` should also serve as a warning that you are leaving Julia's syntax. In practice, it make sense to make things akin to how they are done in Julia or to write Domain Specific Language with syntax familiar in that domain.
28+
29+
We have mentioned above that macros are indispensible in the sense they intercept the code generation after parsing. You might object that I can achieve the above using the following combination of `Meta.parse` and `eval`
30+
```julia
31+
ex = Meta.parse("cosp1(x) = 1 + sin(x)")
32+
ex = replace_sin(ex)
33+
eval(ex)
34+
```
35+
in the following we cannot do the same trick
36+
```julia
37+
function cosp2(x)
38+
@replace_sin 2 + sin(x)
39+
end
40+
cosp2(1) (2 + cos(1))
41+
```
42+
43+
```julia
44+
function parse_eval_cosp2(x)
45+
ex = Meta.parse("2 + sin(x)")
46+
ex = replace_sin(ex)
47+
eval(ex)
48+
end
49+
50+
julia> @code_lowered parse_eval_cosp2(1)
51+
CodeInfo(
52+
1%1 = Base.getproperty(Main.Meta, :parse)
53+
│ ex = (%1)("2 + sin(x)")
54+
│ ex = Main.replace_sin(ex)
55+
%4 = Main.eval(ex)
56+
└── return %4
57+
)
58+
```
59+
60+
!!! info
61+
### Scope of eval
62+
`eval` function is always evaluated in the global scope of the `Module` in which the macro is called (note that there is that by default you operate in the `Main` module). Moreover, `eval` takes effect **after** the function has been has been executed. This can be demonstrated as
63+
```julia
64+
add1(x) = x + 1
65+
function redefine_add(x)
66+
eval(:(add1(x) = x - 1))
67+
add1(x)
68+
end
69+
julia> redefine_add(1)
70+
2
71+
72+
julia> redefine_add(1)
73+
0
74+
```
75+
76+
77+
`@macroexpand` can allow use to observe, how the macro will be expanded. We can use it for example
78+
```julia
79+
@macroexpand @replace_sin(sinp1(x) = 1 + sin(x))
80+
```
81+
82+
## What goes under the hood?
83+
Let's consider what the compiler is doing in this call
84+
```julia
85+
function cosp2(x)
86+
@replace_sin 2 + sin(x)
87+
end
88+
```
3889

90+
First, Julia parses the code into the AST as
91+
```julia
92+
ex = Meta.parse("""
93+
function cosp2(x)
94+
@replace_sin 2 + sin(x)
95+
end
96+
""") |> Base.remove_linenums!
97+
dump(ex)
98+
```
99+
We observe that there is a macrocall in the AST, which means that Julia will expand the macro and put it in place
100+
```julia
101+
ex.args[2].args[1].head # the location of the macrocall
102+
ex.args[2].args[1].args[1] # which macro to call
103+
ex.args[2].args[1].args[2] # line number
104+
ex.args[2].args[1].args[3] # on which expression
105+
```
106+
let's run the `replace_sin` and insert it back
107+
```julia
108+
ex.args[2].args[1] = replace_sin(ex.args[2].args[1].args[3])
109+
ex |> dump
110+
```
111+
now, `ex` contains the expanded macro and we can see that it correctly defines the function
112+
```julia
113+
eval(ex)
114+
```
39115
## Calling macros
40116
Macros can be called without parentheses
41117
```julia
@@ -64,21 +140,202 @@ end
64140
```
65141
(the `@showarg(1 + 1, :x) ` raises an error, since `:(:x)` is of Type `QuoteNode`).
66142

67-
<!-- We can observe the AST corresponding to the macro call using
143+
Observe that macro dispatch is based on the types of AST that are handed to the macro, not the types that the AST evaluates to at runtime.
144+
145+
## Notes on quotation
146+
In the previous lecture we have seen that we can *quote a block of code*, which tells the compiler to treat the input as an data and parse it. We have talked about three ways of quoting code.
147+
1. `:(quoted code)`
148+
2. Meta.parse(input_string)
149+
3. `quote ... end`
150+
The truth is that Julia does not do full quotation, but a *quasiquotation* is it allows you to **interpolate** expressions inside the quoted code using `$` symbol similar to the string. This is handy, as sometimes, when we want to insert into the quoted code an result of some computation / preprocessing.
151+
Observe the following difference in returned code
152+
```julia
153+
a = 5
154+
:(x = a)
155+
:(x = $(a))
156+
let y = :x
157+
:(1 + y), :(1 + $y)
158+
end
159+
```
160+
In contrast to the behavior of `:()` (or `quote ... end`, true quotation would not perform interpolation where unary `$` occurs. Instead, we would capture the syntax that describes interpolation and produce something like the following:
68161
```julia
69-
eval(Expr(:macrocall, Symbol("@showarg"), :(1 + 1)))
70-
Meta.parse("@showarg 1 + 1")
71-
``` -->
162+
(
163+
:(1 + x), # Quasiquotation
164+
Expr(:call, :+, 1, Expr(:$, :x)), # True quotation
165+
)
166+
```
72167

73-
`@macroexpand` can allow use to observe, how the macro will be expanded. We can use it for example
168+
When we need true quoting, i.e. we need something to stay quoted, we can use `QuoteNode` as
74169
```julia
75-
@macroexpand @replace_sin(sinp1(x) = 1 + sin(x))
170+
macro true_quote(e)
171+
QuoteNode(e)
172+
end
173+
let y = :x
174+
(
175+
@true_quote(1 + $y),
176+
:(1 + $y),
177+
)
178+
end
76179
```
180+
At first glance, `QuoteNode` wrapper seems to be useless. But `QuoteNode` has clear value when it's used inside a macro to indicate that something should stay quoted even after the macro finishes executing. Also notice that the expression received by macro was quoted, not quasiquoted, since in the latter case `$y` would be replaced. We can demonstate it using the `@showarg` macro introduced earlier, as
181+
```julia
182+
@showarg(1 + $x)
183+
```
184+
The error is raised after the macro was evaluated and the output has been inserted to parsed AST.
77185

186+
Macros do not know about runtime values, they only know about syntax trees. When a macro receives an expression with a $x in it, it can't interpolate the value of x into the syntax tree because it reads the syntax tree before x ever has a value! So the interpolation syntax in macros is not given any actual meaning in julia.
187+
188+
Instead, when a macro is given an expression with $ in it, it assumes you're going to give your own meaning to $x. In the case of BenchmarkTools.jl they return code that has to wait until runtime to receive the value of x and then splice that value into an expression which is evaluated and benchmarked. Nowhere in the actual body of the macro do they have access to the value of x though.
189+
190+
191+
!!! info
192+
### Why `$` for interpolation?
193+
The `$` string for interpolation was used as it identifies the interpolation inside the string and inside the command. For example
194+
```julia
195+
a = 5
196+
s = "a = $(5)"
197+
typoef(s)
198+
println(s)
199+
filename = "/tmp/test_of_interpolation"
200+
run(`touch $(filename)`)
201+
```
78202

79-
## Basics
80-
## non-standard string literals
81203
## Macro hygiene
204+
Macro hygiene is a term coined in 1986 and it says that the evaluation of the macro should not have an effect on the surrounding call. By default, all macros in Julia are hygienic which means that variables introduced in the macro are `gensym`ed to have unique names and function points to global functions.
205+
206+
Let's demonstrate it on our own version of an macro `@elapsed` which will return the time that was needed to evaluate the block of code.
207+
```julia
208+
macro tooclean_elapsed(ex)
209+
quote
210+
tstart = time()
211+
$(ex)
212+
time() - tstart
213+
end
214+
end
215+
216+
fib(n) = n <= 1 ? n : fib(n-1) + fib(n - 2)
217+
let
218+
t = @tooclean_elapsed r = fib(10)
219+
println("the evaluation of fib took ", t, "s and result is ", r)
220+
end
221+
```
222+
We see that variable `r` has not been assigned during the evaluation of macro. We have also used `let` block in orders not to define any variables in the global scope.
223+
Why is that?
224+
Let's observe how the macro was expanded
225+
```julia
226+
julia> Base.remove_linenums!(@macroexpand @tooclean_elapsed r = fib(10))
227+
quote
228+
var"#12#tstart" = Main.time()
229+
var"#13#r" = Main.fib(10)
230+
Main.time() - var"#12#tstart"
231+
end
232+
```
233+
We see that `tstart` in the macro definition was replaced by `var"#12#tstart"`, which is a name generated by Julia's gensym to prevent conflict. The same happens to `r`, which was replaced by `var"#13#r"`. Notice that in the case of `tstart`, we actually want to replace `tstart` with a unique name, such that if we by a bad luck define `tstart` in our code, it would not be affected, as we can see in this example.
234+
```julia
235+
let
236+
tstart = "should not change the value and type "
237+
t = @tooclean_elapsed r = fib(10)
238+
println(tstart, " ", typeof(tstart))
239+
end
240+
```
241+
But in the second case, we would actually very much like the variable `r` to retain its name, such that we can accesss the results (and also, `ex` can access and change other local variables). Julia offer a way to `escape` from the hygienic mode, which means that the variables will be used and passed as-is. Notice the effect if we escape jus the expression `ex`
242+
243+
```julia
244+
macro justright_elapsed(ex)
245+
quote
246+
tstart = time()
247+
$(esc(ex))
248+
time() - tstart
249+
end
250+
end
251+
252+
let
253+
tstart = "should not change the value and type "
254+
t = @justright_elapsed r = fib(10)
255+
println("the evaluation of fib took ", t, "s and result is ", r)
256+
println(tstart, " ", typeof(tstart))
257+
end
258+
```
259+
which now works as intended. We can inspect the output again using `@macroexpand`
260+
```julia
261+
julia> Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10))
262+
quote
263+
var"#19#tstart" = Main.time()
264+
r = fib(10)
265+
Main.time() - var"#19#tstart"
266+
end
267+
```
268+
and compare it to `Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10))`. We see that the experssion `ex` has its symbols intact.
269+
To use the escaping / hygience correctly, you need to have a good understanding how the macro evaluation works and what is needed. Let's now try the third version of the macro, where we escape everything as
270+
```julia
271+
macro toodirty_elapsed(ex)
272+
ex = quote
273+
tstart = time()
274+
$(ex)
275+
time() - tstart
276+
end
277+
esc(ex)
278+
end
279+
280+
let
281+
tstart = "should not change the value and type "
282+
t = @toodirty_elapsed r = fib(10)
283+
println("the evaluation of fib took ", t, "s and result is ", r)
284+
println(tstart, " ", typeof(tstart))
285+
end
286+
```
287+
288+
```julia
289+
julia> Base.remove_linenums!(@macroexpand @toodirty_elapsed r = fib(10))
290+
quote
291+
tstart = time()
292+
r = fib(10)
293+
time() - tstart
294+
end
295+
```
296+
297+
298+
!!! info
299+
### gensym
300+
301+
`gensym([tag])` Generates a symbol which will not conflict with other variable names.
302+
```julia
303+
julia> gensym("hello")
304+
Symbol("##hello#257")
305+
```
306+
307+
308+
309+
but if we look, how the function is expanded, we see that it is not as we have expected
310+
```julia
311+
312+
macro replace_sin(ex)
313+
replace_sin(esc(ex))
314+
end
315+
316+
function cosp2(x)
317+
@replace_sin 2 + sin(x)
318+
end
319+
320+
julia> @code_lowered(cosp2(1.0))
321+
CodeInfo(
322+
1 ─ %1 = Main.cos(Main.x)
323+
│ %2 = 2 + %1
324+
└── return %2
325+
)
326+
```
327+
why is that
328+
82329
## DSL
330+
## non-standard string literals
331+
```
332+
macro r_str(p)
333+
Regex(p)
334+
end
335+
```
83336
## Write @exfiltrate macro
84-
`Base.@locals`
337+
`Base.@locals`
338+
339+
## sources
340+
Great discussion on evaluation of macros
341+
https://discourse.julialang.org/t/interpolation-in-macro-calls/25530

0 commit comments

Comments
 (0)