From abc504194efb7779981c2ad04c46969236c73389 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Mon, 14 Mar 2022 13:42:28 +0100 Subject: [PATCH] Allow duplicate values. --- src/EnumX.jl | 46 +++++++++++++++++++++++++++------------------- test/runtests.jl | 30 +++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/EnumX.jl b/src/EnumX.jl index 35a5b0a..f3fb416 100644 --- a/src/EnumX.jl +++ b/src/EnumX.jl @@ -6,12 +6,15 @@ export @enumx abstract type Enum{T} <: Base.Enum{T} end -panic(x) = throw(ArgumentError(x)) +@noinline panic(x) = throw(ArgumentError(x)) +@noinline panic() = error("unreachable") macro enumx(args...) return enumx(__module__, args...) end +function symbol_map end + function enumx(_module_, name, args...) if name isa Symbol modname = name @@ -29,7 +32,7 @@ function enumx(_module_, name, args...) else syms = args end - namemap = Dict{baseT,Symbol}() + name_value_map = Vector{Pair{Symbol, baseT}}() next = zero(baseT) for s in syms s isa LineNumberNode && continue @@ -52,46 +55,51 @@ function enumx(_module_, name, args...) else panic("invalid EnumX.@enumx entry: $(s)") end - if next in keys(namemap) - panic( - "duplicate value for Enum $(modname): $(modname).$(sym) = $(next)," * - " value already used for $(modname).$(namemap[next]) = $(next)." - ) - elseif sym in values(namemap) - value = findfirst(x -> x === sym, namemap) + if (idx = findfirst(x -> x.first === sym, name_value_map); idx !== nothing) + v = name_value_map[idx].second panic( "duplicate name for Enum $(modname): $(modname).$(sym) = $(next)," * - " name already used for $(modname).$(namemap[value]) = $(value)." + " name already used for $(modname).$(sym) = $(v)." ) end - namemap[next] = sym + push!(name_value_map, sym => next) next += oneunit(baseT) end + value_name_map = Dict{baseT,Symbol}(v => k for (k, v) in reverse(name_value_map)) module_block = quote primitive type Type <: Enum{$(baseT)} $(sizeof(baseT) * 8) end - let namemap = $(namemap) - check_valid(x) = x in keys(namemap) || + let value_name_map = $(value_name_map) + check_valid(x) = x in keys(value_name_map) || throw(ArgumentError("invalid value for Enum $($(QuoteNode(modname))): $(x).")) global function $(esc(:Type))(x::Integer) check_valid(x) return Base.bitcast($(esc(:Type)), convert($(baseT), x)) end - Base.Enums.namemap(::Base.Type{$(esc(:Type))}) = namemap + Base.Enums.namemap(::Base.Type{$(esc(:Type))}) = value_name_map Base.Enums.instances(::Base.Type{$(esc(:Type))}) = - ($([esc(k) for k in values(namemap)]...),) + ($([esc(k) for (k,v) in name_value_map]...),) + EnumX.symbol_map(::Base.Type{$(esc(:Type))}) = $(name_value_map) end end - for (k, v) in namemap + for (k, v) in name_value_map push!(module_block.args, - Expr(:const, Expr(:(=), esc(v), Expr(:call, esc(:Type), k))) + Expr(:const, Expr(:(=), esc(k), Expr(:call, esc(:Type), v))) ) end return Expr(:toplevel, Expr(:module, false, esc(modname), module_block), nothing) end function Base.show(io::IO, ::MIME"text/plain", x::E) where E <: Enum - print(io, "$(nameof(parentmodule(E))).$(Symbol(x)::Symbol) = $(Integer(x))") + iob = IOBuffer() + ix = Integer(x) + for (k, v) in symbol_map(E) + if v == ix + print(iob, "$(nameof(parentmodule(E))).$(k) = ") + end + end + print(iob, "$(Integer(x))") + write(io, seekstart(iob)) return nothing end function Base.show(io::IO, ::MIME"text/plain", ::Base.Type{E}) where E <: Enum @@ -99,7 +107,7 @@ function Base.show(io::IO, ::MIME"text/plain", ::Base.Type{E}) where E <: Enum insts = Base.Enums.instances(E) n = length(insts) stringmap = Dict{String, Int32}( - string("$(nameof(parentmodule(E))).", v) => k for (k, v) in Base.Enums.namemap(E) + string("$(nameof(parentmodule(E))).", k) => v for (k, v) in symbol_map(E) ) mx = maximum(textwidth, keys(stringmap); init = 0) print(iob, diff --git a/test/runtests.jl b/test/runtests.jl index 6c07a3d..67f674b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -90,6 +90,7 @@ end try @macroexpand @enumx (Fr + uit) Apple + error() catch err err isa LoadError && (err = err.error) @test err isa ArgumentError @@ -135,6 +136,7 @@ end try @macroexpand @enumx Fruit::Int8 Apple=typemax(Int8) Banana + error() catch err err isa LoadError && (err = err.error) @test err isa ArgumentError @@ -142,6 +144,7 @@ catch err end try @macroexpand @enumx Fruit::Int8 Apple="apple" + error() catch err err isa LoadError && (err = err.error) @test err isa ArgumentError @@ -149,6 +152,7 @@ catch err end try @macroexpand @enumx Fruit::Int8 Apple=128 + error() catch err err isa LoadError && (err = err.error) @test err isa ArgumentError @@ -156,24 +160,36 @@ catch err end try @macroexpand @enumx Fruit::Int8 Apple() + error() catch err err isa LoadError && (err = err.error) @test err isa ArgumentError @test err.msg == "invalid EnumX.@enumx entry: Apple()" end -try - @macroexpand @enumx Fruit Apple=0 Banana=0 -catch err - err isa LoadError && (err = err.error) - @test err isa ArgumentError - @test err.msg == "duplicate value for Enum Fruit: Fruit.Banana = 0, value already used for Fruit.Apple = 0." -end try @macroexpand @enumx Fruit Apple Apple + error() catch err err isa LoadError && (err = err.error) @test err isa ArgumentError @test err.msg == "duplicate name for Enum Fruit: Fruit.Apple = 1, name already used for Fruit.Apple = 0." end + +# Duplicate values +@enumx FruitDup Apple=0 Banana=0 +@test FruitDup.Apple === FruitDup.Banana === FruitDup.Type(0) + +let io = IOBuffer() + show(io, "text/plain", FruitDup.Type) + str = String(take!(io)) + @test str == "Enum type FruitDup.Type <: Enum{Int32} with 2 instances:\nFruitDup.Apple = 0\nFruitDup.Banana = 0" + show(io, "text/plain", FruitDup.Apple) + str = String(take!(io)) + @test str == "FruitDup.Apple = FruitDup.Banana = 0" + show(io, "text/plain", FruitDup.Banana) + str = String(take!(io)) + @test str == "FruitDup.Apple = FruitDup.Banana = 0" +end + end # testset