Closed Rewbert closed 3 years ago
My vision was that the environment would be passed in to a program as an argument. Specific to each board, the environment would be an aggregate type (a record) consisting of a type-specific field for every source of I/O available to the program. These fields would be passed around as references and there would be various libraries for type-specific I/O behavior that could be used with these fields.
Something as simple as LEDs would be delivered as a fixed-sized array of references to LED "variables" that, when written, would send the appropriate value to the given LED.
My idea was that we could do this somehow with types. The runtime system is meant to supply two type-specific write functions/macros for each type (e.g., assign_led, later_led) as well as a type-specific update function that's invoked dynamically based on its runtime type (like an OO method or a typeclass). For something like an LED peripheral, there would be custom versions of all of these functions/macros that would get linked with the generated code.
Inputs are more complicated. Again, I intended them to appear as variables passed into the main function. The runtime should collect events from various I/O sources and queue them with a timestamp in an interrupt-safe queue. The runtime should block on this queue and when it sees a pending event, it should transfer the event from the interrupt-safe queue into the normal event queue and call tick().
In some sense, this begins to look a little like the device driver problem/solution in Unix, where devices appear as files, but the operating system instead sees them as major device number/minor device number pairs and dispatches the appropriate read/write functions when a user programs reads or writes to them. Instead of "everything is a file," my philosophy is "everything is a variable."
With your "getButton 1" code, what happens if it's called more than once? Does the "device driver " keep a list of all things that is watching this button? One advantage I can see with your approach is that you force the program to explicitly indicate which I/O sources it actually cares about. In my view, I didn't consider whether particular interrupt sources (really the issue) are enabled or disabled, although it would be easy enough to have some sort of "enable interrupts" function that can be called on the abstract I/O variable for various sources.
My main concerns are that I want a design where we don't need to alter the RTS when we need to support a new peripheral, as few callbacks as necessary are installed (only those that the program requires) and that the programmer in Haskell does not need to worry about a reference really being a LED or just a normal reference with a binary value.
I imagine in my mind that getButton 1
would, the first time you call it, instruct the compiler to do the necessary setup required, installing callbacks etc, and then create a reference that is returned to the programmer. If you now call the same function again it will detect that initialization has already been done, and instead just return the previously defined reference.
This reference could perhaps be declared in the C main
function and then passed as a parameter to some scheduler_main
, which is the point where our scheduler takes over. The only place where it magically needs to appear is in the function that calls getButton 1
. All other procedures will receive it as an argument.
I don't see why we shouldn't be able to use the assign_type
and later_type
etc functions to capture side effects. I hope that this code can be generated by the compiler. I can think of some hacky ways to do it. I imagine that there would be a module LedModule
or something that describes these structures/functions, and when you use it, it generates the correct code. Maybe we can use this to even describe a board in its entirety! I've not thought too much about this, but maybe something like this:
data Board = -- some nice representation
-- | Get an object that you can use to interact with a board
getNRF52 :: SSM Board
-- | Given a board, fetch a reference to the first LED
getLED1 :: Board -> SSM (Ref Bool)
-- | Get a reference to the first button
getButton1 :: Board -> SSM (Ref Word8) -- Word8 for maybe signaling different events (button-press, button-release etc)
When you call e.g getNRF52
you can tell the SSM
monadic computation that when it generates code, it needs to generate initialization code for that specific board, and similarly for getLED1
etc. The getLED1
function would emit instructions for how the struct needs to look, definitions for the later
functions and so on. You would write this module just once for each board, and then the programmers don't need to worry about this. Adding support for a new board just means writing a new module where you describe the different components required in the generated code.
For now we can just hardcode this to only talk about C code, but ideally this description would be generalized so that we can generate code for different targets. E.g the micro:bit's run JavaScript. Maybe we can write a program with two parts that communicate, where one part runs C on a nrf52, while one runs JavaScript on a micro:bit.
This discussion is good, it makes me think harder about what I am envisioning. Let me know if anything sounds funny or weird.
An upside of being explicit in Haskell about which IO devices you will need is that we can generate code that initializes just that specific stuff :)
The problem I see with not drawing a distinction between LED variables and ordinary Booleans is the overhead it suggests. Unless I'm mistaken, taking this approach means doing some sort of virtual function dispatch for every variable assignment since you can't know except at runtime whether something is a boring Boolean that can be assigned in the usual way, or whether you need to call a magical I/O-specific function. While you can do the latter by adding a virtual table pointer to each variable that includes a variable-specific assignment function, having to do this to every single variable in the program seems like a lot of unnecessary overhead.
One possible way to address this is to allow something like typeclasses in the language and have something like an "assignable" typeclass that defines an assignment operation. This might take care of the problem since I imagine making type-specific variants of polymorphic code, something like C++ templates but with typing discipline.
Your idea of creating an "I/O reference factory" function, however, is interesting in that it could also address the problem of creating new Bluetooth connections, for instance.
Comparing it to a boolean was just an example, but what we can do is make the 'reference factory' function create a more involved type that actually statically represents all the proper information needed to light the leds etc, but just pretend in the haskell types that it's a boolean. We'd still be able to generate very specific code but the programmer would not need to care about those details.
This is one of the pros about having an untyped AST representaion with phantom types. We can create a value that represents a led reference, but then give it a type that reflects something far simpler.
But again, representing leds with booleans was just an example :) We'd ideally have our own LED ADT types, where we'd have a reference to a led state or something (ON | OFF).
I've prepared some simple code to show on Tuesday! :)
After some thought it is probably good to not explicitly say in the code that you are using a specific board, e.g getNRF52840 :: SSM Board
, since this means you would need to change this before running the code on another board. Better is to supply the board as a flag or something at a later stage.
The references we have can schedule events and react to events, but they can not interact with anything outside of the scheduler. We need to add another type of reference that carries some extra information, a smarter reference. To the programmer this should look identical to any other reference.
LEDs
In the above example we use the LED just as we would any other reference. The only difference is that instead of getting access to it by having it passed to us as a parameter, or creating the reference by using
var
, we get access to the reference from a module that we import. This module has anSSM (Ref Bool)
function that knows how to create the smarter reference.A simple strategy is to create a variant of references that can carry extra code that is simply inlined when e.g an assignment is done to the variable, but I don't think this is enough. If we schedule an update on a reference, the side effect should be performed when the event occurs as well. When the scheduler applies events, the scheduler only knows that the thing receiving the update is an
sv_t*
, nothing more. We do need to add additional functionalities to thesv_t*
struct, like a pointer to a function, so that having access to ansv_t*
is enough to gain access to the side effects.Buttons
For buttons we need to react to events rather than producing them.
Simply executing
getButton 1
should be enough to get a reference that can interact with the HW button. Furthermore, this should be enough to tell the compiler that it has to install a callback on the actual HW button so that the above-mentioned reference gets an event when the button is clicked. This callback should be installed in themain
function before the scheduler takes over.Even though the underlying value in the runtime system will be a reference to something smart that carries this extra information, the Haskell API should not reflect that anything is strange. E.g representing a LED state as a
Bool
might be very appropriate, in which case the Haskell code should only have to talk aboutBool
s.