Skip to main content

Bitfield Types

A bitfield packs multiple small values (flags, counters, tiers) into a single EVM word. The compiler checks the layout at compile time and lowers reads/writes to efficient mask/shift operations.

Declaration

Explicit Layout

Use @at(offset, width) to place each field at a specific bit position:

bitfield AccountFlags : u256 {
is_admin : bool @at(0, 1);
is_frozen: bool @at(1, 1);
tier : u8 @at(2, 3); // 3 bits → 0..7
nonce : u16 @at(5, 16);
delta : i8 @at(21, 4); // 4 bits → -8..7 (signed)
}

You can also use @bits(start..end) as an alias — @bits(2..5) desugars to @at(2, 3):

bitfield Packed : u256 {
flag: bool @bits(0..1);
tier: u8 @bits(1..4);
nonce: u16 @bits(4..20);
}

Auto-Packed Layout

Omit @at() to let the compiler pack fields sequentially from bit 0:

bitfield AccountFlags : u256 {
is_admin : bool; // @at(0, 1)
is_frozen: bool; // @at(1, 1)
tier : u8(3); // @at(2, 3) — explicit 3-bit width
nonce : u16; // @at(5, 16) — uses full u16 width
}

Mixing @at() and auto-packed fields in the same bitfield is a compile error.

Field Types

TypeWidth RuleRange
boolalways 1 bit0 or 1
uNdefault N, or explicit uN(W)0 to 2^W − 1
iNdefault N, or explicit iN(W) (W ≥ 2)−2^(W−1) to 2^(W−1) − 1

Signed fields store two's complement values. Reads automatically sign-extend to the full word width using SHL + SAR.

Reading Fields

Dot notation reads a field — the compiler emits SHR + AND (unsigned) or adds SHL + SAR for sign extension (signed):

let flags: AccountFlags = ...;
let admin = flags.is_admin; // SHR(0) + AND(1)
let tier = flags.tier; // SHR(2) + AND(7)
let delta = flags.delta; // SHR(21) + AND(0xF) + SHL(252) + SAR(252)

Each read carries an implicit refinement — the compiler knows tier is in [0, 7], so guards like tier < 8 are eliminated at compile time.

Writing Fields

Dot notation writes use clear-then-set:

flags.tier = 5;
// Lowers to: cleared = flags & ~(0x7 << 2); updated = cleared | ((5 & 0x7) << 2)

Values are range-checked: assigning a value outside the field's range reverts by default. Use @truncate(value) for explicit silent truncation.

Construction

Build a bitfield with named fields. Omitted fields default to 0:

let flags = AccountFlags {
is_admin: true,
tier: 3,
}; // is_frozen = false, nonce = 0, delta = 0

If all values are compile-time constants, the entire word folds to a single constant.

Storage & Batching

A storage-backed bitfield occupies one slot, identical to its base integer type:

contract MyContract {
storage var flags: AccountFlags;

pub fn setup() {
// Single field write: SLOAD → modify → SSTORE
flags.is_admin = true;
}
}

Batching: consecutive field writes to the same storage bitfield are automatically batched into a single SLOAD/SSTORE cycle:

pub fn configure() {
flags.is_admin = true;
flags.tier = 5;
flags.nonce = 42;
// Compiler batches into:
// tmp = SLOAD(slot)
// tmp = clear/set is_admin
// tmp = clear/set tier
// tmp = clear/set nonce
// SSTORE(slot, tmp)
}

Cost: 1 SLOAD + N mask/shift sequences + 1 SSTORE, regardless of how many fields are updated.

Conversion

Convert between bitfield and raw integer with @bitCast:

let raw: u256 = @bitCast(u256, flags);           // bitfield → integer
let flags2: AccountFlags = @bitCast(AccountFlags, raw); // integer → bitfield

@bitCast performs no masking or validation. Use .sanitize() after casting from untrusted data.

Utility Methods

.zero()

Returns the all-zero bitfield value:

let empty = AccountFlags.zero();

.sanitize()

Clears all bits not owned by any declared field:

let raw: u256 = untrusted_input;
let flags = @bitCast(AccountFlags, raw).sanitize();
// Unused bits are now guaranteed to be 0

Gas Cost Summary

OperationEVM Cost
Unsigned field read~6 gas (SHR + AND)
Signed field read~12 gas (SHR + AND + SHL + SAR)
Field write (memory)~12–18 gas
Field write (storage, single)2100–5000 gas (SLOAD + ops + SSTORE)
Field write (storage, batched N)2100–5000 gas total
Construction (all constants)3 gas (single PUSH)

Relationship to Structs

  • Bitfields pack at bit granularity within a single word. Fields can be 1-bit bools, 3-bit integers, etc.
  • Structs pack at byte granularity across one or more slots.

A struct field can itself be a bitfield for maximum density:

packed struct Position {
owner: address, // 20 bytes
flags: PositionFlags, // 1 byte (bitfield over u8)
token_id: u32, // 4 bytes
}

For the full formal specification, see the Bitfield Spec.