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.
A mask stays a Layer for its whole life — combining,
testing and removing flags all give you back a Layer, and
it prints as the set it is rather than as a number you have to decode.
Use .to_int() when you actually want the integer, at a wire
or C boundary.
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(mask)
print(Layer.Player in mask)
print(Layer.Enemy in mask)
print(mask.to_int())
Run it:
$ scua layer.scua
Layer.Ground|Layer.Player
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.
#Asking "has all" or "has any"
There are two questions you can ask of a mask, and they have
different answers. in asks whether it has
all of some flags. For any of them,
intersect with & and compare the result with
none:
flags Layer {
Ground,
Player,
Enemy,
Wall,
}
let hits = Layer.Enemy
let solid = Layer.Ground | Layer.Wall
const opts = {}
print((Layer.Enemy | Layer.Wall) in hits) -- has all: no
print(hits & (Layer.Enemy | Layer.Wall) != Layer.none) -- has any: yes
print(hits & solid == Layer.none) -- has none of these
print(opts.mask ?? Layer.Ground) -- a default when missing
$ scua any.scua
false
true
true
Layer.Ground
A mask is never true or false by itself. Even Layer.none
is a value, so if hits & Layer.Wall then would always
be true, and SCUA refuses it rather than guess which question you
meant:
flags Layer {
Ground,
Player,
Enemy,
Wall,
}
let hits = Layer.Enemy
if hits & Layer.Wall then print("blocked") end
$ scua setcond.scua
scua: setcond.scua:8: type error: a condition got a flags set (Layer): a set is never true or false. Say which test you mean:
has all of these: Layer.Ground in p (Layer.Ground | Layer.Player) in p
has any of these: p & (Layer.Ground | Layer.Player) != Layer.none
is empty: p == Layer.none
When the checker can't see the type, the same message comes at run
time, with the set's actual value. For a default when a mask may be
missing, use ?? (opts.mask ?? Layer.Ground),
not or: or would test the mask as a condition.
And don't get around the rule with .to_int():
if hits.to_int() & 8 then is true for 0 as
well, since 0 is a true value, which is the same bug with
extra steps. The checker refuses it too.
#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.