boschresearch / blech

Blech is a language for developing reactive, real-time critical embedded software.
Apache License 2.0
72 stars 6 forks source link

activities with instantaneous control path #15

Closed mterber closed 1 month ago

mterber commented 4 years ago

Is your feature request related to a problem? Please describe. Consider the following example code:

activity LCD_printCounter (line: nat8, num: nat32)
    if EXT_lcdQueueIsFull() then
        await not EXT_lcdQueueIsFull()
    end
    _ = EXT_lcdQueuePutPrintCmd(line, num)
    await not EXT_lcdQueueIsFull() // just added to satisfy the compiler
end

activity Receiver (sysTick: bool)(leds: LedStates)
    var cRx: nat32
    [...] // instantaneous code
    repeat
        [...] // instantaneous code
        res = run COM_receive()(data, len) // Await next packet.
        [...] // instantaneous code
        run LCD_printCounter(15, true, successCount)
        [...] // instantaneous code
    end
end

Basically, there is a receiver activity which continuously receives packets from a communication interface. After each reception, the reception counter is incremented an shall be printed to the LCD. The LCD has a command queue which is capable of storing up to N commands to be executed on the LCD. The LCD activity checks whether it can put a new command into that queue or not. If the queue is full the activity will wait until there is at least one free slot in the queue. Otherwise, the activity will immediately put the new command in the queue.

At this point, the LCD activity actually could terminate because it has already done its job. The Blech compiler, however, enforces that all control paths within an activity must contain at least one await statement. So in order to get the code compiled I have added another unconditional await statement which is actually not required from the applications point of view and unnecessarily suspends the termination of the LCD activity.

Describe the solution you'd like To summarize it: In this scenario I would need the possibility to implement an activity which might await but is not forced to so. In this case it might be required to not enforce the await statement being present in the LCD activity but in the calling Receiver activity instead.

Do you think that this could be feasible? Or maybe do you have a suggestion on how to deal with that problem in a different way?

frameworklabs commented 4 years ago

If you don't want the extra await in LCD_printCounter I would inline the code directly into Receiver. This way its up to the Receiver to deal with and control the stepping.

mterber commented 4 years ago

@frameworklabs Yes, you are right. I also considered this workaround. But LCD_printCounter is called at different locations within my code – not only in the Receiver (which is not obvious from the simplified example code). This means that I have to inline this check manually every time I want to call LCD_printCounter. In particular, this means that, although this check is actually part of the internal logic of LCD_printCounter, I cannot encapsulate it in the same activity. Instead, the caller has to implicitly know that this check is mandatory and has to insert it on each call.

frameworklabs commented 4 years ago

I see you point but I like that run <activity> like await <cond> clearly mark the end/start of instantaneous code.

Also, the approach with guarding an await with an if seems to be some kind of "imperative synchronous programming pattern" already:

if not condition then
    await condition
end
<do something now that condition is met>
mterber commented 4 years ago

I do not see how I could avoid the conditional await in this case. If there are already free slots in the queue then I do not want to unnecessarily postpone the LCD command execution into the future. The command should be executed as soon as possible. Thus, I have to check the condition first before I decide to wait for it to become true.

schorg commented 4 years ago

In the current compiler a run <activity> guarantees that the activity stops at least at one await.

The necessary condition is that every thread/trail must stop at least at one await (must not be instantaneous). This is currently guaranteed by either an await or a run <activity>.

@[EntryPoint]
activity main()
    ... // no await necessary
    cobegin
        run <activity>  // at least one await
    with
        run <activity>  // at least one await
    end
    ... // no await necessary
end

The compiler keeps track of the complete run hierarchy from the entry point activity.

These conditions could be weakened.

If an activity has an instantaneous path, such an activity could be classifed, for example as immediate. An immediate activity does not guarantee to stop at an await.

The following program would be wrong, because the first forked thread/trail might not stop at an await.

@[EntryPoint]
activity main()
    ... // no await necessary
    cobegin
        run <immediate activity>  // at least zero awaits
    with
        run <non-immediate activity>  // at least one await
    end
    ... // no await necessary
end

Adding an await in every path of the thread/trail would make the program correct. For example:

@[EntryPoint]
activity main()
    ... // no await necessary
    cobegin
        ...
        await <cond>
        ...
        run <immediate activity>  // at least zero awaits
    with
        run <non-immediate activity>  // at least one await
    end
    ... // no await necessary
end

The compiler could keep track and could deduce/infer this immediate property. The programmer could define an activity as immediate (new keyword to be chosen), telling the compiler: I plan to use an instantaneous path - even if the current implementation is not instantaneous.

immediate activity noAwait()
    // allows an instantaneous path
end

Problem solved. Should we do this?

frameworklabs commented 4 years ago

I somehow don't like this approach for two reasons:

  1. The caller of an immediate activity might now have to add an unconditional await to his code block (if there is no other stop already).
  2. At the call-site you don't see if this is an instantaneous activity or not complicating human reasoning about your code.
mterber commented 4 years ago

Actually, I do not want to decide or vote whether to implement this feature or not. With the above example I just wanted to show the following:

Today, Blech follows a black-and-white approach. Code abstraction is provided by activities and functions. Functions must never await, activities always have to. This leads to a very sharp distinction between instantaneous and reactive code – a very reasonable and clear separation of concerns which I really like and support!

My example (LCD_printCounter), however, shows that there might be use cases where I need to implement code in Blech which might be either reactive or instantaneous and where the decision is not done / known (statically) during development time but (dynamically) during runtime. As it turns out, today, I basically have one option to deal with that:

I explicitly have to separate the conditional, instantaneous part from the one which is guaranteed to await. In my example, this would mean that the caller of LCD_printCounter has to check whether the queue is full or not. While this approach would be perfectly in line with the synchronous-reactive programming scheme, I see several drawbacks from a software engineering point of view:

  1. Violates "do not repeat yourself".
  2. Low cohesion between things that belong together (queue check and queue manipulation).
  3. Lack encapsulation -> queue check and queue manipulation are distributed across different activities / functions.
  4. No clear abstraction / interface -> the caller has to do some steps (the queue check) in advance in order to make the function (queue manipulation) work properly.

To sum it up: How to properly abstract code in Blech which might be either reactive or instantaneous?

frameworklabs commented 4 years ago

Actually, your initial solution does not have any of the drawbacks you listed - it might just not be optimal from a timing perspective (as you might waste a step when the queue is not full).

Moving the queue check to the callers can be seen as an optimization but you pay for it with the drawbacks you mentioned.

I like the clear separation between instantaneous and stepping code as currently present, but if we want to blur the line I could see an alternative to the immediate activity approach mentioned:

What if await <condition> can be instantaneous instead?

Your code could look like this then:

activity print(stuff)
  await not queueIsFull() // if queue is not full await is instantaneous
  enqueuStuffToPrint(stuff)
end

I don't know of the consequences to the semantics of the language resulting from such a change, but await to wait for a time-step even if the condition is true in the current one was something that surprised me when I first familiarized with the synchronous MoC.

frameworklabs commented 4 years ago

Adding to the above instantaneous await idea:

To have a non-instantaneous await you might need to say something like this: await next <condition>

So, to wait for the next tick you would have to say await next true.

schorg commented 4 years ago

The non-instantaneous await is necessary for 2 reasons.

  1. It syntactically slices the control flow into reaction blocks (the code to be executed in one step). These blocks are the basis of causality analysis. With an instantaneous await, the compiler cannot slice the program into reaction blocks, because the "places" where the execution pauses are determined from runtime properties of the condition.
  2. Due to the same reason, the compiler can prevent infinitely many statements within one reaction. With instantaneous await the following could loop forever
    repeat
    ...
    <instantenous> await condition
    ...
    end
schorg commented 4 years ago

Traditional synchronous languages usually have a pause statement, which slices the control flow into reaction blocks. Blech's await condition can be expressed with pause.

repeat
    pause
until condition end

await true is just pause. The academic language Quartz, has pause, await condition and immediate await condition - which is instantaneous. As pointed out before immediate await condition is just

if not condition then
    await condition
end

Which could be expressed with pause, by inlining the await condition substitution.

if not condition then
    repeat
        pause
    until condition end
end

All this is only valid if the evaluation of condition does not have side effects, which is guaranteed in Blech. In Blech we decided against pause and against immediate await.

schorg commented 4 years ago

@frameworklabs wrote:

I somehow don't like this approach for two reasons:

  1. The caller of an immediate activity might now have to add an unconditional await to his code block (if there is no other stop already).
  2. At the call-site you don't see if this is an instantaneous activity or not complicating human reasoning about your code.

Both are very good arguments. Given that Blech is made for easier reasoning about your code - for example by having separate input and output parameter lists - I will try to work out something else ...

mterber commented 4 years ago

Actually, your initial solution does not have any of the drawbacks you listed - it might just not be optimal from a timing perspective (as you might waste a step when the queue is not full).

Timing is one aspect. Another point is that you have to find a reasonable condition that you can await which might not always be as straight forward as in the above example. Using await true as a general workaround wouldn't be a good solution from my point of view. Depending on how your Blech code is triggered, e.g. entirely event-driven without any periodic ticks, it is unclear at which point in time the next reaction (await true) will be performed. So LCD_printCounter might be suspended for several seconds, minutes or even hours until it has the chance to detect that the condition is (already) true. By this, your system might become inoperable.

I completely agree with both of you and I also do not like the approach to blur the borders between instantaneous and stepping code. However, I think that the use case described above is not only some special corner case. To generalize it: What I basically want to do is to access a resource in the C environment which is either free or occupied. If is already free (in the same reaction in which I decide that I want to use the resource) then I want to use it instantaneously. If it is occupied then I have to wait for the resource to become free again. The pattern could look like this:

activity UseResource ()
  if not isFree() then
    await isFree()
  end
  useResource() // now the resource is occupied
end

This pattern, however, leads to the problem mentioned above. So to overcome this I could add:

activity UseResource ()
  if not isFree() then
    await isFree()
  end
  useResource()
  await isFree() // wait for the resource to become free again
end

In principle, this solves the problem. This approach, however, fails if using the resource is actually an instantaneous operation. E.g. "using" the queue just means to push a new element to it. This happens instantaneously so that it would be possible to push multiple elements to the same queue within the same reaction. But maybe exactly this is the conceptual flaw of the approach that I tried to implement. Maybe, in the "synchronous world", the correct approach would be to distribute the insertion of N queue elements across N consecutive reactions. Still, the question remains on how to reasonably suspend the current reaction after each push operation...

frameworklabs commented 4 years ago

Would it be too much of a hack in an event-driven trigger mode to inform the runtime via some flag or method call to immediately start the next step when the current one is finished?

activity useResource
  if not isAvailable() then 
    await isAvailable()
    useResource() // BTW do act and fun have  separate identifier sets?
  else
    useResource()
    notifyWantsImmediateNextStep()
    await true
  end
end
schorg commented 4 years ago

Would it be too much of a hack in an event-driven trigger mode to inform the runtime via some flag or method call to immediately start the next step when the current one is finished?

Would be perfectly fine, could be done with an external C function (e.g. a software interrupt).

useResource() // BTW do act and fun have  separate identifier sets?

No, currently not. Would be very unusual. Some languages separate identifier sets for types and modules from variables and subprograms. Variables and subprograms usually fall into one category.

FriedrichGretz commented 4 years ago

Here is my 2 cents:

I'd make LCD_printCounter a boolean function

function LCD_printCounter (line: nat8, num: nat32)
    if EXT_lcdQueueIsFull() then
        return false
    else
        _ = EXT_lcdQueuePutPrintCmd(line, num)
        return true
    end
end

then its usage pattern is an immediate-await style call

activity Receiver (sysTick: bool)(leds: LedStates)
    var cRx: nat32
    [...] // instantaneous code
    repeat
        [...] // instantaneous code
        res = run COM_receive()(data, len) // Await next packet.
        [...] // instantaneous code
        if not LCD_printCounter(15, true, successCount) then
            await LCD_printCounter(15, true, successCount)
        end
        [...] // instantaneous code
    end
end

The trick is that the side-effect free function LCD_printCounter will be tried with every tick until the queue is not full and it succeeds, which corresponds to your desired behaviour. Well, actually it obviously does have a side-effect (adding to the queue) but Blech does not know that and therefore would allow its usage in the context of an await. Elegant feature or ugly hack? You decide.

The annoying bit is that whenever you want to use it you have to follow this pattern

if not foo() then
    await foo()
end

Maybe, just maybe, the syntactic sugar immediate-await foo() is not that bad after all?

edit: But regarding the issue title, still activities must not have any instantaneous control paths ;-)