| Copyright | (c) 2015 Chris Hodapp |
|---|---|
| Safe Haskell | None |
| Language | Haskell2010 |
Ivory.Language.Ion
Description
Ion is a Haskell EDSL for concurrent, realtime, embedded programming. It performs compile-time scheduling, and produces scheduling code with constant memory usage and deterministic execution (i.e. no possibility for divergence).
It interfaces with another, more powerful EDSL, <http://ivorylang.org/ Ivory>, to perform code generation. Ivory is responsible for all the code generation to perform the scheduling. One may also embed general Ivory effects in an Ion spec with few restrictions, however, it does very little to enforce constant memory usage or deterministic code here.
Ion generates scheduling code which must be called at regular clock ticks (i.e. from a timer interrupt). The interval of these clock ticks establishes the *base rate* of the system. All scheduled events in the system take place relative to this base rate, defined in terms of period (interval of repetition) and phase (position within that interval).
This functionality is expressed in the Ion monad - in large part to allow composition and modularity in expressing tightly-scheduled functionality. In addition, it has functions like newProc and newArea which define uniquely-named C functions and globals. The purpose of these is to allow that same compositional when working with Ivory definitions that are parametrized and may be instantiated multiple times.
For instance, when dealing with functions that return via asynchronous callbacks or interrupts - a common thing on embedded systems - one must generally work in continuation-passing style. This simplifies the process of creating a reusable pattern for a use-case like:
- Transmit instruction
Iover SPI. Wait to receive 2 bytes. - In a callback: Check that result for being an error condition. If an error, call error handler function
E. If successful, transmit instructionI2and wait to receive 2 bytes. - In a callback: Check for error and call
Eif needed. If successful, combine result into some composite value, and call success handlerSwith that value.
and then parametrizing this whole definition over instructions I and I2, error handler E, and success handler S. This definition then could be parametrized over multiple different instructions, and all of these chained together (e.g. via (=<<)) to create a larger sequence of calls passing control via CPS.
Ion was heavily inspired by another EDSL, Atom. It started as an Atom re-implementation which had other backends, rather than generating C code directly (as Atom does). However, Ion has diverged somewhat, and still does not have many things from Atom, such as synchronous variable access, run-time checks on execution time, various compile-time sanity checks, traces, or most of its standard library.
To-do items:
- Continue writing documentation and examples!
- Get some unit tests for things that I am prone to breaking.
- It *still* does not handle
minimumphase. - This could use a way to
inverta phase, and run at every phase but the ones noted. - I need to convert over the
schedulefunction in Scheduling.hs in Atom. - Atom treats everything within a node as happening at the same time, and I do not handle this yet, though I rather should. This may be complicated - I may either need to process the Ivory effect to look at variable references, or perhaps add certain features to the monad.
- Atom had a way to express things like rising or falling edges, and debouncing. How possible is this to express?
- Right now one can only pass variables to an Ion by way of a Ref or some derivative, and those must then be dereferenced inside of an
ivoryEffcall. Is this okay? Should we make this more flexible somehow? (I feel like Atom did it similarly, with V & E.) - Pretty-printing the schedule itself (as Atom does) would probably be a good idea.
- Consider the case where one puts a condition on a node, and that node has many sub-nodes across various delays. Now, suppose that that condition becomes false somewhere in the middle of those delays. Is the entire node blocked from taking effect, or does it partially take effect? When is the condition considered as being evaluated? Right now it is evaluated at every single sub-node that inherits it. I consider this to be a violation of how Ion should operate - synchronously and atomically.
- Could
ivoryEffmeaningfully return a value toIonrather than ()? - Would it be possible to make a CFG for the continuation-passing style arrangements? (Might Ivory have to handle this?)
- Runtime check: Schedule function being called twice in one clock tick.
- Runtime check: Schedule function never called in a clock tick.
- Runtime check: Schedule function hasn't returned yet when next clock tick occurs (i.e. schedule function takes too long).
- Runtime check: Compute percent utilization, time-wise, in schedule function.
- Compile-time check: Same period and phase occupied. (Atom would throw a compile-time error when this happened.)
- type Ion = State IonDef
- type IonCont a b = Def (b :-> ()) -> Ion (Def (a :-> ()))
- data IonExports a = IonExports {}
- ionDef :: String -> Ion a -> IonExports a
- ion :: String -> Ion a -> Ion a
- phase :: Integral i => i -> Ion a -> Ion a
- delay :: Integral i => i -> Ion a -> Ion a
- period :: Integral i => i -> Ion a -> Ion a
- subPeriod :: Integral i => i -> Ion a -> Ion a
- cond :: IvoryAction IBool -> Ion a -> Ion a
- disable :: Ion a -> Ion ()
- newName :: Ion String
- newProc :: IvoryProcDef proc impl => impl -> Ion (Def proc)
- newProcP :: IvoryProcDef proc impl => Proxy (Def proc) -> impl -> Ion (Def proc)
- area' :: (IvoryArea area, IvoryZero area) => String -> Maybe (Init area) -> Ion (Ref Global area)
- areaP' :: (IvoryArea area, IvoryZero area) => Proxy area -> String -> Maybe (Init area) -> Ion (Ref Global area)
- newArea :: (IvoryArea area, IvoryZero area) => Maybe (Init area) -> Ion (Ref Global area)
- newAreaP :: (IvoryArea area, IvoryZero area) => Proxy area -> Maybe (Init area) -> Ion (Ref Global area)
- ivoryEff :: IvoryAction () -> Ion ()
- timer :: (a ~ Stored t, Num t, IvoryStore t, IvoryInit t, IvoryEq t, IvoryOrd t, IvoryArea a, IvoryZero a) => Proxy t -> Def (`[]` :-> ()) -> Ion (Ref Global (Stored t))
- startTimer :: (Num t, IvoryStore t, IvoryZeroVal t) => Ref Global (Stored t) -> Integer -> Ivory eff ()
- stopTimer :: (Num t, IvoryStore t, IvoryZeroVal t) => Ref Global (Stored * t) -> Ivory eff ()
- getPhase :: Ion Integer
- adapt_0_1 :: (IvoryType a, IvoryVar a) => Def (`[]` :-> ()) -> Ion (Def (`[a]` :-> ()))
- adapt_1_0 :: (Num a, IvoryType a, IvoryVar a) => Def (`[a]` :-> ()) -> Ion (Def (`[]` :-> ()))
- adapt_0_2 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b) => Def (`[]` :-> ()) -> Ion (Def (`[a, b]` :-> ()))
- adapt_2_0 :: (Num a, IvoryType a, IvoryVar a, Num b, IvoryType b, IvoryVar b) => Def (`[a, b]` :-> ()) -> Ion (Def (`[]` :-> ()))
- adapt_0_3 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b, IvoryType c, IvoryVar c) => Def (`[]` :-> ()) -> Ion (Def (`[a, b, c]` :-> ()))
- adapt_3_0 :: (Num a, IvoryType a, IvoryVar a, Num b, IvoryType b, IvoryVar b, Num c, IvoryType c, IvoryVar c) => Def (`[a, b, c]` :-> ()) -> Ion (Def (`[]` :-> ()))
- adapt_0_4 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b, IvoryType c, IvoryVar c, IvoryType d, IvoryVar d) => Def (`[]` :-> ()) -> Ion (Def (`[a, b, c, d]` :-> ()))
- adapt_4_0 :: (Num a, IvoryType a, IvoryVar a, Num b, IvoryType b, IvoryVar b, Num c, IvoryType c, IvoryVar c, Num d, IvoryType d, IvoryVar d) => Def (`[a, b, c, d]` :-> ()) -> Ion (Def (`[]` :-> ()))
- adapt_0_5 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b, IvoryType c, IvoryVar c, IvoryType d, IvoryVar d, IvoryType e, IvoryVar e) => Def (`[]` :-> ()) -> Ion (Def (`[a, b, c, d, e]` :-> ()))
- accum :: (IvoryType a, IvoryVar a, IvoryStore a, IvoryZeroVal a, IvoryType b, IvoryVar b) => IonCont `[]` `[b]` -> IonCont `[a]` (a : `[b]`)
Base types
Arguments
| = Def (b :-> ()) | Continuation function |
| -> Ion (Def (a :-> ())) | Entry function |
This wraps a pattern of functions calling each other in continuation-passing style. The intent is that the returned entry function (which takes arguments a) causes the supplied continuation function to be called (passing arguments b).
This is a common pattern for asynchronous calls, for instance, in which the callback or interrupt calls the continuation function.
Multiple calls of this sort can be composed with '(=<<)' (and with RecursiveDo and mdo) to chain them in the order in which they would proceed.
For instance, in start <- call1 =<< call2 =<< call3 final, start contains the entry function to call1, whose continuation is set to the entry function of call2, whose continuation in turn is set to the entry function of call3, whose continuation is final. Note that chaining these with '(>>=)' is possible too, but the order is somewhat reversed from what is logical - hence, mdo often being sensible here.
Code generation
Arguments
| :: String | Name for schedule function |
| -> Ion a | Ion specification |
| -> IonExports a |
Produce exports from the given Ion specs.
Operators
Compositional
These functions all have (or similar) at the end of their type, and that is because they are meant to be nested by function composition. For instance:Ion a -> Ion a
ion"top_level" $ doion"sub_spec" $period100 $ doion"phase0" $phase0 $ do -- Everything here inherits period 100, phase 0, and -- a new path "top_level.sub_spec.phase0".phase20 $phase'30' $ do -- Everything here inherits period 100, and phase 30phase40 $cond(return true) $ do -- Everything here inherits period 100, phase 40, and -- a (rather vacuous) conditiondisable$phase50 $ do -- This is all disabled.
Note that more inner bindings override outer ones in the case of phase, delay, period, and subPeriod. Applications of cond combine with each other as a logical and. Applications of disable are idempotent.
Specify a name of a sub-node, returning the parent. This node name is used in the paths to the node and in some C identifiers in the generated C code; its purpose is mainly diagnostic and to help the C code be more comprehensible.
Specify a minimum phase for a sub-node - that is, the earliest tick within a period that the sub-node should be scheduled at. Phase must be non-negative, and lower than the period.
Specify a period for a sub-node - that is, the interval, in ticks, at which the sub-node is scheduled to repeat. Period must be positive; a period of 1 indicates that the sub-node executes at every single clock tick.
Arguments
| :: Integral i | |
| => i | Factor by which to multiply period (must be positive) |
| -> Ion a | Sub-node |
| -> Ion a |
Specify a sub-period for a sub-node - that is, the factor by which to multiply the inherited period. A factor of 2, for instance, would execute the sub-node half as often as its parent.
cond :: IvoryAction IBool -> Ion a -> Ion a Source
Make a sub-node's execution conditional; if the given Ivory effect returns true (as evaluated at the inherited phase and period), then this sub-node is active, and otherwise is not. Multiple conditions may accumulate, in which case they combine with a logical and (i.e. all of them must be true for the node to be active).
disable :: Ion a -> Ion () Source
Ignore a sub-node completely. This is intended to mask off some part of a spec while still leaving it present for compilation. Note that this disables only the scheduled effects of a node, and so it has no effect on things like newProc.
Memory & Procedures
newProc :: IvoryProcDef proc impl => impl -> Ion (Def proc) Source
This is like Ivory proc, but using Ion to give the procedure a unique name.
newProcP :: IvoryProcDef proc impl => Proxy (Def proc) -> impl -> Ion (Def proc) Source
newProc with an initial Proxy to disambiguate the procedure type
Arguments
| :: (IvoryArea area, IvoryZero area) | |
| => String | Name of variable |
| -> Maybe (Init area) | Initial value (or |
| -> Ion (Ref Global area) |
Allocate a MemArea for this Ion, returning a reference to it. If the initial value fails to specify the type of this, then an external signature may be needed (or instead areaP'). If access to this variable is needed outside of the Ion monad, retrieve the reference from an Ion with the ionRef function. The ModuleDef for this will be generated automatically.
Arguments
| :: (IvoryArea area, IvoryZero area) | |
| => Proxy area | Proxy (to disambiguate type) |
| -> String | Name of variable |
| -> Maybe (Init area) | Initial value (or |
| -> Ion (Ref Global area) |
Same as area', but with an initial Proxy to disambiguate the area type.
newAreaP :: (IvoryArea area, IvoryZero area) => Proxy area -> Maybe (Init area) -> Ion (Ref Global area) Source
Effects
ivoryEff :: IvoryAction () -> Ion () Source
Attach an Ivory effect to an Ion. This effect will execute at the inherited phase and period of the node.
Utilities
Arguments
| :: (a ~ Stored t, Num t, IvoryStore t, IvoryInit t, IvoryEq t, IvoryOrd t, IvoryArea a, IvoryZero a) | |
| => Proxy t | Proxy to resolve timer type |
| -> Def (`[]` :-> ()) | Timer expiration procedure |
| -> Ion (Ref Global (Stored t)) |
Create a timer resource. The returned Ion still must be called at regular intervals (e.g. by including it in a larger Ion spec that is already active). See startTimer and stopTimer to actually activate this timer.
Arguments
| :: (Num t, IvoryStore t, IvoryZeroVal t) | |
| => Ref Global (Stored t) | Timer from |
| -> Integer | Countdown time |
| -> Ivory eff () |
Begin counting a timer down by the given number of ticks.
stopTimer :: (Num t, IvoryStore t, IvoryZeroVal t) => Ref Global (Stored * t) -> Ivory eff () Source
Stop a timer from running.
adapt_0_1 :: (IvoryType a, IvoryVar a) => Def (`[]` :-> ()) -> Ion (Def (`[a]` :-> ())) Source
All the adapt_X_Y functions adapt an Ivory procedure which takes X arguments and returns nothing, into an Ivory procedure which takes Y arguments. If X > Y then zero is passed for the argument(s); if Y < X then the additional arguments are ignored. The generated procedure is automatically included as part of the Ion spec. The main point of this is to simplify the chaining together of Ivory procedures.
adapt_1_0 :: (Num a, IvoryType a, IvoryVar a) => Def (`[a]` :-> ()) -> Ion (Def (`[]` :-> ())) Source
adapt_0_2 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b) => Def (`[]` :-> ()) -> Ion (Def (`[a, b]` :-> ())) Source
adapt_2_0 :: (Num a, IvoryType a, IvoryVar a, Num b, IvoryType b, IvoryVar b) => Def (`[a, b]` :-> ()) -> Ion (Def (`[]` :-> ())) Source
adapt_0_3 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b, IvoryType c, IvoryVar c) => Def (`[]` :-> ()) -> Ion (Def (`[a, b, c]` :-> ())) Source
adapt_3_0 :: (Num a, IvoryType a, IvoryVar a, Num b, IvoryType b, IvoryVar b, Num c, IvoryType c, IvoryVar c) => Def (`[a, b, c]` :-> ()) -> Ion (Def (`[]` :-> ())) Source
adapt_0_4 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b, IvoryType c, IvoryVar c, IvoryType d, IvoryVar d) => Def (`[]` :-> ()) -> Ion (Def (`[a, b, c, d]` :-> ())) Source
adapt_4_0 :: (Num a, IvoryType a, IvoryVar a, Num b, IvoryType b, IvoryVar b, Num c, IvoryType c, IvoryVar c, Num d, IvoryType d, IvoryVar d) => Def (`[a, b, c, d]` :-> ()) -> Ion (Def (`[]` :-> ())) Source
adapt_0_5 :: (IvoryType a, IvoryVar a, IvoryType b, IvoryVar b, IvoryType c, IvoryVar c, IvoryType d, IvoryVar d, IvoryType e, IvoryVar e) => Def (`[]` :-> ()) -> Ion (Def (`[a, b, c, d, e]` :-> ())) Source
CPS
accum :: (IvoryType a, IvoryVar a, IvoryStore a, IvoryZeroVal a, IvoryType b, IvoryVar b) => IonCont `[]` `[b]` -> IonCont `[a]` (a : `[b]`) Source
Accumulate an argument into a continuation function. Specifically: Given an IonCont taking some argument in its entry function, generate another IonCont with the same type of entry function, but whose continuation function contains another argument (which will receive the same value of that argument).
Note that every use of this requires a static variable of type a. Also, this implementation does not protect against the continuation function being called without the entry function; if this occurs, the continuation will contain old values of a from earlier invocations, or possibly a zero value.
TODO: Right now this handles only converting single-argument to double-argument. I intend to modify this to work similarly to call and callAux in Ivory.