Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Bits & Machine Numbers  ›  Lesson

Masks, Flags & Packed Fields

Masks & Flags 25 minA mask picks out a lane of bits, so several small fields can share one word
You're building a piece ofQuarry — the record encoder
This piece — pack_fields(): Packs several small fields into one word, and refuses any field that doesn't fit.
Scenario Every record Quarry writes gets a 32-bit header: id, shard, type and epoch side by side. The field packer builds that word, and refuses any value that would spill into its neighbour and silently turn one record into another.
Your task
Build pack_fields(values, widths). Pack the values into one integer, first field in the most significant bits, each field exactly widths[i] bits wide. Return None if any value is negative or does not fit its width. Example: pack_fields([5, 3, 1], [4, 2, 1]) → 47.

Masks, Flags & Packed Fields

In short: a mask is a number whose bits mark the positions you care about. x | (1<<k) sets bit k, x & ~(1<<k) clears it, x ^ (1<<k) toggles it, and (x>>k) & 1 tests it. Shift a mask of w ones, (1<<w) - 1, to cover any run of bits and you can pack several fields side by side in one word: shift a value up by its field's position to pack it, shift and mask to unpack it. A field that's too wide for its lane spills silently into the lane next door, so a packer has to check widths, not just trust them.

One 32-bit word split into four lanes — id, shard, type, epoch — each field shifted into its own range of bits

Lesson 1 packed a Quarry record header into one 32-bit word with a line that looked like magic: (3071 << 20) | (17 << 14) | (2 << 11) | 1500. This lesson takes that line apart. It covers the four things you do to a single bit, then masks, the patterns of 1s that pick out several bits at once. Then it packs whole fields side by side in one word. Most binary formats you'll meet work this way: file headers, network packets, pixel formats, CPU instructions and database pages.

One bit at a time: set, clear, toggle, test

Build a word with a single 1 at position k with 1 << k, and combine it with the three operators from lesson 1:

To… Write Why it works
set bit k x | (1 << k) OR with 1 forces a 1, OR with 0 changes nothing
clear bit k x & ~(1 << k) the mask is all 1s except at k, and AND with 0 forces a 0
toggle bit k x ^ (1 << k) XOR with 1 flips, XOR with 0 keeps
test bit k (x >> k) & 1 slide bit k down to position 0 and keep only it

Quarry's status word gives each flag a name, so no one has to remember which bit is which:

python

~DIRTY is negative in Python, because it has infinitely many 1s on the left. That's exactly what you want in a clear mask: every bit except bit 0 survives the AND, however wide status is.

❓ Cross-question — "Isn't there a library for this?" Yes. enum.IntFlag gives named flags that combine with | and print readably. Use it in application code. The operators underneath are the same ones shown here, and a storage engine that packs millions of headers uses them directly.

python

Precedence: where bit code goes wrong

Shifts bind more loosely than + and -, and &, ^, | bind more loosely still (in that order). Comparisons come after all of them. So x & 4 == 4 means (x & 4) == 4 in Python, unlike in C, where it means x & (4 == 4). But arithmetic can still surprise you:

What does this print?
print(1 << 3 + 1)

Masks select many bits at once

A mask of w ones, (1 << w) - 1, selects the low w bits. Shift it to cover any field, meaning any run of adjacent bits. Quarry's header has four fields, packed from the top:

Field Bits Width Largest value Mask (after shifting down)
id 31–20 12 4,095 0xFFF
shard 19–14 6 63 0x3F
type 13–11 3 7 0x7
epoch 10–0 11 2,047 0x7FF

A field's shift is the number of bits below it. To pack a value, move it up by its shift and OR it in. To unpack, shift it back down and mask off everything above it. Step through packing record 2740 on shard 37, type 5, epoch 1234. Then watch what happens when someone stores a shard of 64 in a 6-bit field:

watch it move · array

Four fields share one word. Each is shifted into its own lane, and a value too wide for its lane spills into the lane next door.

id = 2740 is 101010110100. It's shifted left by 20 and ORed in, filling bits 31 to 20. 32 cells.

1 / 6

That last step is the bug that matters. Nothing crashed, and the word is still 32 bits, but it describes a different record. A packer has to either mask every value to its width or refuse values that don't fit. For a storage engine, refusing is the right answer. That's the task at the end of this lesson.

What does this print?
w = 0xAB496CD2
print((w >> 20) & 0xFFF, (w >> 11) & 0x7)

Order matters in unpacking. Mask after the shift, with the mask at the bottom, or mask before it with the mask shifted up. Mixing the two drops the field:

broken — fix it

It prints 0 0. The mask 0x3F keeps bits 5 to 0, which belong to the epoch, and then the shift throws even those away.

Changing one field in place

To overwrite a single field, clear its lane with an inverted mask, then OR in the new value. Every other field is left alone:

step through it
1w = 0xAB496CD2
2shift, width = 14, 6
3lane = ((1 << width) - 1) << shift
4w = w & ~lane
5w = w | (12 << shift)
6print((w >> 20) & 0xFFF, (w >> 14) & 0x3F, w & 0x7FF)

The trace moves the record to shard 12, and id 2740 and epoch 1234 come through unchanged. That's the reason for masks: you change one field without unpacking the rest.

Now pack a header with your own code and watch each field land in its lane. Change the values, or leave out the & mask and give the shard 64, and see the spill for yourself:

your code, drawn · array

Your packer places each field in its lane, and the highlight shows the lane it wrote.

How wide should each field be?

The id and the epoch share 23 bits between them, and the shard and type take the other 9. Give the id w bits and the epoch gets 23 - w. A segment needs up to 4,000 distinct ids, and a record must survive 2,000 rewrites before its epoch wraps:

id bits w ids (2**w) epochs (2**(23 - w))
10 1,024 8,192
11 2,048 4,096
12 4,096 2,048
13 8,192 1,024
14 16,384 512

Only w = 12 meets both targets. One bit fewer and ids collide, one bit more and epochs wrap after 1,024 rewrites. A fixed-width layout is always a trade like this between fields.

Interview corner — two masks, one pass

You're given a list of status words and two masks. need holds the flags a record must have, and forbid holds the flags it must not have. Count the matching records. You need two ANDs per word and nothing else. "All of need" means w & need == need. It's a common mistake to test w & need != 0, which only checks for any of the flags. "None of forbid" means w & forbid == 0:

python

The query engine builds these two masks once per query, and then each record costs O(1) time — two machine instructions — to check, however many flags the query mentions, so scanning n records is O(n) time and O(1) extra space.

Coming from C: C gives you bit-fields in structs, struct { unsigned id:12; … }, but their layout is implementation-defined, so you can't trust them for data that goes to disk. Portable code uses shifts and masks, exactly as here.

Quick practice

What does this print?
x = 0b10110
print((x >> 0) & 1, (x >> 2) & 1, (x >> 4) & 1)
broken — fix it

It prints 14, clearing the wrong bit. Fix clear_bit so it clears bit k specifically.


🎯 Your turn

Write pack_fields(values, widths). The first field goes in the most significant bits and each later field sits below it. Return the packed integer, or None if any value is negative or doesn't fit its width. Quarry refuses to store a header it would corrupt.

  • pack_fields([5, 3, 1], [4, 2, 1]) → 47 (1011 11 1)
  • pack_fields([2740, 37, 5, 1234], [12, 6, 3, 11]) → 2873715922
  • pack_fields([64], [6]) → None

Hint — loop over the fields in order, shifting the word left by each width and ORing the value in. That way the first field ends up on top without computing any shifts up front.

Then press ▶ Run, try the Live App chips, and hit ✓ Check. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upIs this flag on?

Write has_flag(word, bit) returning True if bit number bit of word is 1, else False. Bit 0 is the least significant.

bitmaskbitwise
DrillReplay a list of flag changes

Write apply_ops(word, ops). Each op is [action, bit], where action is "set", "clear" or "toggle". Apply them in order and return the final word. Setting a bit that's already set, or clearing one that's already clear, changes nothing.

set-bitclear-bit
BuildUnpack a header

Write unpack_fields(word, widths), the inverse of pack_fields. The first width names the most significant field. Return the list of field values, top field first. unpack_fields(47, [4, 2, 1]) is [5, 3, 1].

bitmaskbit-shift
BossAdd packed counters, field by field

Quarry keeps several small hit counters packed in one word, laid out by widths, top field first. Write add_packed(a, b, widths) that adds two such words field by field. A carry must never leak from one counter into the next, and each counter saturates: if a sum is too big for its width it sticks at the maximum 2**width - 1. Return the packed result.

bitmaskbit-manipulation
CapstoneMigrate a header to a new layout

Quarry is changing its header layout. Write repack(word, old_widths, new_widths). Both layouts have the same number of fields, in the same order, top field first. Unpack word with the old layout, pack the values with the new layout, and return the new word. Return None if any value doesn't fit its new width. A migration must never silently corrupt a record.

bit-manipulationbitmask
ExplainMask it, or refuse it?

A reviewer suggests simplifying Quarry's packer: instead of returning None when a value is too wide, just write (value & mask) << shift so every call succeeds. Explain what would happen to a record written with shard 64 into the 6-bit shard field under each version, and argue which behaviour a storage engine should have.

pack_fields.py