4

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 recover do 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.

Cactus
  • 27,075
  • 9
  • 69
  • 149
  • Instead of an indexed monad, perhaps you could use a monad parameterized by a type-level list of names/types. (Possibly requiring a type signature or type application from the user.) The monad would carry as state some kind of extensible record indexed by the list. Commands which created "backpane connections" would require a type application with the name (or index) of the particular port. This wouldn't enforce that all the ports mentioned in type-level list are given values though. Perhaps the missing ports could be given some kind of "neutral" value, if it makes sense. – danidiaz Dec 07 '20 at 19:47

0 Answers0