Skip to content

Commit 395dced

Browse files
committed
provides ways to circumvent unexpected type bindings
This PR fixes issues involved with the syntaxes to specify expression types. Currently, `@capture` and `@match` can't distinguish between their syntax to specify a `:head` of `Expr` and a variable name with underscores. For example, in the example below, `@capture` recognizes `global_string` as the syntax to specify `Expr`'s head (i.e. `:string`), not as a simple variable name: ```julia julia> ex = :(global_string = 10); julia> @capture(ex, global_string = n_) # tries to match `Expr(:string, ...) = n_` and bound the matched lhs into a variable `global` and the matched rhs into a variable `n`. false ``` Since an expression can really have an arbitrary head, `@capture` macro can't really distinguish them (while we can do some assertion when the syntax to specify atomic expression type, though). This PR implements new macros `@capture_notb` and `@match_notb`, which ignore all the expression type matching syntaxes and provide the ways to circumvent the issue described above: ```julia julia> ex = :(global_string = 10) julia> @capture_notb(ex, global_string = n_) # tries to match `global_string = n_` pattern and bound the matched rhs into a variable `n`. true ``` These changes aren't breaking but they're somewhat a big change, I'd like to minor version bump.
1 parent fef1c6f commit 395dced

File tree

7 files changed

+165
-51
lines changed

7 files changed

+165
-51
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "MacroTools"
22
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
3-
version = "0.5.6"
3+
version = "0.6.0"
44

55
[deps]
66
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"

docs/Manifest.toml

+73-19
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,66 @@
11
# This file is machine-generated - editing it directly is not advised
22

3+
[[ArgTools]]
4+
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
5+
6+
[[Artifacts]]
7+
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
8+
39
[[Base64]]
410
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
511

612
[[Dates]]
713
deps = ["Printf"]
814
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
915

10-
[[Distributed]]
11-
deps = ["Random", "Serialization", "Sockets"]
12-
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"
13-
1416
[[DocStringExtensions]]
1517
deps = ["LibGit2", "Markdown", "Pkg", "Test"]
16-
git-tree-sha1 = "88bb0edb352b16608036faadcc071adda068582a"
18+
git-tree-sha1 = "9d4f64f79012636741cf01133158a54b24924c32"
1719
uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
18-
version = "0.8.1"
20+
version = "0.8.4"
1921

2022
[[Documenter]]
21-
deps = ["Base64", "Dates", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"]
22-
git-tree-sha1 = "646ebc3db49889ffeb4c36f89e5d82c6a26295ff"
23+
deps = ["Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"]
24+
git-tree-sha1 = "3ebb967819b284dc1e3c0422229b58a40a255649"
2325
uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
24-
version = "0.24.7"
26+
version = "0.26.3"
27+
28+
[[Downloads]]
29+
deps = ["ArgTools", "LibCURL", "NetworkOptions"]
30+
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
31+
32+
[[IOCapture]]
33+
deps = ["Logging"]
34+
git-tree-sha1 = "377252859f740c217b936cebcd918a44f9b53b59"
35+
uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
36+
version = "0.1.1"
2537

2638
[[InteractiveUtils]]
2739
deps = ["Markdown"]
2840
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
2941

3042
[[JSON]]
3143
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
32-
git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e"
44+
git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4"
3345
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
34-
version = "0.21.0"
46+
version = "0.21.1"
47+
48+
[[LibCURL]]
49+
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
50+
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
51+
52+
[[LibCURL_jll]]
53+
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
54+
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
3555

3656
[[LibGit2]]
37-
deps = ["Printf"]
57+
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
3858
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
3959

60+
[[LibSSH2_jll]]
61+
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
62+
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
63+
4064
[[Libdl]]
4165
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
4266

@@ -47,31 +71,41 @@ uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
4771
deps = ["Markdown", "Random"]
4872
path = ".."
4973
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
50-
version = "0.5.5"
74+
version = "0.5.6"
5175

5276
[[Markdown]]
5377
deps = ["Base64"]
5478
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
5579

80+
[[MbedTLS_jll]]
81+
deps = ["Artifacts", "Libdl"]
82+
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
83+
5684
[[Mmap]]
5785
uuid = "a63ad114-7e13-5084-954f-fe012c677804"
5886

87+
[[MozillaCACerts_jll]]
88+
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
89+
90+
[[NetworkOptions]]
91+
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
92+
5993
[[Parsers]]
60-
deps = ["Dates", "Test"]
61-
git-tree-sha1 = "0c16b3179190d3046c073440d94172cfc3bb0553"
94+
deps = ["Dates"]
95+
git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc"
6296
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
63-
version = "0.3.12"
97+
version = "1.1.0"
6498

6599
[[Pkg]]
66-
deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"]
100+
deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
67101
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
68102

69103
[[Printf]]
70104
deps = ["Unicode"]
71105
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
72106

73107
[[REPL]]
74-
deps = ["InteractiveUtils", "Markdown", "Sockets"]
108+
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
75109
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
76110

77111
[[Random]]
@@ -87,8 +121,16 @@ uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
87121
[[Sockets]]
88122
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
89123

124+
[[TOML]]
125+
deps = ["Dates"]
126+
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
127+
128+
[[Tar]]
129+
deps = ["ArgTools", "SHA"]
130+
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
131+
90132
[[Test]]
91-
deps = ["Distributed", "InteractiveUtils", "Logging", "Random"]
133+
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
92134
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
93135

94136
[[UUIDs]]
@@ -97,3 +139,15 @@ uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
97139

98140
[[Unicode]]
99141
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
142+
143+
[[Zlib_jll]]
144+
deps = ["Libdl"]
145+
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
146+
147+
[[nghttp2_jll]]
148+
deps = ["Artifacts", "Libdl"]
149+
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
150+
151+
[[p7zip_jll]]
152+
deps = ["Artifacts", "Libdl"]
153+
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"

docs/src/pattern-matching.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Highlander there can only be one (per expression).
6868

6969
### Matching on expression type
7070

71-
`@capture` can match expressions by their type, which is either the `head` of `Expr`
71+
`@capture` can match expressions by their type, which is either the `:head` of `Expr`
7272
objects or the `typeof` atomic stuff like `Symbol`s and `Int`s. For example:
7373

7474
```julia
@@ -91,6 +91,26 @@ Another common use case is to catch symbol literals, e.g.
9191

9292
which will match e.g. `struct Foo ...` but not `struct Foo{V} ...`
9393

94+
!!! tip "Matching without expression type"
95+
[Matching on expression type](@ref) can be useful, but the problem is that `@capture` can't distinguish between
96+
its syntax to specify a `:head` of `Expr` and a variable name with underscores:
97+
98+
For example, in the example below, `@capture` recognizes `global_string` as the syntax to specify `Expr`'s head (i.e. `:string`),
99+
not as a simple variable name:
100+
```julia
101+
julia> ex = :(global_string = 10);
102+
julia> @capture(ex, global_string = n_) # tries to match `Expr(:string, ...) = n_` and bound the matched lhs into a variable `global` and the matched rhs into a variable `n`.
103+
false
104+
```
105+
106+
To circumvent this issue, MacroTools exports `@capture_notb`, which skips all the expression type matching syntaxes:
107+
```julia
108+
julia> ex = :(global_string = 10)
109+
julia> @capture_notb(ex, global_string = n_) # tries to match `global_string = n_` pattern and bound the matched rhs into a variable `n`.
110+
true
111+
```
112+
113+
94114
### Unions
95115

96116
`@capture` can also try to match the expression against one pattern or another,

src/MacroTools.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module MacroTools
22

33
using Markdown, Random
4-
export @match, @capture
4+
export @match, @capture, @match_notb, @capture_notb
55

66
include("match/match.jl")
77
include("match/types.jl")

src/match/macro.jl

+22-20
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
function allbindings(pat, bs)
1+
function allbindings(context, pat, bs)
22
if isa(pat, QuoteNode)
3-
return allbindings(pat.value, bs)
3+
return allbindings(context, pat.value, bs)
44
end
55
return isbinding(pat) || (isslurp(pat) && pat :__) ? push!(bs, bname(pat)) :
66
isa(pat, TypeBind) ? push!(bs, pat.name) :
7-
isa(pat, OrBind) ? (allbindings(pat.pat1, bs); allbindings(pat.pat2, bs)) :
8-
istb(pat) ? push!(bs, tbname(pat)) :
7+
isa(pat, OrBind) ? (allbindings(context, pat.pat1, bs); allbindings(context, pat.pat2, bs)) :
8+
istb(context, pat) ? push!(bs, tbname(pat)) :
99
isexpr(pat, :$) ? bs :
10-
isa(pat, Expr) ? map(pat -> allbindings(pat, bs), [pat.head, pat.args...]) :
10+
isa(pat, Expr) ? map(pat -> allbindings(context, pat, bs), [pat.head, pat.args...]) :
1111
bs
1212
end
1313

14-
allbindings(pat) = (bs = Any[]; allbindings(pat, bs); bs)
14+
allbindings(context, pat) = (bs = Any[]; allbindings(context, pat, bs); bs)
1515

1616
function bindinglet(bs, body)
1717
ex = :(let $(esc(:env)) = env, $((:($(esc(b)) = get(env, $(Expr(:quote, b)), nothing)) for b in bs)...)
@@ -20,9 +20,9 @@ function bindinglet(bs, body)
2020
return ex
2121
end
2222

23-
function makeclause(pat, yes, els = nothing)
24-
bs = allbindings(pat)
25-
pat = subtb(subor(pat))
23+
function makeclause(context, pat, yes, els = nothing)
24+
bs = allbindings(context, pat)
25+
pat = subtb(context, subor(pat))
2626
quote
2727
env = trymatch($(Expr(:quote, pat)), ex)
2828
if env != nothing
@@ -46,28 +46,30 @@ function clauses(ex)
4646
return clauses
4747
end
4848

49-
macro match(ex, lines)
49+
macro match(ex, lines) _match(__module__, ex, lines) end
50+
macro match_notb(ex, lines) _match(nothing, ex, lines) end
51+
function _match(context, ex, lines)
5052
@assert isexpr(lines, :block)
5153
result = quote
5254
ex = $(esc(ex))
5355
end
5456

55-
@static if VERSION < v"0.7.0-"
56-
body = foldr((clause, body) -> makeclause(clause..., body),
57-
nothing, clauses(lines))
58-
else
59-
body = foldr((clause, body) -> makeclause(clause..., body),
57+
body = @static VERSION < v"0.7.0-" ?
58+
foldr((clause, body) -> makeclause(context, clause..., body),
59+
nothing, clauses(lines)) :
60+
foldr((clause, body) -> makeclause(context, clause..., body),
6061
clauses(lines); init=nothing)
61-
end
6262

6363
push!(result.args, body)
6464
return result
6565
end
6666

67-
macro capture(ex, pat)
68-
bs = allbindings(pat)
69-
pat = subtb(subor(pat))
70-
quote
67+
macro capture(ex, pat) _capture(__module__, ex, pat) end
68+
macro capture_notb(ex, pat) _capture(nothing, ex, pat) end
69+
function _capture(context, ex, pat)
70+
bs = allbindings(context, pat)
71+
pat = subtb(context, subor(pat))
72+
return quote
7173
$([:($(esc(b)) = nothing) for b in bs]...)
7274
env = trymatch($(esc(Expr(:quote, pat))), $(esc(ex)))
7375
if env == nothing

src/match/types.jl

+29-9
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,37 @@ struct TypeBind
33
ts::Set{Any}
44
end
55

6-
istb(s) = false
7-
istb(s::Symbol) = !(endswith(string(s), "_") ||
8-
endswith(string(s), "_str")) &&
9-
occursin("_", string(s))
6+
istb(::Nothing, _) = false
7+
istb(::Module, _) = false
8+
function istb(context::Module, s::Symbol)
9+
(endswith(string(s), "_") || endswith(string(s), "_str")) && return false
10+
occursin("_", string(s)) || return false
11+
ts = map(Symbol, split(string(s), "_"))
12+
popfirst!(ts)
13+
return all(s->istype(context, s), ts)
14+
end
15+
16+
function istype(context::Module, s::Symbol)
17+
if string(s)[1] in 'A':'Z'
18+
if isdefined(context, s) && isa(getfield(context, s), Type)
19+
return true
20+
else
21+
throw(ArgumentError("""
22+
the syntax to specify expression type syntax is used, but the given type isn't defined:
23+
if you want to ignore the syntaxes to specify expression type, use `@capture_notb` or `@match_notb` instead
24+
"""))
25+
end
26+
end
27+
return true
28+
end
1029

1130
tbname(s::Symbol) = Symbol(split(string(s), "_")[1])
1231
tbname(s::TypeBind) = s.name
1332

1433
totype(s::Symbol) = string(s)[1] in 'A':'Z' ? s : Expr(:quote, s)
1534

16-
function tbnew(s::Symbol)
17-
istb(s) || return s
35+
function tbnew(context::Module, s::Symbol)
36+
istb(context, s) || return s
1837
ts = map(Symbol, split(string(s), "_"))
1938
name = popfirst!(ts)
2039
ts = map(totype, ts)
@@ -24,6 +43,7 @@ end
2443
match_inner(b::TypeBind, ex, env) =
2544
isexpr(ex, b.ts...) ? (env[tbname(b)] = ex; env) : @nomatch(b, ex)
2645

27-
subtb(s) = s
28-
subtb(s::Symbol) = tbnew(s)
29-
subtb(s::Expr) = isexpr(s, :line) ? s : Expr(subtb(s.head), map(subtb, s.args)...)
46+
subtb(::Nothing, s) = s
47+
subtb(context::Module, s) = s
48+
subtb(context::Module, s::Symbol) = tbnew(context, s)
49+
subtb(context::Module, s::Expr) = isexpr(s, :line) ? s : Expr(subtb(context, s.head), map(s->subtb(context, s), s.args)...)

test/match.jl

+18
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,21 @@ let
9696
@capture(ex, $f(args__))
9797
@test args == [:a, :b]
9898
end
99+
100+
# hint to use `@capture_notb`
101+
let
102+
ex = :(const GLOBAL_STRING = 10)
103+
@static if v"1.7.0-DEV" VERSION
104+
@test_throws ArgumentError macroexpand(@__MODULE__, :(@capture($ex, const GLOBAL_STRING = x_)))
105+
else
106+
# before v1.7, `macroexpand` throws `LoadError` instead of actual error
107+
@test_throws LoadError macroexpand(@__MODULE__, :(@capture($ex, const GLOBAL_STRING = x_)))
108+
end
109+
end
110+
111+
# if we don't want to make `global_string` type bind syntax, we need to use `@capture_notb`
112+
let
113+
ex = :(const global_string = 10)
114+
@capture_notb(ex, const global_string = x_)
115+
@test x === 10
116+
end

0 commit comments

Comments
 (0)