Closed mterber closed 1 month 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.
@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.
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>
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.
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?
I somehow don't like this approach for two reasons:
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:
To sum it up: How to properly abstract code in Blech which might be either reactive or instantaneous?
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.
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
.
The non-instantaneous await is necessary for 2 reasons.
repeat
...
<instantenous> await condition
...
end
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
.
@frameworklabs wrote:
I somehow don't like this approach for two reasons:
- 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).
- 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 ...
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...
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
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.
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 ;-)
Is your feature request related to a problem? Please describe. Consider the following example code:
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?