Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provides ways to circumvent unexpected type bindings #160

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "MacroTools"
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
version = "0.5.6"
version = "0.6.0"

[deps]
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Expand Down
92 changes: 73 additions & 19 deletions docs/Manifest.toml
Original file line number Diff line number Diff line change
@@ -1,42 +1,66 @@
# This file is machine-generated - editing it directly is not advised

[[ArgTools]]
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"

[[Artifacts]]
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"

[[Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[Distributed]]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"

[[DocStringExtensions]]
deps = ["LibGit2", "Markdown", "Pkg", "Test"]
git-tree-sha1 = "88bb0edb352b16608036faadcc071adda068582a"
git-tree-sha1 = "9d4f64f79012636741cf01133158a54b24924c32"
uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
version = "0.8.1"
version = "0.8.4"

[[Documenter]]
deps = ["Base64", "Dates", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"]
git-tree-sha1 = "646ebc3db49889ffeb4c36f89e5d82c6a26295ff"
deps = ["Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"]
git-tree-sha1 = "3ebb967819b284dc1e3c0422229b58a40a255649"
uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
version = "0.24.7"
version = "0.26.3"

[[Downloads]]
deps = ["ArgTools", "LibCURL", "NetworkOptions"]
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"

[[IOCapture]]
deps = ["Logging"]
git-tree-sha1 = "377252859f740c217b936cebcd918a44f9b53b59"
uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
version = "0.1.1"

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

[[JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e"
git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.0"
version = "0.21.1"

[[LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"

[[LibCURL_jll]]
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"

[[LibGit2]]
deps = ["Printf"]
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"

[[LibSSH2_jll]]
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"

[[Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

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

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

[[MbedTLS_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"

[[Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[MozillaCACerts_jll]]
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"

[[NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"

[[Parsers]]
deps = ["Dates", "Test"]
git-tree-sha1 = "0c16b3179190d3046c073440d94172cfc3bb0553"
deps = ["Dates"]
git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "0.3.12"
version = "1.1.0"

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

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

[[REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets"]
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

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

[[TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[[Tar]]
deps = ["ArgTools", "SHA"]
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"

[[Test]]
deps = ["Distributed", "InteractiveUtils", "Logging", "Random"]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

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

[[Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[[Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"

[[nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"

[[p7zip_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
22 changes: 21 additions & 1 deletion docs/src/pattern-matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Highlander there can only be one (per expression).

### Matching on expression type

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

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

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

!!! tip "Matching without expression type"
[Matching on expression type](@ref) can be useful, but the problem is that `@capture` can't distinguish between
its 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
```

To circumvent this issue, MacroTools exports `@capture_notb`, which skips all the expression type matching syntaxes:
```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
```


### Unions

`@capture` can also try to match the expression against one pattern or another,
Expand Down
2 changes: 1 addition & 1 deletion src/MacroTools.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module MacroTools

using Markdown, Random
export @match, @capture
export @match, @capture, @match_notb, @capture_notb

include("match/match.jl")
include("match/types.jl")
Expand Down
42 changes: 22 additions & 20 deletions src/match/macro.jl
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
function allbindings(pat, bs)
function allbindings(context, pat, bs)
if isa(pat, QuoteNode)
return allbindings(pat.value, bs)
return allbindings(context, pat.value, bs)
end
return isbinding(pat) || (isslurp(pat) && pat ≠ :__) ? push!(bs, bname(pat)) :
isa(pat, TypeBind) ? push!(bs, pat.name) :
isa(pat, OrBind) ? (allbindings(pat.pat1, bs); allbindings(pat.pat2, bs)) :
istb(pat) ? push!(bs, tbname(pat)) :
isa(pat, OrBind) ? (allbindings(context, pat.pat1, bs); allbindings(context, pat.pat2, bs)) :
istb(context, pat) ? push!(bs, tbname(pat)) :
isexpr(pat, :$) ? bs :
isa(pat, Expr) ? map(pat -> allbindings(pat, bs), [pat.head, pat.args...]) :
isa(pat, Expr) ? map(pat -> allbindings(context, pat, bs), [pat.head, pat.args...]) :
bs
end

allbindings(pat) = (bs = Any[]; allbindings(pat, bs); bs)
allbindings(context, pat) = (bs = Any[]; allbindings(context, pat, bs); bs)

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

function makeclause(pat, yes, els = nothing)
bs = allbindings(pat)
pat = subtb(subor(pat))
function makeclause(context, pat, yes, els = nothing)
bs = allbindings(context, pat)
pat = subtb(context, subor(pat))
quote
env = trymatch($(Expr(:quote, pat)), ex)
if env != nothing
Expand All @@ -46,28 +46,30 @@ function clauses(ex)
return clauses
end

macro match(ex, lines)
macro match(ex, lines) _match(__module__, ex, lines) end
macro match_notb(ex, lines) _match(nothing, ex, lines) end
function _match(context, ex, lines)
@assert isexpr(lines, :block)
result = quote
ex = $(esc(ex))
end

@static if VERSION < v"0.7.0-"
body = foldr((clause, body) -> makeclause(clause..., body),
nothing, clauses(lines))
else
body = foldr((clause, body) -> makeclause(clause..., body),
body = @static VERSION < v"0.7.0-" ?
foldr((clause, body) -> makeclause(context, clause..., body),
nothing, clauses(lines)) :
foldr((clause, body) -> makeclause(context, clause..., body),
clauses(lines); init=nothing)
end

push!(result.args, body)
return result
end

macro capture(ex, pat)
bs = allbindings(pat)
pat = subtb(subor(pat))
quote
macro capture(ex, pat) _capture(__module__, ex, pat) end
macro capture_notb(ex, pat) _capture(nothing, ex, pat) end
function _capture(context, ex, pat)
bs = allbindings(context, pat)
pat = subtb(context, subor(pat))
return quote
$([:($(esc(b)) = nothing) for b in bs]...)
env = trymatch($(esc(Expr(:quote, pat))), $(esc(ex)))
if env == nothing
Expand Down
38 changes: 29 additions & 9 deletions src/match/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,37 @@ struct TypeBind
ts::Set{Any}
end

istb(s) = false
istb(s::Symbol) = !(endswith(string(s), "_") ||
endswith(string(s), "_str")) &&
occursin("_", string(s))
istb(::Nothing, _) = false
istb(::Module, _) = false
function istb(context::Module, s::Symbol)
(endswith(string(s), "_") || endswith(string(s), "_str")) && return false
occursin("_", string(s)) || return false
ts = map(Symbol, split(string(s), "_"))
popfirst!(ts)
return all(s->istype(context, s), ts)
end

function istype(context::Module, s::Symbol)
if string(s)[1] in 'A':'Z'
if isdefined(context, s) && isa(getfield(context, s), Type)
return true
else
throw(ArgumentError("""
the syntax to specify expression type syntax is used, but the given type isn't defined:
if you want to ignore the syntaxes to specify expression type, use `@capture_notb` or `@match_notb` instead
"""))
end
end
return true
end

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

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

function tbnew(s::Symbol)
istb(s) || return s
function tbnew(context::Module, s::Symbol)
istb(context, s) || return s
ts = map(Symbol, split(string(s), "_"))
name = popfirst!(ts)
ts = map(totype, ts)
Expand All @@ -24,6 +43,7 @@ end
match_inner(b::TypeBind, ex, env) =
isexpr(ex, b.ts...) ? (env[tbname(b)] = ex; env) : @nomatch(b, ex)

subtb(s) = s
subtb(s::Symbol) = tbnew(s)
subtb(s::Expr) = isexpr(s, :line) ? s : Expr(subtb(s.head), map(subtb, s.args)...)
subtb(::Nothing, s) = s
subtb(context::Module, s) = s
subtb(context::Module, s::Symbol) = tbnew(context, s)
subtb(context::Module, s::Expr) = isexpr(s, :line) ? s : Expr(subtb(context, s.head), map(s->subtb(context, s), s.args)...)
18 changes: 18 additions & 0 deletions test/match.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,21 @@ let
@capture(ex, $f(args__))
@test args == [:a, :b]
end

# hint to use `@capture_notb`
let
ex = :(const GLOBAL_STRING = 10)
@static if v"1.7.0-DEV" ≤ VERSION
@test_throws ArgumentError macroexpand(@__MODULE__, :(@capture($ex, const GLOBAL_STRING = x_)))
else
# before v1.7, `macroexpand` throws `LoadError` instead of actual error
@test_throws LoadError macroexpand(@__MODULE__, :(@capture($ex, const GLOBAL_STRING = x_)))
end
end

# if we don't want to make `global_string` type bind syntax, we need to use `@capture_notb`
let
ex = :(const global_string = 10)
@capture_notb(ex, const global_string = x_)
@test x === 10
end