I'm creating a library for describing the connection of address lines to memory elements. I think a monadic API would work very nicely for creating components (with an identity, to support mirroring), and then using those component handles at various restricted address ranges; e.g:
(dataIn, ()) = memoryMap _addrOut _dataOut $ do
rom <- romFromFile (SNat @0x2000) "SpaceInvaders.bin"
ram <- ram0 (SNat @0x0400)
from 0x0000 $ connect rom
from 0x2000 $ connect ram
from 0x4000 $ connect ram
However, some memory components also have "backpane connections". For example, suppose a memory-mapped IO peripheral has a byte output. I could represent that as a monadic action that returns a pair of a result: the memory element handle, and the output signal; and then leave it to the user to route it somehow to the end result of the whole memory mapping description:
(dataIn, outByte) = memoryMap _addrOut _dataOut $ do
rom <- romFromFile (SNat @0x0800) "_build/intel8080/image.bin"
ram <- ram0 (SNat @0x1800)
(acia, outByte) <- port $ acia inByte outReady
matchLeft $ do
from 0x10 $ connect acia
matchRight $ do
from 0x0000 $ connect rom
from 0x0800 $ connect ram
return outByte
The problem with this approach is that it doesn't admit an easy implementation. In the background, these components are ultimately just Signal addr -> Signal (Maybe dat) -> (Signal dat, Signal backpane)
functions, so to get the backpane
signal, I need to apply them on an addr
signal; this of course requires looking at all the subsequent connect
calls on the handle. This needs a messy "knot-tying" implementation that causes problems further downstream, so I'd like to avoid it.
Also, the user isn't really able to do anything with these backpane outputs like outByte
, other than returning them at the end. So it would be cleaner if only the handles (like rom
, ram
and acia
in the examples above) were bound, and other outputs were... somehow... magically... transported to the outside.
What I mean by that is that instead of applying the individual memory components on this recursively defined address line, creating a memory component could just return some opaque handle, hold on to that function to be applied after all the connect
calls, and then apply it. No recursion needed, no backpane values are exposed to the user, etc. But how would these extra outputs be ultimately collected?
As I am writing this question, one idea that pops to mind is using a graded monad where the index is the list of types of backpane output signals; creating the acia
handle, for example, would extend that list by a new element. The runner memoryMap
would return some kind of generalized product type alongside the memory read result. But I don't like this idea for two reasons:
Using a graded monad is unwieldy for the user, you need to use
RebindableSyntax
to recoverdo
notation, type inference isn't so good, and overall it's just a pain to explain to users.The eventual type of
memoryMap
would be surprising because it would depend on the order in which components with backpane signals are created.