Skip to content

Commit eed07ec

Browse files
committed
up to chapter 3. no need to continue for now
1 parent f7665cf commit eed07ec

File tree

7 files changed

+348
-27
lines changed

7 files changed

+348
-27
lines changed

README.md

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,26 @@ Moreover, don't expect direct quotes, changing the sentences vocbulary helps me
1313

1414
### Contents
1515

16-
<details>
17-
<summary>Chapter 1 - The Language of Macros</summary>
18-
19-
- [What are Macros?](chapters/chapter1.md#what-are-macros)
20-
- [The Abstract Syntax Tree](chapters/chapter1.md#the-abstract-syntax-tree)
21-
- [Trying It All Together](chapters/chapter1.md#trying-it-all-together)
22-
- [Macro Rules](chapters/chapter1.md#macro-rules)
23-
- [The Abstract Syntax Tree - Demystified](chapters/chapter1.md#the-abstract-syntax-tree-demystified)
24-
- [High-Level Syntax vs. Low-level AST](chapters/chapter1.md#high-level-syntax-vs-low-level-ast)
25-
- [AST Literals](chapters/chapter1.md#ast-literals)
26-
- [Macros: The Building Blocks of Elixir](chapters/chapter1.md#macros-the-building-blocks-of-elixir)
27-
- [Macro Expansion](chapters/chapter1.md#macro-expansion)
28-
- [Code Injection and the Caller's Context](chapters/chapter1.md#code-injection-and-the-callers-context)
29-
30-
</details>
31-
32-
<details>
33-
<summary>Chapter 2 - Extending Elixir with Metaprogramming</summary>
34-
35-
- [Re-Creating the if Macro](chapters/chapter2.md#re-creating-the-if-macro)
36-
- [Adding a while Loop to Elixir](chapters/chapter2.md#adding-a-while-loop-to-elixir)
37-
- [Smarter Testing with Macros](chapters/chapter2.md#smarter-testing-with-macros)
38-
- [Extending Modules](chapters/chapter2.md#extending-modules)
39-
- [Using Module Attributes for Code Generation](chapters/chapter2.md#using-module-attributes-for-code-generation)
40-
41-
</details>
16+
- [Chapter 1 - The Language of Macros](chapters/chapter1.md)
17+
18+
- [What are Macros?](chapters/chapter1.md#what-are-macros)
19+
- [The Abstract Syntax Tree](chapters/chapter1.md#the-abstract-syntax-tree)
20+
- [Trying It All Together](chapters/chapter1.md#trying-it-all-together)
21+
- [Macro Rules](chapters/chapter1.md#macro-rules)
22+
- [The Abstract Syntax Tree - Demystified](chapters/chapter1.md#the-abstract-syntax-tree-demystified)
23+
- [High-Level Syntax vs. Low-level AST](chapters/chapter1.md#high-level-syntax-vs-low-level-ast)
24+
- [AST Literals](chapters/chapter1.md#ast-literals)
25+
- [Macros: The Building Blocks of Elixir](chapters/chapter1.md#macros-the-building-blocks-of-elixir)
26+
- [Macro Expansion](chapters/chapter1.md#macro-expansion)
27+
- [Code Injection and the Caller's Context](chapters/chapter1.md#code-injection-and-the-callers-context)
28+
29+
- [Chapter 2 - Extending Elixir with Metaprogramming](chapters/chapter2.md)
30+
31+
- [Re-Creating the if Macro](chapters/chapter2.md#re-creating-the-if-macro)
32+
- [Adding a while Loop to Elixir](chapters/chapter2.md#adding-a-while-loop-to-elixir)
33+
- [Smarter Testing with Macros](chapters/chapter2.md#smarter-testing-with-macros)
34+
- [Extending Modules](chapters/chapter2.md#extending-modules)
35+
- [Using Module Attributes for Code Generation](chapters/chapter2.md#using-module-attributes-for-code-generation)
36+
- [Compile-Time Hooks](chapters/chapter2.md#compile-time-hooks)
37+
38+
- [Chapter 3 - Advanced Compile-Time Code Generation](chapters/chapter3.md)

assertion/assertion.exs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
defmodule Assertion do
2+
defmacro __using__(_options) do
3+
quote do
4+
import unquote(__MODULE__)
5+
Module.register_attribute(__MODULE__, :tests, accumulate: true)
6+
@before_compile unquote(__MODULE__)
7+
end
8+
end
9+
10+
defmacro __before_compile__(_env) do
11+
quote do
12+
def run, do: Assertion.Test.run(@tests, __MODULE__)
13+
end
14+
end
15+
16+
defmacro test(description, do: test_block) do
17+
test_func = String.to_atom(description)
18+
19+
quote do
20+
@tests {unquote(test_func), unquote(description)}
21+
def unquote(test_func)(), do: unquote(test_block)
22+
end
23+
end
24+
25+
defmacro assert({operator, _, [lhs, rhs]}) do
26+
quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
27+
Assertion.Test.assert(operator, lhs, rhs)
28+
end
29+
end
30+
end
31+
32+
defmodule Assertion.Test do
33+
def run(tests, module) do
34+
Enum.each(tests, fn {test_func, description} ->
35+
case apply(module, test_func, []) do
36+
:ok ->
37+
IO.write(".")
38+
39+
{:fail, reason} ->
40+
IO.puts("""
41+
42+
=============================================
43+
FAILURE: #{description}
44+
=============================================
45+
46+
#{reason}
47+
""")
48+
end
49+
end)
50+
end
51+
52+
def pass do
53+
[:green, :bright, "PASSED!"]
54+
|> IO.ANSI.format()
55+
|> IO.puts()
56+
end
57+
58+
def fail(lhs, rhs) do
59+
fail =
60+
[:red, :bright, "FAILED:"]
61+
|> IO.ANSI.format()
62+
63+
IO.puts("""
64+
#{fail}
65+
Expected: #{lhs}
66+
but received: #{rhs}
67+
""")
68+
end
69+
70+
# def assert(operator, lhs, rhs) do
71+
# case operator do
72+
# :== -> if lhs == rhs, do: pass(), else: fail(lhs, rhs)
73+
# :> -> if lhs > rhs, do: pass(), else: fail(lhs, rhs)
74+
# :< -> if lhs < rhs, do: pass(), else: fail(lhs, rhs)
75+
# end
76+
# end
77+
78+
def assert(:==, lhs, rhs) when lhs == rhs do
79+
:ok
80+
end
81+
82+
def assert(:==, lhs, rhs) do
83+
{:fail,
84+
"""
85+
Expected: #{lhs}
86+
to be equal to: #{rhs}
87+
"""}
88+
end
89+
90+
def assert(:>, lhs, rhs) when lhs > rhs do
91+
:ok
92+
end
93+
94+
def assert(:>, lhs, rhs) do
95+
{:fail,
96+
"""
97+
Expected: #{lhs}
98+
to be greater than: #{rhs}
99+
"""}
100+
end
101+
end
102+
103+
defmodule MathTest do
104+
use Assertion
105+
106+
test "integers can be added and subtracted" do
107+
assert 1 + 1 == 2
108+
assert 2 + 3 == 5
109+
assert 5 - 5 == 10
110+
end
111+
112+
test "ints can be multiplied and divided" do
113+
assert 5 * 5 == 25
114+
assert 10 / 2 == 5
115+
assert 50 / 2 == 40
116+
end
117+
end

chapters/chapter2.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,166 @@ end
398398
```
399399

400400
</details>
401+
402+
- The macro `__using__` in the above code with the `run/0` function there is an issue. It's defined just after registering the tests attribute.
403+
- The issue is that the run function was expanded before any of the `test` macro accumulations could take place.
404+
- To fix this we can use the elixir hook `before_compile`.
405+
406+
#### Compile-Time Hooks
407+
408+
- Elixir allows us to set a special module attribute, `@before_compile`, to notify the compiler that an extra step is required just before compilation is finished.
409+
- The `@before_compile` attribute accepts a module argument where a `__before_compile__/1` macro must be defined.
410+
411+
[assertion.exs](assertion/assertion.exs)
412+
413+
<details>
414+
<summary>assertion.exs</summary>
415+
416+
```elixir
417+
defmodule Assertion do
418+
defmacro __using__(_options) do
419+
quote do
420+
import unquote(__MODULE__)
421+
Module.register_attribute(__MODULE__, :tests, accumulate: true)
422+
@before_compile unquote(__MODULE__)
423+
end
424+
end
425+
426+
defmacro __before_compile__(_env) do
427+
quote do
428+
def run, do: Assertion.Test.run(@tests, __MODULE__)
429+
end
430+
end
431+
432+
defmacro test(description, do: test_block) do
433+
test_func = String.to_atom(description)
434+
435+
quote do
436+
@tests {unquote(test_func), unquote(description)}
437+
def unquote(test_func)(), do: unquote(test_block)
438+
end
439+
end
440+
441+
defmacro assert({operator, _, [lhs, rhs]}) do
442+
quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
443+
Assertion.Test.assert(operator, lhs, rhs)
444+
end
445+
end
446+
end
447+
448+
defmodule Assertion.Test do
449+
def run(tests, module) do
450+
Enum.each(tests, fn {test_func, description} ->
451+
case apply(module, test_func, []) do
452+
:ok ->
453+
IO.write(".")
454+
455+
{:fail, reason} ->
456+
IO.puts("""
457+
458+
=============================================
459+
FAILURE: #{description}
460+
=============================================
461+
462+
#{reason}
463+
""")
464+
end
465+
end)
466+
end
467+
468+
def pass do
469+
[:green, :bright, "PASSED!"]
470+
|> IO.ANSI.format()
471+
|> IO.puts()
472+
end
473+
474+
def fail(lhs, rhs) do
475+
fail =
476+
[:red, :bright, "FAILED:"]
477+
|> IO.ANSI.format()
478+
479+
IO.puts("""
480+
#{fail}
481+
Expected: #{lhs}
482+
but received: #{rhs}
483+
""")
484+
end
485+
486+
# def assert(operator, lhs, rhs) do
487+
# case operator do
488+
# :== -> if lhs == rhs, do: pass(), else: fail(lhs, rhs)
489+
# :> -> if lhs > rhs, do: pass(), else: fail(lhs, rhs)
490+
# :< -> if lhs < rhs, do: pass(), else: fail(lhs, rhs)
491+
# end
492+
# end
493+
494+
def assert(:==, lhs, rhs) when lhs == rhs do
495+
:ok
496+
end
497+
498+
def assert(:==, lhs, rhs) do
499+
{:fail,
500+
"""
501+
Expected: #{lhs}
502+
to be equal to: #{rhs}
503+
"""}
504+
end
505+
506+
def assert(:>, lhs, rhs) when lhs > rhs do
507+
:ok
508+
end
509+
510+
def assert(:>, lhs, rhs) do
511+
{:fail,
512+
"""
513+
Expected: #{lhs}
514+
to be greater than: #{rhs}
515+
"""}
516+
end
517+
end
518+
519+
defmodule MathTest do
520+
use Assertion
521+
522+
test "integers can be added and subtracted" do
523+
assert 1 + 1 == 2
524+
assert 2 + 3 == 5
525+
assert 5 - 5 == 10
526+
end
527+
528+
test "ints can be multiplied and divided" do
529+
assert 5 * 5 == 25
530+
assert 10 / 2 == 5
531+
assert 50 / 2 == 40
532+
end
533+
end
534+
```
535+
536+
Output:
537+
538+
```elixir
539+
iex(1)> MathTest.run
540+
541+
=============================================
542+
FAILURE: ints can be multiplied and divided
543+
=============================================
544+
545+
Expected: 25.0
546+
to be equal to: 40
547+
548+
549+
550+
=============================================
551+
FAILURE: integers can be added and subtracted
552+
=============================================
553+
554+
Expected: 0
555+
to be equal to: 10
556+
557+
558+
:ok
559+
```
560+
561+
</details>
562+
563+
Up to this point we have created a mini testing framework, complete with its own pattern matching definitions, testing DSL, and compile-time hooks for more advanced code generation. The macro expansions are concise and we delegated to outside functions where possible to keep our code easy to reason about.

chapters/chapter3.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Chapter 3 - Advanced Compile-Time Code Generation
2+
3+
### to be continued...?

module-extension/accumulated_module_attributes.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule Assertion do
1111
end
1212

1313
defmacro test(description, do: test_block) do
14-
test_func = String.to_atom(descriptions)
14+
test_func = String.to_atom(description)
1515

1616
quote do
1717
@tests {unquote(test_func), unquote(description)}

module-extension/before_compile.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule Assertion do
2+
defmacro __using__(_options) do
3+
quote do
4+
import unquote(__MODULE__)
5+
Module.register_attribute(__MODULE__, :tests, accumulate: true)
6+
@before_compile unquote(__MODULE__)
7+
end
8+
end
9+
10+
defmacro __before_compile_(_env) do
11+
quote do
12+
def run do
13+
IO.puts("Running the tests (#{inspect(@tests)}")
14+
end
15+
end
16+
end
17+
18+
defmacro test(description, do: test_block) do
19+
test_func = String.to_atom(description)
20+
21+
quote do
22+
@tests {unquote(test_func), unquote(description)}
23+
def unquote(test_func)(), do: unquote(test_block)
24+
end
25+
end
26+
end

module-extension/math_test.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule MathTest do
2+
use Assertion
3+
4+
test "integers can be added and subtracted" do
5+
assert 1 + 1 == 2
6+
assert 2 + 3 == 5
7+
assert 5 - 5 == 10
8+
end
9+
10+
test "ints can be multiplied and divided" do
11+
assert 5 * 5 == 25
12+
assert 10 / 2 == 5
13+
assert 50 / 2 == 40
14+
end
15+
end

0 commit comments

Comments
 (0)