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 d3d6a84
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 32 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "BitFlags"
uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35"
authors = ["Justin Willmert <[email protected]>"]
version = "0.1.7"
version = "0.1.8"

[compat]
julia = "1"
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,43 @@ Stacktrace:
...
```

In the above examples, 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
```
Because the module is named `TrigFunction`, the generated type must have
a different name.
By default, the name of the type is `T`, but it may be overridden by choosing
using the keyword option `T = new_name` as the first argument:
```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
148 changes: 117 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,12 @@ 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)
errmsg = "bad macro call: $(Expr(:macrocall, Symbol(macroname), nothing, args...))"
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 +148,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 +180,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

"""
@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 _bitflag(__module__::Module, T::Union{Symbol, Expr}, x::Vector{Any})
if T isa Symbol
typename = T
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 +271,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 +315,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 +328,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 +343,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

2 comments on commit d3d6a84

@jmert
Copy link
Owner Author

@jmert jmert commented on d3d6a84 Nov 13, 2023

Choose a reason for hiding this comment

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

@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/95228

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

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.1.8 -m "<description of version>" d3d6a845408e476e11a7200c477c05aacf5f4dcc
git push origin v0.1.8

Please sign in to comment.