Re: [PATCH v7 05/10] rust: io: add IoLoc and IoWrite types
From: Alexandre Courbot
Date: Fri Mar 06 2026 - 09:35:54 EST
On Fri Mar 6, 2026 at 10:20 PM JST, Gary Guo wrote:
> On Fri Mar 6, 2026 at 12:50 PM GMT, Alexandre Courbot wrote:
>> On Fri Mar 6, 2026 at 8:35 PM JST, Gary Guo wrote:
>>> On Fri Mar 6, 2026 at 11:10 AM GMT, Alexandre Courbot wrote:
>>>> On Fri Mar 6, 2026 at 7:42 PM JST, Gary Guo wrote:
>>>>> On Fri Mar 6, 2026 at 5:37 AM GMT, Alexandre Courbot wrote:
>>>>>> On Thu Mar 5, 2026 at 7:15 AM JST, Gary Guo wrote:
>>>>>>> On Wed Mar 4, 2026 at 9:38 PM GMT, Danilo Krummrich wrote:
>>>>>>>> On Wed Mar 4, 2026 at 10:13 PM CET, Gary Guo wrote:
>>>>>>>>> Even for the cases where there's a PIO register, I think it's beneficial to just
>>>>>>>>> get a value without a type.
>>>>>>>>>
>>>>>>>>> I don't see why we want people to write
>>>>>>>>>
>>>>>>>>> self.io.read(UART_RX).value()
>>>>>>>>>
>>>>>>>>> vs
>>>>>>>>>
>>>>>>>>> self.io.read(UART_RX)
>>>>>>>>>
>>>>>>>>> or
>>>>>>>>>
>>>>>>>>> self.io.write(UART_TX::from(byte))
>>>>>>>>>
>>>>>>>>> vs
>>>>>>>>>
>>>>>>>>> self.io.write(UART_TX, byte)
>>>>>>>>>
>>>>>>>>> what benefit does additional type provide?
>>>>>>>>
>>>>>>>> Well, for FIFO registers this is indeed better. However, my main concern was
>>>>>>>> this
>>>>>>>>
>>>>>>>> bar.write(regs::MyReg, regs::MyReg::foo())
>>>>>>>
>>>>>>> This specific case is indeed more cumbersome with the two argument approach,
>>>>>>> although given Alex's nova diff I think the occurance shouldn't be that
>>>>>>> frequent.
>>>>>>>
>>>>>>> It's also not that the two argument approach would preclude us from having a
>>>>>>> single argument option. In fact, with the two-argument design as the basis, we
>>>>>>> can implement such a helper function cleaner than Alex's PATCH 10/10 (which uses
>>>>>>> `Into<IoWrite>`:
>>>>>>>
>>>>>>> /// Indicates that this type is always associated with a specific fixed I/O
>>>>>>> /// location.
>>>>>>> ///
>>>>>>> /// This allows use of `io.bikeshed_shorthand_name(value)` instead of specifying
>>>>>>> /// the register name explicitly `io.write(REG, value)`.
>>>>>>> trait FixedIoLocation {
>>>>>>> type IoLocType: IoLoc<Self>;
>>>>>>> const IO_LOCATION: Self::IoLocType;
>>>>>>> }
>>>>>>>
>>>>>>> trait Io {
>>>>>>> fn bikeshed_shorthand_name<T>(&self, value: T)
>>>>>>> where T: FixedIoLocation +
>>>>>>> Self: IoCapable<<T::IoLocType as IoLoc<T>>::IoType>,
>>>>>>> {
>>>>>>> self.write(T::IO_LOCATION, value)
>>>>>>> }
>>>>>>> }
>>>>>>>
>>>>>>> No need for a `IoWrite` type, everything is done via traits.
>>>>>>
>>>>>> That's cool but will only work for fixed registers. If you work with, say, an
>>>>>> array of registers, cannot implement this trait on a value as the value
>>>>>> doesn't have an index assigned - meaning you would have to build a
>>>>>> location in addition of it.
>>>>>
>>>>> For array registers I think it makes more sense to use the two-argument version,
>>>>> no?
>>>>>
>>>>> The example here is to demonstrate that we can add a shorthand version for the
>>>>> fixed register version that can write a value to register without mentioning its
>>>>> name (as a supplemental helper), and the basic write method is the two-argument
>>>>> one.
>>>>>
>>>>> For cases where the type doesn't guarantee a fixed location like FIFO register
>>>>> or an array register, mentioning the name twice is fine.
>>>>
>>>> It's still tedious, and a step back compared to the one-argument version
>>>> imho.
>>>>
>>>>>
>>>>> [
>>>>>
>>>>> For array case, you *could* also do
>>>>>
>>>>> impl IoLoc<RegisterName> for usize {
>>>>> fn offset(self) -> usize {
>>>>> self * stride + fixed_base
>>>>> }
>>>>> }
>>>>>
>>>>>
>>>>> and now you can do `self.write(index, reg_value)`, although I think this
>>>>> might confuse some people.
>>>>
>>>> Yes, in this case the semantics of write's first argument would be
>>>> dependent on the second argument... I think that's a potential footgun.
>>>
>>> I mean, `bar.write(Reg::at(10, regs::MyRegArray::foo()))` in your example is
>>> also kind of "first argument depends on the second argument" situation, just
>>> with a bit more boilerplate.
>>
>> Not really, `at` is enough to know that you are accessing an array.
>>
>> Whereas `write(index, reg_value)` doesn't give us any indication of what
>> type of indirection (if any) we have.
>
> I mean not sure `at` gives me that impression at all. It would just let me know
> that I am accessing it at a different location. If you omit the `MyRegArray`
> part then there's no real indication that this is an array to me.
`at` is a function name, we can change it - I picked it because it is
short and reasonably descriptive. The point being: we have a unique
function that indicates unambigously that we are using a location for
an array of registers.
You seem to reject this design because the syntax isn't obvious and
natural to you at first read. We are trying to build something new, so
of course if will look a bit alien at the first encounter. That's why we
have documentation, and I think it is not very difficult to wrap your
head around it after seeing a few examples.
>
> If `at` is only for array, how would you represent the case where the same type
> is being used in multiple registers?
That's not something that is supported by the register macro currently
(probably not a big change, but not something I will do in this series).
But to try and answer your question, such register types would not have
an `IoLoc` implementation of their own and would need to have their
location constructed explicitly. In accordance, the construction of
their value would not bear any location information; thus there would be
no redundancy.
We do have a case that is pretty close to this with relative registers.
These are accessed like this (real examples):
bar.write(Reg::of::<Gsp>(regs::NV_PFALCON_FALCON_DMACTL::zeroed()));
and
bar.write(Reg::of::<Sec2>(regs::NV_PFALCON_FALCON_DMACTL::zeroed()));
But for register types that can be used at several arbitrary locations,
I think this would be even simpler. The different locations would just
need to implement `IoLoc<T>`, where `T` is the shared register type.
Then, considering that the two-arguments version is called `write_at`,
you can simply do:
bar.write_at(REG_LOCATION, reg_value);
... but this design also makes this possible:
bar.write((REG_LOCATION, reg_value));
Tuples of (location, value) do implement `IoLoc` themselves, so we can
use this little trick to support a 2-arguments syntax with a single
method.
>
>>
>>>
>>> If you want to make things more explicit you could also have
>>> `bar.write(at_array(10), ...)` or something similar.
>>
>> Is it possible to generate an `IoLoc<T>` without having `T` mentioned
>> anywhere in the call to `at_array`?
>
> Exactly same as the `impl IoLoc<REG> for usize`:
>
> struct AtArray(usize);
>
> impl IoLoc<REG> for AtArray {
> ...
> }
Right, but can the correct `REG` be inferred when the call to `at_array`
doesn't bear that information? The type inferred by the second argument
would have to be propagated to the first. Guess I'll try and see.
>
>>
>>>
>>> For the array case I really think trying to shove everything into a single
>>> argument is a footgun. The type of value in this case *doesn't* tell us the
>>> location, and the location needs to be explicit.
>>
>> bar.write(Reg::at(10, regs::MyRegArray::foo()))
>>
>> "write the constructed value at the 10th position of the `MyRegArray`
>> register array"
>>
>> What is missing here?
>
> This is completely un-natural if I try to read it with fresh mind (try to forget
> about implementation details for a second).
That's what documentation is for. Please give it a fair chance and ask
yourself: would it still look unnatural after working with it for
20 minutes?
>
> `MyRegArray` here is a type name that is a bitfield and not an array. `foo` returns a
> single value and not an array. "at" here is saying that the register is at a
> specific location and doesn't really indicate the array nature.
>
> This is why I insist that I would prefer an explicit location
>
> bar.write(REG_ARRAY.at(10), Reg::foo())
>
> would have no ambiguity whatsoever about user's intent.
IIUC `REG_ARRAY` would be a const ZST and `at` a method returning an
`AtArray(usize)`? I still have doubts that its generic type could be
inferred automatically but it's worth giving it a go.
If that works, then I assume fixed register writes would look like
bar.write(FIXED, Reg::foo());
Unless we have a specialized `write` variant for them.