Skip to content

Commit

Permalink
Add macro interface @bitflagx to scope definitions to a module
Browse files Browse the repository at this point in the history
Extend the capability of the expression generator to wrap the resulting
definitions within a `baremodule`, thereby introducing a scope to
isolate flag value names.

This requires some mild rewiring of the macro expansion to "flip" the
interpretation of the name in `BitFlagName::BaseType` to instead
become the module name, and a new optional first argument adds support
for choosing the actual type name (defaulting to `T`) within the
module.

Fixes #13.
  • Loading branch information
jmert committed Nov 13, 2023
1 parent e222c1e commit 9c57d39
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 31 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,40 @@ Stacktrace:
...
```

Both the bit flag type and member instances are added to the surrounding scope.
If some members have common or conflicting names — or if scoped names are
simply desired on principle — the `@bitflagx` macro can be used instead.
This variation supports the same features and syntax as `@bitflag` (with
respect to choosing the base integer type, inline versus block definitions,
and setting particular flag values), but the definitions are instead placed
within a [bare] module, avoiding adding anything but the module name to the
surrounding scope.

For example, the following avoids shadowing the `sin` function:
```julia
julia> @bitflagx TrigFunctions sin cos tan csc sec cot

julia> TrigFunctions.sin
sin::TrigFunctions.T = 0x00000001

julia> sin(π)
0.0

julia> print(typeof(TrigFunctions.sin))
Main.TrigFunctions.T
```
By default, the name of the type is `T`, but may be overridden by adding as
a first argument an alternative name:
```julia
julia> @bitflag T=type HyperbolicTrigFunctions sinh cosh tanh csch sech coth

julia> HyperbolicTrigFunctions.tanh
tanh::HyperbolicTrigFunctions.type = 0x00000004

julia> print(typeof(HyperbolicTrigFunctions.tanh))
Main.HyperbolicTrigFunctions.type
```

## Printing

Each flag value is then printed with contextual information which is more
Expand Down
151 changes: 120 additions & 31 deletions src/BitFlags.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module BitFlags
import Core.Intrinsics.bitcast
import Base.Meta.isexpr

export BitFlag, @bitflag
export BitFlag, @bitflag, @bitflagx

function namemap end
function haszero end
Expand Down Expand Up @@ -78,9 +78,30 @@ function Base.show(io::IO, x::BitFlag)
print(io, x)
else
print(io, x, "::")
# explicitly setting :compact => false prints the type with its
# "contextual path", i.e. MyFlag (for Main.MyFlag) or Main.SubModule.OtherFlags
show(IOContext(io, :compact => false), typeof(x))

T = typeof(x)
Tdef = parentmodule(T)
from = get(io, :module, @static isdefined(Base, :active_module) ? Base.active_module() : Main)

# Detect a scoped BitFlag inside a baremodule by looking for the implicit import
# of Base bindings. For scoped bitflags, we actually care about whether the
# module itself is visible instead of the type.
isscoped = !isdefined(Tdef, :Base)
sym = nameof(!isscoped ? T : Tdef)
refmod = !isscoped ? Tdef : parentmodule(Tdef)
if from === nothing || !Base.isvisible(sym, refmod, from)
if !isscoped
print(io, refmod, ".", sym)
else
print(io, Tdef, ".", nameof(T))
end
else
if !isscoped
print(io, sym)
else
print(io, nameof(Tdef), ".", nameof(T))
end
end
print(io, " = ")
show(io, Integer(x))
end
Expand All @@ -103,7 +124,15 @@ end
throw(ArgumentError("invalid value for BitFlag $typename: $x"))
end

@noinline function _throw_error(typename, s, msg = nothing)
@noinline function _throw_macro_error(macroname, args, msg = nothing)
errmsg = "bad macro call: $(Expr(:macrocall, Symbol(macroname), nothing, args...))"
if msg !== nothing
errmsg *= "; " * msg
end
throw(ArgumentError(errmsg))
end

@noinline function _throw_named_error(typename, s, msg = nothing)
errmsg = "invalid argument for BitFlag $typename: $s"
if msg !== nothing
errmsg *= "; " * msg
Expand All @@ -122,14 +151,14 @@ Create a `BitFlag{BaseType}` subtype with name `BitFlagName` and flag member val
```jldoctest itemflags
julia> @bitflag Items apple=1 fork=2 napkin=4
julia> f(x::Items) = "I'm an Item with value: \$x"
julia> f(x::Items) = "I'm a flag with value: \$x"
f (generic function with 1 method)
julia> f(apple)
"I'm an Item with value: apple"
"I'm a flag with value: apple"
julia> f(apple | fork)
"I'm an Item with value: apple | fork"
"I'm a flag with value: (apple | fork)"
```
Values can also be specified inside a `begin` block, e.g.
Expand All @@ -154,34 +183,78 @@ julia> instances(Items)
```
"""
macro bitflag(T::Union{Symbol, Expr}, x::Union{Symbol, Expr}...)
return _bitflag(__module__, T, Any[x...])
flagname, basetype = _parse_name(__module__, T)
return _bitflag(__module__, nothing, flagname, basetype, Any[x...])
end

function _bitflag(__module__::Module, T::Union{Symbol, Expr}, x::Vector{Any})
if T isa Symbol
typename = T
"""
@bitflagx [T=FlagTypeName] BitFlagName[::BaseType] value1[=x] value2[=y]
Like [`@bitflag`](@ref) but instead scopes the new type `FlagTypeName` (named `T` if not
overridden via the first optional argument) and member constants within a module named
`BitFlagName`.
# Examples
```jldoctest scopedflags
julia> @bitflagx ScopedItems apple=1 fork=2 napkin=4
julia> f(x::ScopedItems.T) = "I'm a scoped flag with value: \$x"
f (generic function with 1 method
julia> f(ScopedItems.apple | ScopedItems.fork)
"I'm a scoped flag with value: (fork | apple)"
"""
macro bitflagx(arg1::Union{Symbol, Expr}, args::Union{Symbol, Expr}...)
self = Symbol("@bitflagx")
x = Any[args...]
if isexpr(arg1, :(=), 2) && (e = arg1::Expr; (e.args[1] === :T && e.args[2] isa Symbol))
# For this case, we need to decompose and swap symbols:
# - `FlagTypeName` in `T = FlagTypeName` needs to get moved to the flagexpr argument
# - `BitFlagName` in `BitFlagName[::BaseType]` becomes the scope name
length(x) < 1 && _throw_macro_error(self, (arg1, args...))
arg2 = popfirst!(x)
flagname = arg1.args[2]
scope, basetype = _parse_name(__module__, arg2)
return _bitflag(__module__, scope, flagname, basetype, x)
elseif isexpr(arg1, :(::), 2) && (e = arg1::Expr; e.args[1] isa Symbol)
scope, basetype = _parse_name(__module__, arg1)
return _bitflag(__module__, scope, :T, basetype, x)
elseif arg1 isa Symbol
return _bitflag(__module__, arg1, :T, UInt32, x)
else
_throw_macro_error(self, (arg1, args...))
end
end

function _parse_name(__module__::Module, flagexpr::Union{Symbol, Expr})
if flagexpr isa Symbol
flagname = flagexpr
basetype = UInt32
elseif isexpr(T, :(::), 2) && (e = T::Expr; e.args[1] isa Symbol)
typename = e.args[1]::Symbol
elseif isexpr(flagexpr, :(::), 2) && (e = flagexpr::Expr; e.args[1] isa Symbol)
flagname = e.args[1]::Symbol
baseexpr = Core.eval(__module__, e.args[2])
if !(baseexpr isa DataType) || !(baseexpr <: Unsigned) || !isbitstype(baseexpr)
_throw_error(typename, T, "base type must be a bitstype unsigned integer")
_throw_named_error(flagname, flagexpr, "base type must be a bitstype unsigned integer")
end
basetype = baseexpr::Type{<:Unsigned}
else
_throw_error(T, "bad expression head")
_throw_named_error(flagexpr, "bad expression head")
end
if isempty(x)
throw(ArgumentError("no arguments given for BitFlag $typename"))
elseif length(x) == 1 && isexpr(x[1], :block)
return (flagname, basetype)
end

function _bitflag(__module__::Module, scope::Union{Symbol, Nothing}, flagname::Symbol, basetype::Type{<:Unsigned}, x::Vector{Any})
isempty(x) && throw(ArgumentError("no arguments given for BitFlag $flagname"))
if length(x) == 1 && isexpr(x[1], :block)
syms = (x[1]::Expr).args
else
syms = x
end
return _bitflag_impl(__module__, typename, basetype, syms)
return _bitflag_impl(__module__, scope, flagname, basetype, syms)
end

function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Unsigned}, syms::Vector{Any})
function _bitflag_impl(__module__::Module, scope::Union{Symbol, Nothing}, typename::Symbol, basetype::Type{<:Unsigned},
syms::Vector{Any})
names = Vector{Symbol}()
values = Vector{basetype}()
seen = Set{Symbol}()
Expand All @@ -201,23 +274,23 @@ function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Un
sym = e.args[1]::Symbol
ei = Core.eval(__module__, e.args[2]) # allow exprs, e.g. uint128"1"
if !(ei isa Integer)
_throw_error(typename, s, "values must be unsigned integers")
_throw_named_error(typename, s, "values must be unsigned integers")
end
i = convert(basetype, ei)::basetype
if !iszero(i) && !ispow2(i)
_throw_error(typename, s, "values must be a positive power of 2")
_throw_named_error(typename, s, "values must be a positive power of 2")
end
else
_throw_error(typename, s)
_throw_named_error(typename, s)
end
if !Base.isidentifier(sym)
_throw_error(typename, s, "not a valid identifier")
_throw_named_error(typename, s, "not a valid identifier")
end
if (iszero(i) && maskzero) || (i & maskother) != 0
_throw_error(typename, s, "value is not unique")
_throw_named_error(typename, s, "value is not unique")
end
if sym in seen
_throw_error(typename, s, "name is not unique")
_throw_named_error(typename, s, "name is not unique")
end
push!(seen, sym)
push!(names, sym)
Expand Down Expand Up @@ -245,7 +318,6 @@ function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Un
permute!(values, order)

etypename = esc(typename)
ebasetype = esc(basetype)

n = length(names)
instances = Vector{Expr}(undef, n)
Expand All @@ -259,9 +331,9 @@ function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Un

blk = quote
# bitflag definition
Base.@__doc__(primitive type $etypename <: BitFlag{$ebasetype} $(8sizeof(basetype)) end)
primitive type $etypename <: BitFlag{$basetype} $(8sizeof(basetype)) end
function $etypename(x::Integer)
z = convert($ebasetype, x)
z = convert($basetype, x)
$membershiptest || _argument_error($(Expr(:quote, typename)), x)
return bitcast($etypename, z)
end
Expand All @@ -274,9 +346,26 @@ function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Un
end
Base.instances(::Type{$etypename}) = ($(instances...),)
$(flagconsts...)
nothing
end

if scope isa Symbol
escope = esc(scope)
blk = quote
baremodule $escope
$(blk.args...)
end
Base.@__doc__ $escope
nothing
end
else
blk = quote
$(blk.args...)
Base.@__doc__ $etypename
nothing
end
end
blk.head = :toplevel

return blk
end

Expand Down
85 changes: 85 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,88 @@ end
end
#end

#@testset "Scoped bit flags" begin
# Individual feature tests are less stringent since most of the generated code is
# the same as the extensively tested unscoped variety. Therefore, only do basic
# functionality tests, and then test for properties specific to the scoped definition.

# Inline definition
@bitflagx SFlag1 flag1a flag1b flag1c
@test SFlag1.T <: BitFlags.BitFlag
@test Int(SFlag1.flag1a) == 1
@test flag1a !== SFlag1.flag1a # new value is scoped and distinct from unscoped name

# Block definition
@bitflagx SFlag2 begin
flag2a
flag2b
flag2c
end
@test SFlag2.T <: BitFlags.BitFlag
@test Int(SFlag2.flag2a) == 1
@test flag2a !== SFlag2.flag2a # new value is scoped and distinct from unscoped name

# Inline definition with explicit type name
@bitflagx T=U SFlag3 S=2 T
@test SFlag3.U <: BitFlags.BitFlag
@test SFlag3.T isa SFlag3.U
@test Int(SFlag3.T) == 4

# Block definition with explicit type name
@bitflagx T=U SFlag4 begin
S = 2
T
end
@test SFlag4.U <: BitFlags.BitFlag
@test SFlag4.T isa SFlag4.U
@test Int(SFlag4.T) == 4

# Definition with explicit integer type
@bitflagx SFlag5::UInt8 flag1
@test typeof(Integer(SFlag5.flag1)) === UInt8

# Definition with both explicit integer type and type name
@bitflagx T=_T SFlag6::UInt8 flag1
@test SFlag6._T <: BitFlags.BitFlag
@test typeof(Integer(SFlag6.flag1)) === UInt8

# Documentation
"""My Docstring""" @bitflagx SDocFlag1 docflag
@test string(@doc(SDocFlag1)) == "My Docstring\n"
@doc raw"""Raw Docstring""" @bitflagx SDocFlag2 docflag
@test string(@doc(SDocFlag2)) == "Raw Docstring\n"

# Error conditions
# Too few arguments
@test_throws ArgumentError("bad macro call: @bitflagx A = B"
) @macrocall(@bitflagx A=B)
# Optional argument must be `T = $somesymbol`
@test_throws ArgumentError("bad macro call: @bitflagx A = B Foo flag"
) @macrocall(@bitflagx A=B Foo flag)
@test_throws ArgumentError("bad macro call: @bitflagx T = 1 Foo flag"
) @macrocall(@bitflagx T=1 Foo flag)

# Printing
@bitflagx SFilePerms::UInt8 NONE=0 READ=4 WRITE=2 EXEC=1
module ScopedSubModule
using ..BitFlags
@bitflagx SBits::UInt8 BIT_ONE BIT_TWO BIT_FOUR BIT_EIGHT
end

@test string(SFilePerms.NONE) == "NONE"
@test string(ScopedSubModule.SBits.BIT_ONE) == "BIT_ONE"
@test repr("text/plain", SFilePerms.T) ==
"""BitFlag Main.SFilePerms.T:
NONE = 0x00
EXEC = 0x01
WRITE = 0x02
READ = 0x04"""
@test repr("text/plain", ScopedSubModule.SBits.T) ==
"""BitFlag Main.ScopedSubModule.SBits.T:
BIT_ONE = 0x01
BIT_TWO = 0x02
BIT_FOUR = 0x04
BIT_EIGHT = 0x08"""
@test repr(SFilePerms.EXEC) == "EXEC::SFilePerms.T = 0x01"
@test repr(ScopedSubModule.SBits.BIT_ONE) == "BIT_ONE::Main.ScopedSubModule.SBits.T = 0x01"
#end

0 comments on commit 9c57d39

Please sign in to comment.