diff --git a/src/EnumX.jl b/src/EnumX.jl index 9330dfd..35a5b0a 100644 --- a/src/EnumX.jl +++ b/src/EnumX.jl @@ -6,6 +6,8 @@ export @enumx abstract type Enum{T} <: Base.Enum{T} end +panic(x) = throw(ArgumentError(x)) + macro enumx(args...) return enumx(__module__, args...) end @@ -14,11 +16,12 @@ function enumx(_module_, name, args...) if name isa Symbol modname = name baseT = Int32 - elseif name isa Expr && name.head == :(::) && name.args[1] isa Symbol && length(name.args) == 2 + elseif name isa Expr && name.head == :(::) && name.args[1] isa Symbol && + length(name.args) == 2 modname = name.args[1] baseT = Core.eval(_module_, name.args[2]) else - throw(ArgumentError("invalid EnumX.@enumx type specification: $(name)")) + panic("invalid EnumX.@enumx type specification: $(name).") end name = modname if length(args) == 1 && args[1] isa Expr && args[1].head === :block @@ -27,18 +30,49 @@ function enumx(_module_, name, args...) syms = args end namemap = Dict{baseT,Symbol}() - next = 0 + next = zero(baseT) for s in syms s isa LineNumberNode && continue - s isa Symbol || throw(ArgumentError("invalid member expression: $(s)")) - namemap[next] = s - next += 1 + local sym + if s isa Symbol + if next == typemin(baseT) + panic("value overflow for Enum $(modname): $(modname).$(s) = $(next).") + end + sym = s + elseif s isa Expr && s.head === :(=) && s.args[1] isa Symbol && length(s.args) == 2 + nx = Core.eval(_module_, s.args[2]) + if !(nx isa Integer && typemin(baseT) <= nx <= typemax(baseT)) + panic( + "invalid value for Enum $(modname){$(baseT)}: " * + "$(modname).$(s.args[1]) = $(repr(nx))." + ) + end + next = convert(baseT, nx) + sym = s.args[1] + 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) + panic( + "duplicate name for Enum $(modname): $(modname).$(sym) = $(next)," * + " name already used for $(modname).$(namemap[value]) = $(value)." + ) + end + namemap[next] = sym + + next += oneunit(baseT) end module_block = quote primitive type Type <: Enum{$(baseT)} $(sizeof(baseT) * 8) end let namemap = $(namemap) check_valid(x) = x in keys(namemap) || - throw(ArgumentError("invalid value $(x) for Enum $($(QuoteNode(modname)))")) + 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)) @@ -68,7 +102,10 @@ function Base.show(io::IO, ::MIME"text/plain", ::Base.Type{E}) where E <: Enum string("$(nameof(parentmodule(E))).", v) => k for (k, v) in Base.Enums.namemap(E) ) mx = maximum(textwidth, keys(stringmap); init = 0) - print(iob, "Enum type $(nameof(parentmodule(E))).Type <: Enum{$(Base.Enums.basetype(E))} with $(n) instance$(n == 1 ? "" : "s"):") + print(iob, + "Enum type $(nameof(parentmodule(E))).Type <: ", + "Enum{$(Base.Enums.basetype(E))} with $(n) instance$(n == 1 ? "" : "s"):" + ) for (k, v) in stringmap print(iob, "\n", rpad(k, mx), " = $(v)") end diff --git a/test/runtests.jl b/test/runtests.jl index 9a03220..6c07a3d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -40,8 +40,8 @@ getInt64() = Int64 @test Fruit.Type(Int32(0)) === Fruit.Type(0) === Fruit.Apple @test Fruit.Type(Int32(1)) === Fruit.Type(1) === Fruit.Banana -@test_throws ArgumentError("invalid value 123 for Enum Fruit") Fruit.Type(Int32(123)) -@test_throws ArgumentError("invalid value 123 for Enum Fruit") Fruit.Type(123) +@test_throws ArgumentError("invalid value for Enum Fruit: 123.") Fruit.Type(Int32(123)) +@test_throws ArgumentError("invalid value for Enum Fruit: 123.") Fruit.Type(123) @test Fruit.Apple < Fruit.Banana @@ -56,7 +56,7 @@ let io = IOBuffer() seekstart(io) write(io, Int32(123)) seekstart(io) - @test_throws ArgumentError("invalid value 123 for Enum Fruit") read(io, Fruit.Type) + @test_throws ArgumentError("invalid value for Enum Fruit: 123.") read(io, Fruit.Type) end let io = IOBuffer() @@ -93,7 +93,7 @@ try catch err err isa LoadError && (err = err.error) @test err isa ArgumentError - @test err.msg == "invalid EnumX.@enumx type specification: Fr + uit" + @test err.msg == "invalid EnumX.@enumx type specification: Fr + uit." end @@ -114,4 +114,66 @@ end @test FruitBlock8.Apple === FruitBlock8.Type(0) @test FruitBlock8.Banana === FruitBlock8.Type(1) + +# Custom values +@enumx FruitValues Apple = 1 Banana = (1 + 2) Orange +@test FruitValues.Apple === FruitValues.Type(1) +@test FruitValues.Banana === FruitValues.Type(3) +@test FruitValues.Orange === FruitValues.Type(4) + +@enumx FruitValues8::Int8 Apple = -1 Banana = (1 + 2) Orange +@test FruitValues8.Apple === FruitValues8.Type(-1) +@test FruitValues8.Banana === FruitValues8.Type(3) +@test FruitValues8.Orange === FruitValues8.Type(4) + +@enumx FruitValuesBlock begin + Apple = sum((1, 2, 3)) + Banana +end +@test FruitValuesBlock.Apple === FruitValuesBlock.Type(6) +@test FruitValuesBlock.Banana === FruitValuesBlock.Type(7) + +try + @macroexpand @enumx Fruit::Int8 Apple=typemax(Int8) Banana +catch err + err isa LoadError && (err = err.error) + @test err isa ArgumentError + @test err.msg == "value overflow for Enum Fruit: Fruit.Banana = -128." +end +try + @macroexpand @enumx Fruit::Int8 Apple="apple" +catch err + err isa LoadError && (err = err.error) + @test err isa ArgumentError + @test err.msg == "invalid value for Enum Fruit{Int8}: Fruit.Apple = \"apple\"." +end +try + @macroexpand @enumx Fruit::Int8 Apple=128 +catch err + err isa LoadError && (err = err.error) + @test err isa ArgumentError + @test err.msg == "invalid value for Enum Fruit{Int8}: Fruit.Apple = 128." +end +try + @macroexpand @enumx Fruit::Int8 Apple() +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 +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 + end # testset