Skip to content

Commit d3d6a84

Browse files
committed
Add macro interface @bitflagx to scope definitions to a module
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.
1 parent e222c1e commit d3d6a84

File tree

4 files changed

+240
-32
lines changed

4 files changed

+240
-32
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "BitFlags"
22
uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35"
33
authors = ["Justin Willmert <[email protected]>"]
4-
version = "0.1.7"
4+
version = "0.1.8"
55

66
[compat]
77
julia = "1"

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,43 @@ Stacktrace:
6464
...
6565
```
6666

67+
In the above examples, both the bit flag type and member instances are added to
68+
the surrounding scope.
69+
If some members have common or conflicting names — or if scoped names are
70+
simply desired on principle — the `@bitflagx` macro can be used instead.
71+
This variation supports the same features and syntax as `@bitflag` (with
72+
respect to choosing the base integer type, inline versus block definitions,
73+
and setting particular flag values), but the definitions are instead placed
74+
within a [bare] module, avoiding adding anything but the module name to the
75+
surrounding scope.
76+
77+
For example, the following avoids shadowing the `sin` function:
78+
```julia
79+
julia> @bitflagx TrigFunctions sin cos tan csc sec cot
80+
81+
julia> TrigFunctions.sin
82+
sin::TrigFunctions.T = 0x00000001
83+
84+
julia> sin(π)
85+
0.0
86+
87+
julia> print(typeof(TrigFunctions.sin))
88+
Main.TrigFunctions.T
89+
```
90+
Because the module is named `TrigFunction`, the generated type must have
91+
a different name.
92+
By default, the name of the type is `T`, but it may be overridden by choosing
93+
using the keyword option `T = new_name` as the first argument:
94+
```julia
95+
julia> @bitflag T=type HyperbolicTrigFunctions sinh cosh tanh csch sech coth
96+
97+
julia> HyperbolicTrigFunctions.tanh
98+
tanh::HyperbolicTrigFunctions.type = 0x00000004
99+
100+
julia> print(typeof(HyperbolicTrigFunctions.tanh))
101+
Main.HyperbolicTrigFunctions.type
102+
```
103+
67104
## Printing
68105

69106
Each flag value is then printed with contextual information which is more

src/BitFlags.jl

Lines changed: 117 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module BitFlags
77
import Core.Intrinsics.bitcast
88
import Base.Meta.isexpr
99

10-
export BitFlag, @bitflag
10+
export BitFlag, @bitflag, @bitflagx
1111

1212
function namemap end
1313
function haszero end
@@ -78,9 +78,30 @@ function Base.show(io::IO, x::BitFlag)
7878
print(io, x)
7979
else
8080
print(io, x, "::")
81-
# explicitly setting :compact => false prints the type with its
82-
# "contextual path", i.e. MyFlag (for Main.MyFlag) or Main.SubModule.OtherFlags
83-
show(IOContext(io, :compact => false), typeof(x))
81+
82+
T = typeof(x)
83+
Tdef = parentmodule(T)
84+
from = get(io, :module, @static isdefined(Base, :active_module) ? Base.active_module() : Main)
85+
86+
# Detect a scoped BitFlag inside a baremodule by looking for the implicit import
87+
# of Base bindings. For scoped bitflags, we actually care about whether the
88+
# module itself is visible instead of the type.
89+
isscoped = !isdefined(Tdef, :Base)
90+
sym = nameof(!isscoped ? T : Tdef)
91+
refmod = !isscoped ? Tdef : parentmodule(Tdef)
92+
if from === nothing || !Base.isvisible(sym, refmod, from)
93+
if !isscoped
94+
print(io, refmod, ".", sym)
95+
else
96+
print(io, Tdef, ".", nameof(T))
97+
end
98+
else
99+
if !isscoped
100+
print(io, sym)
101+
else
102+
print(io, nameof(Tdef), ".", nameof(T))
103+
end
104+
end
84105
print(io, " = ")
85106
show(io, Integer(x))
86107
end
@@ -103,7 +124,12 @@ end
103124
throw(ArgumentError("invalid value for BitFlag $typename: $x"))
104125
end
105126

106-
@noinline function _throw_error(typename, s, msg = nothing)
127+
@noinline function _throw_macro_error(macroname, args)
128+
errmsg = "bad macro call: $(Expr(:macrocall, Symbol(macroname), nothing, args...))"
129+
throw(ArgumentError(errmsg))
130+
end
131+
132+
@noinline function _throw_named_error(typename, s, msg = nothing)
107133
errmsg = "invalid argument for BitFlag $typename: $s"
108134
if msg !== nothing
109135
errmsg *= "; " * msg
@@ -122,14 +148,14 @@ Create a `BitFlag{BaseType}` subtype with name `BitFlagName` and flag member val
122148
```jldoctest itemflags
123149
julia> @bitflag Items apple=1 fork=2 napkin=4
124150
125-
julia> f(x::Items) = "I'm an Item with value: \$x"
151+
julia> f(x::Items) = "I'm a flag with value: \$x"
126152
f (generic function with 1 method)
127153
128154
julia> f(apple)
129-
"I'm an Item with value: apple"
155+
"I'm a flag with value: apple"
130156
131157
julia> f(apple | fork)
132-
"I'm an Item with value: apple | fork"
158+
"I'm a flag with value: (apple | fork)"
133159
```
134160
135161
Values can also be specified inside a `begin` block, e.g.
@@ -154,34 +180,78 @@ julia> instances(Items)
154180
```
155181
"""
156182
macro bitflag(T::Union{Symbol, Expr}, x::Union{Symbol, Expr}...)
157-
return _bitflag(__module__, T, Any[x...])
183+
flagname, basetype = _parse_name(__module__, T)
184+
return _bitflag(__module__, nothing, flagname, basetype, Any[x...])
185+
end
186+
187+
"""
188+
@bitflagx [T=FlagTypeName] BitFlagName[::BaseType] value1[=x] value2[=y]
189+
190+
Like [`@bitflag`](@ref) but instead scopes the new type `FlagTypeName` (named `T` if not
191+
overridden via the first optional argument) and member constants within a module named
192+
`BitFlagName`.
193+
194+
# Examples
195+
```jldoctest scopedflags
196+
julia> @bitflagx ScopedItems apple=1 fork=2 napkin=4
197+
198+
julia> f(x::ScopedItems.T) = "I'm a scoped flag with value: \$x"
199+
f (generic function with 1 method
200+
201+
julia> f(ScopedItems.apple | ScopedItems.fork)
202+
"I'm a scoped flag with value: (fork | apple)"
203+
"""
204+
macro bitflagx(arg1::Union{Symbol, Expr}, args::Union{Symbol, Expr}...)
205+
self = Symbol("@bitflagx")
206+
x = Any[args...]
207+
if isexpr(arg1, :(=), 2) && (e = arg1::Expr; (e.args[1] === :T && e.args[2] isa Symbol))
208+
# For this case, we need to decompose and swap symbols:
209+
# - `FlagTypeName` in `T = FlagTypeName` needs to get moved to the flagexpr argument
210+
# - `BitFlagName` in `BitFlagName[::BaseType]` becomes the scope name
211+
length(x) < 1 && _throw_macro_error(self, (arg1, args...))
212+
arg2 = popfirst!(x)
213+
flagname = arg1.args[2]
214+
scope, basetype = _parse_name(__module__, arg2)
215+
return _bitflag(__module__, scope, flagname, basetype, x)
216+
elseif isexpr(arg1, :(::), 2) && (e = arg1::Expr; e.args[1] isa Symbol)
217+
scope, basetype = _parse_name(__module__, arg1)
218+
return _bitflag(__module__, scope, :T, basetype, x)
219+
elseif arg1 isa Symbol
220+
return _bitflag(__module__, arg1, :T, UInt32, x)
221+
else
222+
_throw_macro_error(self, (arg1, args...))
223+
end
158224
end
159225

160-
function _bitflag(__module__::Module, T::Union{Symbol, Expr}, x::Vector{Any})
161-
if T isa Symbol
162-
typename = T
226+
function _parse_name(__module__::Module, flagexpr::Union{Symbol, Expr})
227+
if flagexpr isa Symbol
228+
flagname = flagexpr
163229
basetype = UInt32
164-
elseif isexpr(T, :(::), 2) && (e = T::Expr; e.args[1] isa Symbol)
165-
typename = e.args[1]::Symbol
230+
elseif isexpr(flagexpr, :(::), 2) && (e = flagexpr::Expr; e.args[1] isa Symbol)
231+
flagname = e.args[1]::Symbol
166232
baseexpr = Core.eval(__module__, e.args[2])
167233
if !(baseexpr isa DataType) || !(baseexpr <: Unsigned) || !isbitstype(baseexpr)
168-
_throw_error(typename, T, "base type must be a bitstype unsigned integer")
234+
_throw_named_error(flagname, flagexpr, "base type must be a bitstype unsigned integer")
169235
end
170236
basetype = baseexpr::Type{<:Unsigned}
171237
else
172-
_throw_error(T, "bad expression head")
238+
_throw_named_error(flagexpr, "bad expression head")
173239
end
174-
if isempty(x)
175-
throw(ArgumentError("no arguments given for BitFlag $typename"))
176-
elseif length(x) == 1 && isexpr(x[1], :block)
240+
return (flagname, basetype)
241+
end
242+
243+
function _bitflag(__module__::Module, scope::Union{Symbol, Nothing}, flagname::Symbol, basetype::Type{<:Unsigned}, x::Vector{Any})
244+
isempty(x) && throw(ArgumentError("no arguments given for BitFlag $flagname"))
245+
if length(x) == 1 && isexpr(x[1], :block)
177246
syms = (x[1]::Expr).args
178247
else
179248
syms = x
180249
end
181-
return _bitflag_impl(__module__, typename, basetype, syms)
250+
return _bitflag_impl(__module__, scope, flagname, basetype, syms)
182251
end
183252

184-
function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Unsigned}, syms::Vector{Any})
253+
function _bitflag_impl(__module__::Module, scope::Union{Symbol, Nothing}, typename::Symbol, basetype::Type{<:Unsigned},
254+
syms::Vector{Any})
185255
names = Vector{Symbol}()
186256
values = Vector{basetype}()
187257
seen = Set{Symbol}()
@@ -201,23 +271,23 @@ function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Un
201271
sym = e.args[1]::Symbol
202272
ei = Core.eval(__module__, e.args[2]) # allow exprs, e.g. uint128"1"
203273
if !(ei isa Integer)
204-
_throw_error(typename, s, "values must be unsigned integers")
274+
_throw_named_error(typename, s, "values must be unsigned integers")
205275
end
206276
i = convert(basetype, ei)::basetype
207277
if !iszero(i) && !ispow2(i)
208-
_throw_error(typename, s, "values must be a positive power of 2")
278+
_throw_named_error(typename, s, "values must be a positive power of 2")
209279
end
210280
else
211-
_throw_error(typename, s)
281+
_throw_named_error(typename, s)
212282
end
213283
if !Base.isidentifier(sym)
214-
_throw_error(typename, s, "not a valid identifier")
284+
_throw_named_error(typename, s, "not a valid identifier")
215285
end
216286
if (iszero(i) && maskzero) || (i & maskother) != 0
217-
_throw_error(typename, s, "value is not unique")
287+
_throw_named_error(typename, s, "value is not unique")
218288
end
219289
if sym in seen
220-
_throw_error(typename, s, "name is not unique")
290+
_throw_named_error(typename, s, "name is not unique")
221291
end
222292
push!(seen, sym)
223293
push!(names, sym)
@@ -245,7 +315,6 @@ function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Un
245315
permute!(values, order)
246316

247317
etypename = esc(typename)
248-
ebasetype = esc(basetype)
249318

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

260329
blk = quote
261330
# bitflag definition
262-
Base.@__doc__(primitive type $etypename <: BitFlag{$ebasetype} $(8sizeof(basetype)) end)
331+
primitive type $etypename <: BitFlag{$basetype} $(8sizeof(basetype)) end
263332
function $etypename(x::Integer)
264-
z = convert($ebasetype, x)
333+
z = convert($basetype, x)
265334
$membershiptest || _argument_error($(Expr(:quote, typename)), x)
266335
return bitcast($etypename, z)
267336
end
@@ -274,9 +343,26 @@ function _bitflag_impl(__module__::Module, typename::Symbol, basetype::Type{<:Un
274343
end
275344
Base.instances(::Type{$etypename}) = ($(instances...),)
276345
$(flagconsts...)
277-
nothing
346+
end
347+
348+
if scope isa Symbol
349+
escope = esc(scope)
350+
blk = quote
351+
baremodule $escope
352+
$(blk.args...)
353+
end
354+
Base.@__doc__ $escope
355+
nothing
356+
end
357+
else
358+
blk = quote
359+
$(blk.args...)
360+
Base.@__doc__ $etypename
361+
nothing
362+
end
278363
end
279364
blk.head = :toplevel
365+
280366
return blk
281367
end
282368

test/runtests.jl

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,88 @@ end
286286
end
287287
#end
288288

289+
#@testset "Scoped bit flags" begin
290+
# Individual feature tests are less stringent since most of the generated code is
291+
# the same as the extensively tested unscoped variety. Therefore, only do basic
292+
# functionality tests, and then test for properties specific to the scoped definition.
293+
294+
# Inline definition
295+
@bitflagx SFlag1 flag1a flag1b flag1c
296+
@test SFlag1.T <: BitFlags.BitFlag
297+
@test Int(SFlag1.flag1a) == 1
298+
@test flag1a !== SFlag1.flag1a # new value is scoped and distinct from unscoped name
299+
300+
# Block definition
301+
@bitflagx SFlag2 begin
302+
flag2a
303+
flag2b
304+
flag2c
305+
end
306+
@test SFlag2.T <: BitFlags.BitFlag
307+
@test Int(SFlag2.flag2a) == 1
308+
@test flag2a !== SFlag2.flag2a # new value is scoped and distinct from unscoped name
309+
310+
# Inline definition with explicit type name
311+
@bitflagx T=U SFlag3 S=2 T
312+
@test SFlag3.U <: BitFlags.BitFlag
313+
@test SFlag3.T isa SFlag3.U
314+
@test Int(SFlag3.T) == 4
315+
316+
# Block definition with explicit type name
317+
@bitflagx T=U SFlag4 begin
318+
S = 2
319+
T
320+
end
321+
@test SFlag4.U <: BitFlags.BitFlag
322+
@test SFlag4.T isa SFlag4.U
323+
@test Int(SFlag4.T) == 4
324+
325+
# Definition with explicit integer type
326+
@bitflagx SFlag5::UInt8 flag1
327+
@test typeof(Integer(SFlag5.flag1)) === UInt8
328+
329+
# Definition with both explicit integer type and type name
330+
@bitflagx T=_T SFlag6::UInt8 flag1
331+
@test SFlag6._T <: BitFlags.BitFlag
332+
@test typeof(Integer(SFlag6.flag1)) === UInt8
333+
334+
# Documentation
335+
"""My Docstring""" @bitflagx SDocFlag1 docflag
336+
@test string(@doc(SDocFlag1)) == "My Docstring\n"
337+
@doc raw"""Raw Docstring""" @bitflagx SDocFlag2 docflag
338+
@test string(@doc(SDocFlag2)) == "Raw Docstring\n"
339+
340+
# Error conditions
341+
# Too few arguments
342+
@test_throws ArgumentError("bad macro call: @bitflagx A = B"
343+
) @macrocall(@bitflagx A=B)
344+
# Optional argument must be `T = $somesymbol`
345+
@test_throws ArgumentError("bad macro call: @bitflagx A = B Foo flag"
346+
) @macrocall(@bitflagx A=B Foo flag)
347+
@test_throws ArgumentError("bad macro call: @bitflagx T = 1 Foo flag"
348+
) @macrocall(@bitflagx T=1 Foo flag)
349+
350+
# Printing
351+
@bitflagx SFilePerms::UInt8 NONE=0 READ=4 WRITE=2 EXEC=1
352+
module ScopedSubModule
353+
using ..BitFlags
354+
@bitflagx SBits::UInt8 BIT_ONE BIT_TWO BIT_FOUR BIT_EIGHT
355+
end
356+
357+
@test string(SFilePerms.NONE) == "NONE"
358+
@test string(ScopedSubModule.SBits.BIT_ONE) == "BIT_ONE"
359+
@test repr("text/plain", SFilePerms.T) ==
360+
"""BitFlag Main.SFilePerms.T:
361+
NONE = 0x00
362+
EXEC = 0x01
363+
WRITE = 0x02
364+
READ = 0x04"""
365+
@test repr("text/plain", ScopedSubModule.SBits.T) ==
366+
"""BitFlag Main.ScopedSubModule.SBits.T:
367+
BIT_ONE = 0x01
368+
BIT_TWO = 0x02
369+
BIT_FOUR = 0x04
370+
BIT_EIGHT = 0x08"""
371+
@test repr(SFilePerms.EXEC) == "EXEC::SFilePerms.T = 0x01"
372+
@test repr(ScopedSubModule.SBits.BIT_ONE) == "BIT_ONE::Main.ScopedSubModule.SBits.T = 0x01"
373+
#end

0 commit comments

Comments
 (0)