A flags type is a bit-set: a value that
holds any combination of a fixed set of named flags, packed into a
single integer. It's the type you reach for when something can be
several things at once — a collision mask (Ground | Wall),
an entity's state (Grounded | Attacking), an input bitmask,
a set of ability flags.
It's a close sibling of enum. An
enum is one of its variants; a flags value is any
subset of its flags. Where an enum gives you exhaustiveness in a
match, a flags type gives you type-safe set operations:
combine, test, and remove flags without hand-rolling &
/ | / << on bare integers (and getting
the precedence or the != 0 wrong).
flags Layer {
Ground,
Player,
Enemy,
Wall,
}
let mask = Layer.Ground | Layer.Player
print(Layer.Player in mask)
print(Layer.Enemy in mask)
print(mask.to_int())
Run it:
$ scua layer.scua
true
false
3
The declaration reads like an enum: the type name first, then the
flags in a comma-separated { } list, one per line (so you
can comment each one). That's the layout scua fmt produces
and keeps.
Each flag is one bit — Ground is 1,
Player is 2, Enemy is
4, Wall is 8 — so a combination
is just those bits OR-ed together (Ground | Player is
3). That's the same representation a hand-written bitmask
would use, so a flags value costs nothing extra at runtime; the
difference is that the compiler now knows its type.
#Building and testing a mask
Combine flags with | (union), and test membership with
in. flag in mask is true when every bit of the
left is set in the right — so it works for a single flag or a
combination:
flags Layer {
Ground,
Player,
Enemy,
Wall,
}
let collides = Layer.Player | Layer.Enemy | Layer.Wall
print(Layer.Player in collides) -- a single flag
print((Layer.Enemy | Layer.Wall) in collides) -- both must be set
print(Layer.Ground in collides)
$ scua in.scua
true
true
false
The other set operations: & is intersection (the
flags in both), ^ is toggle (symmetric difference), and
.without(...) removes flags — one, or several at once:
flags Layer {
Ground,
Player,
Enemy,
Wall,
}
let mask = Layer.Ground | Layer.Player | Layer.Wall
print(mask.without(Layer.Player).to_int()) -- remove one
print(mask.without(Layer.Player | Layer.Wall).to_int()) -- remove two at once
print(Layer.none.to_int())
print(Layer.all.to_int())
print(mask == Layer.none)
$ scua ops.scua
9
1
0
15
false
Every flags type comes with none (the empty set) and
all (every flag) for free, and you test for "no flags set"
by comparing with none.
#Type safety
A flags value is its own type, distinct from a plain int
and from every other flags type. Mixing two different flags
types — the classic "I OR-ed a collision layer into a state mask" bug —
is a compile error, not a value that's silently wrong:
flags Layer {
Ground,
Player,
}
flags State {
Idle,
Moving,
}
let oops = Layer.Ground | State.Moving
$ scua mix.scua
scua: mix.scua:9: type error: cannot combine a Layer with a State — different flags types
The same protection applies to comparing (==), testing
(in), and removing (.without(...)) across two
flags types. You can annotate bindings, parameters, and returns with the
flags type (let m: Layer = ...,
fn f(mask: Layer)), and the type flows through
| / & / .without(...) so it
stays checked.
#Bridging to and from an int
A flags value already is an int, but its type is the flags
type — so when you need the raw number (to store in a save, send to a
host, or print), call .to_int(). To go the other way,
Name.from_int(n) converts a raw int back to the flags type.
It is fail-closed: if the int carries a bit the type
doesn't define, it raises a fault rather than smuggle in a value the
type can't represent.
flags Layer {
Ground,
Player,
}
print(Layer.from_int(3).to_int()) -- ok: bits 0 and 1 are both declared
print(Layer.from_int(8)) -- bit 3 isn't a Layer flag
$ scua fromint.scua
3
scua: fromint.scua:6: from_int: this int has bits that flags `Layer` does not define
#Notes and limits
- A flags type holds up to 64 flags (one 64-bit word); declaring more is a compile error.
- Flags are positional — the first flag is bit 0, the next bit 1, and so on. When a flags type is saved and loaded across versions, only ever add flags at the end, so existing bits keep their meaning.
- Printing a flags value shows its underlying integer (like an enum), not the flag names.
#See also
- Enums — the closed-set sibling: one variant rather than a subset.
- Pattern matching —
matchover an enum's variants. - Records and gradual types — annotating bindings, parameters, and returns.