Skip to content

Add Protocols.MemoryMap.Mask#1264

Merged
martijnbastiaan merged 1 commit into
mainfrom
martijn/mask-builtin
May 15, 2026
Merged

Add Protocols.MemoryMap.Mask#1264
martijnbastiaan merged 1 commit into
mainfrom
martijn/mask-builtin

Conversation

@martijnbastiaan

@martijnbastiaan martijnbastiaan commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

In Clash designs we often use Vec n Bool to track per-bit enables, valid bits, and similar. The default BitPackC representation of Vec n Bool allocates a full byte per bit, which is wasteful when these end up in memory-mapped registers. Switching to a BitVector is denser, but inverts the index correspondence: Vec index 0 maps to the MSB of the bitvector via Clash's standard pack/unpack, so a loop walking 0..n on the Haskell side walks the wrong direction relative to firmware that pokes individual bits.

Mask fixes both problems: it is packed compactly (like BitVector), and its toVec / fromVec / toBitVector / fromBitVector conversions all preserve the Vec-index <-> bit-position correspondence: index 0 is bit 0 (the least significant bit).

Lastly, it introduces a mask-focused interface on the Rust side.

Dear reviewer
I'm currently extending the native types clash-protocols-memmap and friends know about. This is because in the future I'd like to have Masks operate more efficiently for tasks common to them, e.g., testing a single bit. As far as I understand, the complete structure is currently fetched, after which the bit is tested. IMO that's out of scope for this PR though.

Note that the type is backed by Unsigned. I had originally implemented backed by BitVector, but this requires modulo operations in software. @hydrolarus pointed out that this isn't true because you can translate %8 to & 0b111.

AI disclaimer
You bet!

TODO

  • Write (regression) test
  • Update documentation, including docs/
  • Link to existing issue
  • Use BitVector-like backend in Rust
  • Add Mask![n] marcos

@hydrolarus hydrolarus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looking good! Nice work. Is the idea to eventually have this type be in clash-prelude?

Comment thread firmware-support/bittide-hal/src/manual_additions/mask.rs Outdated
@martijnbastiaan

Copy link
Copy Markdown
Contributor Author

Is the idea to eventually have this type be in clash-prelude?

Oh, I didn't even think of that! I'll open a Discussion.

@martijnbastiaan martijnbastiaan force-pushed the martijn/mask-builtin branch 2 times, most recently from 899f69f to d44ecbc Compare April 30, 2026 08:49
@martijnbastiaan martijnbastiaan marked this pull request as draft April 30, 2026 09:02

@hydrolarus hydrolarus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed off-PR, there's still some changes to be made, but here's still a naming thing I noticed!

Comment thread firmware-support/bittide-hal/src/manual_additions/mask.rs
Comment thread firmware-support/bittide-hal/src/manual_additions/mask.rs

@rslawson rslawson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to Lara's comments about test names, otherwise just some cleanup.

Comment thread firmware-support/bittide-hal/src/manual_additions/mask.rs Outdated
Comment on lines +185 to +197
impl<const M: u8, T> From<Unsigned<M, T>> for Mask<M, T> {
#[inline]
fn from(u: Unsigned<M, T>) -> Self {
Mask(u)
}
}

impl<const M: u8, T> From<Mask<M, T>> for Unsigned<M, T> {
#[inline]
fn from(m: Mask<M, T>) -> Self {
m.0
}
}

@rslawson rslawson Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to see these be a little more flexible on the size of the type of other. Maybe something like this?

impl<const M: u8, const N: u8, T, U> From<Unsigned<M, T>> for Mask<N, U>
where
    Unsigned<M, T>: UnsignedSizeCheck,
    Unsigned<N, U>: UnsignedSizeCheck,
    U: FromAs<T>,
{
    fn from(other: Unsigned<M, T>) -> Mask<N, U> {
        let _ = Unsigned::<M, T>::SIZE_CHECK;
        let _ = Unsigned::<N, U>::SIZE_CHECK;
        let _ = const {
            // ensure that M <= N here, else panic
        };
        if const { M == N } {
            Mask::<N, U>::from_unsigned(other)
        } else {
            Mask(Unsigned(other.0))
        }
    }
}

As well as From<Mask<N, U>> for Unsigned<M, T>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        let _ = const {
            // ensure that M <= N here, else panic
        };

That's super interesting Rust lets you do that! I think it means that this will trigger a compile error, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you check whether the current implementation is what you intended?

@rslawson rslawson May 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        let _ = const {
            // ensure that M <= N here, else panic
        };

That's super interesting Rust lets you do that! I think it means that this will trigger a compile error, right?

Exactly, yep! It'll fail to evaluate the const { ... } block so it'll produce an error at comptime. You can just write something like

let _ = const {
    if M > N {
        panic!("Cannot infallibly convert if the `Unsigned` is larger than the `Mask`!");
    }
};

And then it'll fail to compile at call sites that are invalid.

Could you check whether the current implementation is what you intended?

This is still missing from the current implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I messed up when reverting to the BV implementation, it's monomorphic again..

@martijnbastiaan martijnbastiaan force-pushed the martijn/mask-builtin branch 2 times, most recently from efad2fc to 5fd8699 Compare May 7, 2026 12:48
@martijnbastiaan martijnbastiaan marked this pull request as ready for review May 11, 2026 12:30

@hydrolarus hydrolarus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better with the BitVector backing! Still some naming discrepancies and small suggesetions, but it's look good!

Comment thread clash-protocols-memmap/tests/Tests/Protocols/MemoryMap/Mask.hs
Comment thread firmware-support/bittide-hal/src/manual_additions/mask.rs Outdated
Comment thread firmware-support/bittide-hal/src/manual_additions/mask.rs
/// ```
#[proc_macro]
#[allow(non_snake_case)]
pub fn Mask(toks: proc_macro::TokenStream) -> proc_macro::TokenStream {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! It would be cool if there was also a value-level macro mask!(0b...., n = 32), but if you don't want to add that in this PR I think that would be fine too!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll open another PR :)

@hydrolarus hydrolarus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good to me! One more tiny naming nit-pick but it's not a blocker to get this merged IMO

Comment thread firmware-support/bittide-hal/src/manual_additions/mask.rs Outdated

@hydrolarus hydrolarus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@martijnbastiaan martijnbastiaan force-pushed the martijn/mask-builtin branch 2 times, most recently from 18a8d25 to e48207a Compare May 12, 2026 08:23
In Clash designs we often use `Vec n Bool` to track per-bit enables, valid
bits, and similar. The default `BitPackC` representation of `Vec n Bool`
allocates a full byte per bit, which is wasteful when these end up in
memory-mapped registers. Switching to a `BitVector` is denser, but inverts the
index correspondence: `Vec` index `0` maps to the MSB of the bitvector via
Clash's standard `pack`/`unpack`, so a loop walking `0..n` on the Haskell side
walks the wrong direction relative to firmware that pokes individual bits.

`Mask` fixes both problems: it is packed compactly (like `BitVector`), and its
`toVec` / `fromVec` / `toBitVector` / `fromBitVector` conversions all preserve
the `Vec`-index <-> bit-position correspondence: index `0` is bit `0` (the least
significant bit).

Lastly, it introduces a mask-focused interface on the Rust side.
@martijnbastiaan martijnbastiaan enabled auto-merge (squash) May 15, 2026 06:10
@martijnbastiaan martijnbastiaan merged commit 7d1731a into main May 15, 2026
48 checks passed
@martijnbastiaan martijnbastiaan deleted the martijn/mask-builtin branch May 15, 2026 06:15
lmbollen pushed a commit that referenced this pull request May 18, 2026
In Clash designs we often use `Vec n Bool` to track per-bit enables, valid
bits, and similar. The default `BitPackC` representation of `Vec n Bool`
allocates a full byte per bit, which is wasteful when these end up in
memory-mapped registers. Switching to a `BitVector` is denser, but inverts the
index correspondence: `Vec` index `0` maps to the MSB of the bitvector via
Clash's standard `pack`/`unpack`, so a loop walking `0..n` on the Haskell side
walks the wrong direction relative to firmware that pokes individual bits.

`Mask` fixes both problems: it is packed compactly (like `BitVector`), and its
`toVec` / `fromVec` / `toBitVector` / `fromBitVector` conversions all preserve
the `Vec`-index <-> bit-position correspondence: index `0` is bit `0` (the least
significant bit).

Lastly, it introduces a mask-focused interface on the Rust side.
@martijnbastiaan martijnbastiaan mentioned this pull request May 27, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants