Skip to content

Commit

Permalink
introduce MLStyle.enum_matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
thautwarm committed Feb 13, 2023
1 parent 0b12b7a commit c582f71
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "MLStyle"
uuid = "d8e11817-5142-5d16-987a-aa16d5891078"
authors = ["thautwarm <[email protected]>"]
version = "0.4.16"
version = "0.4.17"

[deps]

Expand Down
16 changes: 10 additions & 6 deletions docs/syntax/pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,24 +350,28 @@ You can extend following APIs for your pattern objects, to implement custom patt
`MLStyle.pattern_unref(pat_obj, expr_to_pat, [:a, :b]`.

- `MLStyle.is_enum`

In a pattern `[A, B]`, usually we think both `A` and `B` are capturing patterns. However, it is handy if we can have a pattern `A` whose match means comparing to the global variable `A`.

To achieve this, we provide `MLStyle.is_enum`.
For a visible global variable `A`, if `MLStyle.is_enum(A) == true`, a symbol `A` will compile into a pattern with `MLStyle.pattern_uncall(A, expr_to_ast, [], [], [])`.

- `MLStyle.enum_matcher(E, value_to_match)`:

If `MLStyle.is_enum(E) == true`, we will call `MLStyle.enum_matcher(E, value_to_match)` to compile `E` into a pattern.

We present some examples for understandability:

### Support Pattern Matching for Julia Enums

```julia-console
julia> using MLStyle
julia> using MLStyle.AbstractPatterns: literal
julia> @enum E E1 E2
# mark E1, E2 as non-capturing patterns
julia> MLStyle.is_enum(::E) = true
# tell the compiler how to match E1, E2
julia> MLStyle.pattern_uncall(e::E, _, _, _, _) = literal(e)
# tell the compiler how to match E1 and E2
# NOTE: make sure it evaluates to a boolean value!
julia> MLStyle.enum_matcher(e::E, expr) = :($e === $expr)
julia> x = E2
julia> @match x begin
E1 => "match E1!"
Expand All @@ -383,13 +387,13 @@ julia> @macroexpand @match x begin
:(let
var"##return#261" = nothing
var"##263" = x
if var"##263" === E1
if E1 === var"##263"
var"##return#261" = let
"match E1!"
end
$(Expr(:symbolicgoto, Symbol("####final#262#264")))
end
if var"##263" === E2
if E1 === var"##263"
var"##return#261" = let
"match E2!"
end
Expand Down
93 changes: 87 additions & 6 deletions src/MatchImpl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@compi
end

export is_enum,
pattern_uncall, pattern_unref, pattern_unmacrocall, @switch, @case, @tryswitch, @match, @trymatch, Where, gen_match, gen_switch
enum_matcher,
pattern_uncall,
pattern_unref,
pattern_unmacrocall,
@switch,
@case,
@tryswitch,
@match,
@trymatch,
Where,
gen_match,
gen_switch

export Q
import MLStyle
using MLStyle: mlstyle_report_deprecation_msg!
Expand All @@ -17,8 +29,70 @@ using MLStyle.AbstractPatterns
using MLStyle.AbstractPatterns.BasicPatterns
OptionalLn = Union{LineNumberNode, Nothing}

"""
is_enum(EnumPattern)::Bool
Convert the pattern `EnumPattern` to `EnumPattern()`.
e.g.,
```
abstract type AbsS end
struct S1 <: AbsS end
struct S2 <: AbsS end
MLStyle.pattern_uncall(::Type{S}, self, _, _, _) where {S<:AbsS} = literal(S())
MLStyle.is_enum(::Type{<:AbsS}) = true
x = S1()
@match x begin
S2 => 1
S1 => 2
end
```
"""
is_enum(_)::Bool = false
function pattern_uncall end

"""
enum_matcher(Enum, value)::Expr
Generates the expression used to test if `value` is the case `Enum`.
NOTE that this only works when `is_enum(Enum)` is `true`!!!
@match V begin
Enum => ...
@end
Above single case matches when
1. `enum_matcher(Enum, ::Any)` is not defined and `V == Enum`.
2. The expression generated from `enum_matcher(Enum, :V)`
evaluates to `true` under the current module.
"""
function enum_matcher end

struct _EnumCase{E}
pattern::E
end

function pattern_uncall(enumCase::_EnumCase{E}, self, type_params, type_args, args) where E
isempty(type_params) || error("Enum type should not have type parameters!")
isempty(type_args) || error("Enum type should not have type arguments!")
isempty(args) || error("Enum type should not have arguments!")

let enumPattern = enumCase.pattern
if hasmethod(MLStyle.enum_matcher, Tuple{E, Any})
function via_enum_matcher(target, _, _)
return MLStyle.enum_matcher(enumPattern, target)
end
guard(via_enum_matcher)
else
pattern_uncall(enumPattern, self, type_params, type_args, args)
end
end
end

function pattern_unref end
function pattern_unmacrocall(macro_func, self::Function, args::AbstractArray)
@sswitch args begin
Expand Down Expand Up @@ -85,6 +159,12 @@ function guess_type_from_expr(m::Module, ex::Any, tps::Set{Symbol})
end
end

struct ModuleBoundedEx2tf <: Function
m::Module
end

@inline (self::ModuleBoundedEx2tf)(arg) = ex2tf(self.m, arg)

ex2tf(m::Module, @nospecialize(a)) = literal(a)
ex2tf(m::Module, l::LineNumberNode) = wildcard
ex2tf(m::Module, q::QuoteNode) = literal(q.value)
Expand All @@ -97,8 +177,8 @@ ex2tf(m::Module, n::Symbol) =
else
if isdefined(m, n)
p = getfield(m, n)
rec(x) = ex2tf(m, x)
is_enum(p) && return pattern_uncall(p, rec, [], [], [])
rec = ModuleBoundedEx2tf(m)
is_enum(p) && return pattern_uncall(_EnumCase(p), rec, [], [], [])
end
P_capture(n)
end
Expand All @@ -112,7 +192,8 @@ function ex2tf(m::Module, s::QuotePattern)
end

function ex2tf(m::Module, w::Where)
rec(x) = ex2tf(m, x)
rec = ModuleBoundedEx2tf(m)

@sswitch w begin
@case Where(; value = val, type = t, type_parameters = tps)

Expand Down Expand Up @@ -170,7 +251,7 @@ end

function ex2tf(m::Module, ex::Expr)
eval = m.eval
rec(x) = ex2tf(m, x)
rec = ModuleBoundedEx2tf(m)

@sswitch ex begin
@case Expr(:||, args)
Expand Down
63 changes: 63 additions & 0 deletions test/issues/154.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using MLStyle
import MLStyle.AbstractPatterns

abstract type Enum154 end

struct Enum154_1_Cons <: Enum154 end

struct Enum154_2_Cons <: Enum154
x::Vector{Int}
end
MLStyle.@as_record Enum154_2_Cons

MLStyle.is_enum(::Enum154) = true
MLStyle.enum_matcher(enum::Enum154, expr) = :($enum === $expr)

const Enum154_1 = Enum154_1_Cons()

function Base.:(==)(a::Enum154, b::Enum154)
@match (a, b) begin
(Enum154_1, Enum154_1) => true
(Enum154_2_Cons(xs), Enum154_2_Cons(ys)) => xs == ys
_ => false
end
end

# traditional behaviour

@enum JuliaEnum_154 begin
JuliaEnum_154_a
JuliaEnum_154_b
JuliaEnum_154_c
end

MLStyle.is_enum(::JuliaEnum_154) = true

MLStyle.pattern_uncall(a::JuliaEnum_154, ::Vararg) = MLStyle.AbstractPatterns.literal(a)

function eq_154(a, b)
@match (a, b) begin
(JuliaEnum_154_a, JuliaEnum_154_a) => true
(JuliaEnum_154_b, JuliaEnum_154_b) => true
(JuliaEnum_154_c, JuliaEnum_154_c) => true
_ => false
end
end

@testset "issue 154" begin
@testset "tag matching support" begin
@test Enum154_1 == Enum154_1
@test Enum154_2_Cons([1, 2, 3]) == Enum154_2_Cons([1, 2, 3])
@test Enum154_2_Cons([1, 2, 3]) != Enum154_2_Cons([1, 2, 4])
@test Enum154_1 != Enum154_2_Cons([1, 2, 3])
end

@testset "traditional" begin
@test eq_154(JuliaEnum_154_a, JuliaEnum_154_a)
@test eq_154(JuliaEnum_154_b, JuliaEnum_154_b)
@test eq_154(JuliaEnum_154_c, JuliaEnum_154_c)
@test !eq_154(JuliaEnum_154_a, JuliaEnum_154_b)
@test !eq_154(JuliaEnum_154_b, JuliaEnum_154_c)
@test !eq_154(JuliaEnum_154_c, JuliaEnum_154_a)
end
end
3 changes: 2 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ MODULE = TestModule

@use GADT

include("issues/109.jl")
include("when.jl")
include("switch.jl")
include("untyped_lam.jl")
Expand Down Expand Up @@ -80,5 +79,7 @@ include("MQuery/test.jl")

include("issues/87.jl")
include("issues/62.jl")
include("issues/109.jl")
include("issues/154.jl")

end

2 comments on commit c582f71

@thautwarm
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register branch=main

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/77551

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.4.17 -m "<description of version>" c582f7151d04b60dd9525fd6dfbdc2091a70cfae
git push origin v0.4.17

Please sign in to comment.