Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup the C++ ReflexGame and add an enclaved based keyboard input reactor #88

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions examples/Cpp/ReflexGame/src/ReflexGame.cmake

This file was deleted.

157 changes: 79 additions & 78 deletions examples/Cpp/ReflexGame/src/ReflexGame.lf
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,97 @@
* This example illustrates the use of logical and physical actions, asynchronous external inputs,
* the use of startup and shutdown reactions, and the use of actions with values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should acknowledge the original source of the example:

 * The example is fashioned after an Esterel implementation given by Berry and Gonthier in "The
 * ESTEREL synchronous programming language: design, semantics, implementation," Science of Computer
 * Programming, 19(2) pp. 87-152, Nov. 1992, DOI: 10.1016/0167-6423(92)90005-V.

*
* @author Christian Menard
* @author Felix Wittwer
* @author Edward A. Lee
* @author Marten Lohstroh
*/
target Cpp {
cmake-include: "ReflexGame.cmake"
}
target Cpp

/**
* Produce a counting sequence at random times with a minimum and maximum time between outputs
* specified as parameters.
*
* @param min_time The minimum time between outputs.
* @param max_time The maximum time between outputs.
*/
reactor RandomSource(min_time: time(2 sec), max_time: time(8 sec)) {
private preamble {=
// Generate a random additional delay over the minimum.
// Assume millisecond precision is enough.
reactor::Duration additional_time(reactor::Duration min_time, reactor::Duration max_time) {
int interval_in_msec = (max_time - min_time) / std::chrono::milliseconds(1);
return (std::rand() % interval_in_msec) * std::chrono::milliseconds(1);
}
reactor RandomDelay(min_delay: time(2 sec), max_delay: time(8 sec)) {
public preamble {=
#include <random>
=}
input another: void

input in: void
output out: void
logical action prompt(min_time)
state count: int(0)
logical action delay(min_delay): void

reaction(startup) -> prompt {=
std::cout << "***********************************************" << std::endl;
std::cout << "Watch for the prompt, then hit Return or Enter." << std::endl;
std::cout << "Type Control-D (EOF) to quit." << std::endl << std::endl;
// a random number generator seeded with the current physical time
state rand: std::mt19937({= get_physical_time().time_since_epoch().count() =})
// a uniform random distribution with a range from 1 to (max_delay - min_delay) milliseconds
state dist: std::uniform_int_distribution<int>(
1,
{= (max_delay - min_delay) / std::chrono::milliseconds(1) =})

// TODO: Manual inclusion of header necessary?
// Set a seed for random number generation based on the current time.
std::srand(std::time(nullptr));
reaction(delay) -> out {=
out.set();
=}

// Schedule the first event.
prompt.schedule(additional_time(0ms, max_time - min_time));
reaction(in) -> delay {=
delay.schedule(dist(rand) * std::chrono::milliseconds(1));
=}
}

reaction(prompt) -> out {=
count++;
std::cout << count << ". Hit Return or Enter!" << std::endl << std::flush;
out.set();
reactor KeyboardInput {
state thread: std::thread
state terminate: std::atomic<bool> = false
physical action keyboard_input: char

output enter: void
output quit: void

reaction(startup) -> keyboard_input {=
// Start the thread that listens for keyboard input.
thread = std::thread([&] () {
int c{0};
while(!terminate.load()) {
c = getchar();
keyboard_input.schedule(c);
}
});
=}

reaction(another) -> prompt {=
// Schedule the next event.
prompt.schedule(additional_time(0ms, max_time - min_time));
reaction(keyboard_input) -> enter, quit {=
char key = *keyboard_input.get();
if(key == '\n') {
enter.set();
} else if (key == EOF) {
quit.set();
}
=}
}

/**
* Upon receiving a prompt, record the time of the prompt, then listen for user input. When the user
* hits return, then schedule a physical action that records the time of this event and then report
* the response time.
*/
reactor GetUserInput {
public preamble {=
#include <thread>
reaction(shutdown) {=
terminate.store(true);
thread.join();
=}
}

physical action user_response: char
state prompt_time: {= reactor::TimePoint =}({= reactor::TimePoint::min() =})
reactor GameLogic {
output request_prompt: void
input prompt: void
input enter: void
input quit: void

state prompt_time: {= reactor::TimePoint =} = {= reactor::TimePoint::min() =}
state count: unsigned = 0
state total_time: time(0)
state count: int(0)
state thread: {= std::thread =}

input prompt: void
output another: void
reaction(startup) -> request_prompt {=
std::cout << "***********************************************" << std::endl;
std::cout << "Watch for the prompt, then hit Return or Enter." << std::endl;
std::cout << "Type Control-D (EOF) to quit." << std::endl << std::endl;

reaction(startup) -> user_response {=
// Start the thread that listens for Enter or Return.
thread = std::thread([&] () {
int c;
while(1) {
while((c = getchar()) != '\n') {
if (c == EOF) break;
}
user_response.schedule(c, 0ms);
if (c == EOF) break;
}
});
// request the first prompt
request_prompt.set();
=}

reaction(prompt) {=
prompt_time = get_physical_time();
std::cout << std::endl << "Hit Return or Enter!" << std::endl;
=}

reaction(user_response) -> another {=
auto c = user_response.get();
if (*c == EOF) {
environment()->sync_shutdown();
return;
}
reaction(enter) -> request_prompt {=
// If the prompt_time is 0, then the user is cheating and
// hitting return before being prompted.
if (prompt_time == reactor::TimePoint::min()) {
Expand All @@ -113,13 +107,16 @@ reactor GetUserInput {
total_time += time_in_ms;
// Reset the prompt_time to indicate that there is no new prompt.
prompt_time = reactor::TimePoint::min();
// Trigger another prompt.
another.set();
// Request another prompt.
request_prompt.set();
}
=}

reaction(quit) {=
environment()->sync_shutdown();
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
=}

reaction(shutdown) {=
thread.join();
if (count > 0) {
std::cout << std::endl << "**** Average response time: " << std::chrono::duration_cast<std::chrono::milliseconds>(total_time/count) << std::endl;
} else {
Expand All @@ -128,9 +125,13 @@ reactor GetUserInput {
=}
}

main reactor ReflexGame {
p = new RandomSource()
g = new GetUserInput()
p.out -> g.prompt
g.another -> p.another
main reactor {
delay = new RandomDelay()
keyboard = new KeyboardInput()
logic = new GameLogic()

logic.request_prompt -> delay.in
delay.out -> logic.prompt
keyboard.enter -> logic.enter
keyboard.quit -> logic.quit
}
52 changes: 52 additions & 0 deletions examples/Cpp/ReflexGame/src/ReflexGameEnclave.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* This example illustrates the use of enclaves to avoid spawning an extra thread for handling user
* input.
*
* The thread that is created in the startup reaction of KeyboardInput in eflexgame.lf uses a while
* loop. This loop is replaced by a enclave with a reaction that schedules using a physical action.
* A physical action was chosen here, because it alligns the progress of logical time in the enclave
* with the progress of physical time. Also it avoids the need for specifying or calculating a
* delay.
*
* The outputs that the EnclaveKeybardInput reactor produce are forwarded to the other reactors via
* physical actions. This fully decouples the input from the rest of the system, and time stamps of
* keyboard events are assigned based on the current physical time.
*
* @author Christian Menard
*/
target Cpp

import GameLogic from "ReflexGame.lf"
import RandomDelay from "ReflexGame.lf"

reactor EnclaveKeyboardInput {
physical action get_next

output enter: void
output quit: void

reaction(startup, get_next) -> get_next, enter, quit {=
int key = getchar();
if(key == '\n') {
enter.set();
get_next.schedule();
} else if (key == EOF) {
quit.set();
environment()->sync_shutdown();
} else {
get_next.schedule();
}
=}
}

main reactor {
@enclave
keyboard = new EnclaveKeyboardInput()
delay = new RandomDelay()
logic = new GameLogic()

logic.request_prompt -> delay.in
delay.out -> logic.prompt
keyboard.enter ~> logic.enter
keyboard.quit ~> logic.quit
}
Loading