> The idea of checking that a pin is "only used in one place" doesn't really jive with how I think about microcontroller programming.
The borrow checker is not checking that the pin is used in "only one place", it is checking that you don't use the same pin for two different purposes at the same time.
It make sure that you configure your pin as output pin before using it as an output pin, and that you reconfigure it to input pin when using it as such.
(And there are some escape hatch to use when the type system is not sufficient to express that different code paths are disjoint, like RefCell, with runtime check, or unsafe)
Borrow checker tracks who is using what over time. The can prevent concurrency and uncoordinated mutation, use after free type problems.
Type system checks how it is being used.
Both are tools and can used to help ensure a correct program. It really comes down to how these _tools_ are used to help the programmer and the team deconstruct and manage a system to solve a problem.
I think petgraph [1] is an excellent example of relaxing some of the constraints imposed by the tools (borrow checker, type system) to make a system that was easier to use and extend. These things are much more continuous than we realize, it isn't all or nothing.
In a lot of ways, I wish Rust's complexity was a little more gradual, or that we knew how to use it in a gradual way. Use of Rust features encourages C++ levels of complexity. Use of Zig features encourages C-ish levels of complexity.
Zig is to C
as
Rust is to C++
I also think the author had a much better model of the system and the hardware and what they wanted to accomplish during the rewrite and could better project the problem domain and the language together.
Learning Rust and the problem domain at the same time is extremely difficult and in a way leads to a perversion of both.
What do you think about modeling the hardware as a "Resource" register, port, memory, etc. Then modeling a type over a collection of resources.
The question that I would ask myself when trying to use Rust and the features it has out of the box is, "How much fine grain rigor do I want Rust to model for me?" For the keyboard scanning code, in asm or C, one might just have a function `get_keyboard_state(*keyboard_buffer)` but this exposes a sampling problem and would require the programmer to determine state transitions. So maybe a channel or an iterator would be better. Then we might nee to run it in an ISR, the hardware it uses might be multiplexed with other parts of the system, etc.
Every Rust feature needs to be weighed, or rather, given a complexity budget, every Rust feature needs to be purchased.
Zig is awesome BTW, but it doesn't make the same correctness guarantees that Rust can.
> Borrow checker tracks who is using what over time.
This is a very imprecise statement. Do you mean tracks at "compile time" or at "run time"?
A more accurate statement would be -- the borrow checker enforces a specific set of rules and constraints at _compile time_ only. But this set of constraints guarantees memory safety at run time (with the exception of unsafe code). In fact, Rust's runtime is minimal -- it handles panics but has no GC or other bells and whistles. The fancy things are in libraries.
Ah, I was going on what the OP said ("ensuring that a given pin cannot be used in multiple places").
That seems sensible, but also not particularly valuable. A lot of the time it makes sense both to 'read' and 'write' from a pin (e.g. if it's open-drain with a pullup).
The borrow checker is not checking that the pin is used in "only one place", it is checking that you don't use the same pin for two different purposes at the same time.
It make sure that you configure your pin as output pin before using it as an output pin, and that you reconfigure it to input pin when using it as such.
(And there are some escape hatch to use when the type system is not sufficient to express that different code paths are disjoint, like RefCell, with runtime check, or unsafe)