diff --git a/CMakeLists.txt b/CMakeLists.txt index bfcc76793..9876ea9c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,7 +81,7 @@ endif() target_compile_definitions(reactor-uc PUBLIC "PLATFORM_${PLATFORM}") # Add compile definition for scheduler used -target_compile_definitions(reactor-uc PRIVATE "SCHEDULER_${SCHEDULER}") +target_compile_definitions(reactor-uc PUBLIC "SCHEDULER_${SCHEDULER}") # Add compile definitions for event and reaction queue sizes. Has to be PUBLIC because they are used in the header files. message(STATUS "Setting event queue size to ${EVENT_QUEUE_SIZE} and reaction queue size to ${REACTION_QUEUE_SIZE}") diff --git a/examples/posix/static-connection/src/SimpleConnection.lf b/examples/posix/static-connection/src/SimpleConnection.lf new file mode 100644 index 000000000..f0d937c77 --- /dev/null +++ b/examples/posix/static-connection/src/SimpleConnection.lf @@ -0,0 +1,47 @@ +target uC + +preamble {= + #define EXPECTED -9 +=} + +reactor Source { + output out:int + timer t(0, 1 sec) + state s:int = 0 + reaction(t) -> out {= + lf_set(out, self->s); + lf_print("Sent %d @ %lld", self->s++, lf_time_logical_elapsed()); + =} + reaction(t) -> out {= + int v = -1 * self->s; + lf_set(out, v); + =} +} + +reactor Sink { + input in:int + state last_received:int = 0 + reaction(in) {= + self->last_received = in->value; + =} + // FIXME: Multiple reactions triggered by the same port does not yet work. + reaction(in) {= + lf_print("Received %d @ %lld", in->value, lf_time_logical_elapsed()); + =} + reaction(shutdown) {= + if (self->last_received != EXPECTED) { + fprintf(stderr, "FAILURE: Expected %d, Received %d\n", EXPECTED, self->last_received); + exit(1); + } else { + lf_print("Successfully received %d", self->last_received); + } + =} +} + +main reactor { + source = new Source() + sink = new Sink() + source.out -> sink.in after 2 sec + // source.out -> sink.in after 500 msec + // source.out -> sink.in +} \ No newline at end of file diff --git a/examples/posix/static-connection/src/lf_main.c b/examples/posix/static-connection/src/lf_main.c new file mode 100644 index 000000000..02c44edf8 --- /dev/null +++ b/examples/posix/static-connection/src/lf_main.c @@ -0,0 +1,99 @@ +#include "reactor-uc/macros.h" +#include "SimpleConnection/SimpleConnection.h" +#include "reactor-uc/schedulers/static/scheduler.h" +#include "reactor-uc/schedulers/static/instructions.h" +#include "reactor-uc/schedulers/static/circular_buffer.h" + +#define WORKER_0_SYNC_BLOCK 0LL +#define WORKER_0_INIT 4LL +#define WORKER_0_CLEANUP_0 7LL +#define WORKER_0_END 14LL + + +Reactor_SimpleConnection main_reactor; +Environment env; +Environment *_lf_environment = &env; +void lf_exit(void) { Environment_free(&env); } +void lf_start() { + Environment_ctor(&env, (Reactor *)&main_reactor); + Reactor_SimpleConnection_ctor(&main_reactor, NULL, &env); + env.scheduler->duration = NEVER; + env.scheduler->keep_alive = false; + env.fast_mode = false; + env.assemble(&env); +} +void lf_execute() { + env.start(&env); + lf_exit(); +} + +void conn_0_pop_event_and_prepare_port(TriggerBuffer *trigger_buffer) { + CircularBuffer buffer = trigger_buffer->buffer; + Event e; + cb_pop_front(&buffer, &e); + main_reactor.conn_source_out_0[0][0].super.super.super.prepare(&main_reactor.conn_source_out_0[0][0], &e); +} + +int main() { + lf_start(); + StaticSchedulerState* state = &((StaticScheduler*)_lf_environment->scheduler)->state; + const size_t reactor_tags_size = 3; + ReactorTagPair reactor_tags[reactor_tags_size] = {{&main_reactor, 0}, {&main_reactor.source, 0}, {&main_reactor.sink, 0}}; + size_t trigger_buffers_size = 1; + TriggerBuffer trigger_buffers[trigger_buffers_size]; + cb_init(&trigger_buffers[0].buffer, 10, sizeof(Event)); + trigger_buffers[0].trigger = &main_reactor.conn_source_out_0[0][0]; + inst_t schedule_0[] = { + // WORKER_0_PREAMBLE: + // Line 0: Increment OFFSET by adding START_TIME and 0LL + {.func=execute_inst_ADDI, .opcode=ADDI, .op1.reg=(reg_t*)&(state->time_offset), .op2.reg=(reg_t*)&(state->start_time), .op3.imm=0LL}, + // Line 1: Increment TIMEOUT by adding START_TIME and 10000000000LL + {.func=execute_inst_ADDI, .opcode=ADDI, .op1.reg=(reg_t*)&(state->timeout), .op2.reg=(reg_t*)&(state->start_time), .op3.imm=10000000000LL}, + // Line 2: Increment OFFSET_INC by adding ZERO and 0LL + {.func=execute_inst_ADDI, .opcode=ADDI, .op1.reg=(reg_t*)&(state->offset_inc), .op2.reg=(reg_t*)&(state->zero), .op3.imm=0LL}, + // Line 3: JAL: store return address in ZERO and jump to INIT + {.func=execute_inst_JAL, .opcode=JAL, .op1.reg=(reg_t*)&(state->zero), .op2.imm=WORKER_0_INIT, .op3.imm=0}, + // WORKER_0_EXECUTE_SimpleConnection_source_reaction_1_6570d54d: + // WORKER_0_INIT: + // Line 4: Execute function envs[simpleconnection_main].reaction_array[0]->function with argument envs[simpleconnection_main].reactor_self_array[1] + // IMPORTANT NOTE: To let reaction body and DelayConnection_cleanup both use scheduler->current_tag, which needs to access the reactor's pointer, pass the reactor pointer from op3, which can be accessed from current_tag to identify the reactor. + {.func=execute_inst_EXE, .opcode=EXE, .op1.reg=(reg_t*)(main_reactor.source->reaction0.super.body), .op2.reg=(reg_t*)&(main_reactor.source->reaction0.super), .op3.reg=(reg_t*)&(main_reactor.source)}, + // Line 5: Increment Worker 0's COUNTER by adding Worker 0's COUNTER and 1LL + {.func=execute_inst_ADDI, .opcode=ADDI, .op1.reg=(reg_t*)&(state->counter), .op2.reg=(reg_t*)&(state->counter), .op3.imm=1LL}, + + // Line 6: Check if the port is being set by the reaction. If so, invoke the cleanup function to push the payload into the buffer. Otherwise, skip the cleanup, which is not supposed to be called when the out port is not set. + {.func=execute_inst_BEQ, .opcode=BEQ, .op1.reg=(reg_t*)&(main_reactor.source->out->super.super.is_present), .op2.reg=(reg_t*)&(state->zero), .op3.imm=WORKER_0_CLEANUP_0 + 1}, + + // Line 7: Call DelayConnection_cleanup to get to the schedule_at in StaticScheduler. + {.func=execute_inst_EXE, .opcode=EXE, .op1.reg=(reg_t*)(main_reactor.conn_source_out_0[0][0].super.super.super.cleanup), .op2.reg=(reg_t*)&(main_reactor.conn_source_out_0[0][0]), .op3.reg=(reg_t*)&(main_reactor.source)}, + + // Line 8: Advance time for sink = reactor_tags[2].tag.time + {.func=execute_inst_ADDI, .opcode=ADDI, .op1.reg=(reg_t*)&(reactor_tags[2].tag.time), .op2.reg=(reg_t*)&(reactor_tags[2].tag.time), .op3.imm=2000000000LL}, + + // Line 9: Check if the current count of the connection buffer is 0. + {.func=execute_inst_BEQ, .opcode=BEQ, .op1.reg=(reg_t*)&(trigger_buffers[0].buffer.count), .op2.reg=(reg_t*)&(state->zero), .op3.imm=WORKER_0_END}, + + // Line 10: If not null, check if the time field of the current head matches the time of sink. If they don't, skip the reaction. + {.func=execute_inst_BNE, .opcode=BEQ, .op1.reg=(reg_t*)&(((Event*)trigger_buffers[0].buffer.head)->tag.time), .op2.reg=(reg_t*)&(reactor_tags[2].tag.time), .op3.imm=WORKER_0_END}, + + // Line 11: Pop the event from the connection buffer and prepare the downstream port. + {.func=execute_inst_EXE, .opcode=EXE, .op1.reg=(reg_t*)conn_0_pop_event_and_prepare_port, .op2.reg=(reg_t*)&trigger_buffers[0]}, + + // WORKER_0_EXECUTE_SimpleConnection_sink_reaction_1_7ed46f7f: + // Line 12: Execute function envs[simpleconnection_main].reaction_array[2]->function with argument envs[simpleconnection_main].reactor_self_array[2] + {.func=execute_inst_EXE, .opcode=EXE, .op1.reg=(reg_t*)(main_reactor.sink->reaction0.super.body), .op2.reg=(reg_t*)&((main_reactor.sink->reaction0.super)), .op3.reg=(reg_t*)&(main_reactor.sink)}, + + // WORKER_0_EXECUTE_SimpleConnection_sink_reaction_2_f52ba644: + // Line 13: Execute function envs[simpleconnection_main].reaction_array[3]->function with argument envs[simpleconnection_main].reactor_self_array[2] + {.func=execute_inst_EXE, .opcode=EXE, .op1.reg=(reg_t*)(main_reactor.sink->reaction1.super.body), .op2.reg=(reg_t*)&((main_reactor.sink->reaction1.super)), .op3.reg=(reg_t*)&(main_reactor.sink)}, + + // Line 14 + {.func=execute_inst_STP, .opcode=STP}, + }; + ((StaticScheduler*)_lf_environment->scheduler)->static_schedule = schedule_0; + ((StaticScheduler*)_lf_environment->scheduler)->reactor_tags = reactor_tags; + ((StaticScheduler*)_lf_environment->scheduler)->reactor_tags_size = reactor_tags_size; + ((StaticScheduler*)_lf_environment->scheduler)->trigger_buffers = trigger_buffers; + ((StaticScheduler*)_lf_environment->scheduler)->trigger_buffers_size = trigger_buffers_size; + lf_execute(); +} diff --git a/examples/posix/static/CMakeLists.txt b/examples/posix/static/CMakeLists.txt new file mode 100644 index 000000000..f44892b67 --- /dev/null +++ b/examples/posix/static/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20.0) +project(reactor-uc-posix-hello) + +set(PLATFORM "POSIX" CACHE STRING "Platform to target") +set(SCHEDULER "STATIC" CACHE STRING "Scheduler to use") +add_subdirectory(reactor-uc) + +add_executable(app R1.c R2.c static_schedule.c Connections.c) +target_link_libraries(app PRIVATE reactor-uc) \ No newline at end of file diff --git a/examples/posix/static/Connections.c b/examples/posix/static/Connections.c new file mode 100644 index 000000000..9d770336f --- /dev/null +++ b/examples/posix/static/Connections.c @@ -0,0 +1,51 @@ +#include "Connections.h" +// Private preambles + +// Reaction bodies + +// Reaction deadline handlers + +// Reaction constructors + + +// Timer constructors + +// Port constructors + +// Connection constructors +LF_DEFINE_LOGICAL_CONNECTION_CTOR(Reactor_Connections, conn_r1_out_0, 1); +LF_DEFINE_DELAYED_CONNECTION_CTOR(Reactor_Connections, conn_r3_out_1, 1, int, 1, MSEC(10), false); +LF_REACTOR_CTOR_SIGNATURE(Reactor_Connections) { + LF_REACTOR_CTOR_PREAMBLE(); + LF_REACTOR_CTOR(Reactor_Connections); + // Initialize Reactor parameters + + // Initialize State variables + LF_DEFINE_CHILD_OUTPUT_ARGS(r1, out, 1, 1); + + LF_INITIALIZE_CHILD_REACTOR_WITH_PARAMETERS(Reactor_R1, r1, 1 , _r1_out_args[i] ); + + + LF_DEFINE_CHILD_INPUT_ARGS(r2, in, 1, 1); + LF_INITIALIZE_CHILD_REACTOR_WITH_PARAMETERS(Reactor_R2, r2, 1 , _r2_in_args[i] , 11); + + LF_DEFINE_CHILD_OUTPUT_ARGS(r3, out, 1, 1); + + LF_INITIALIZE_CHILD_REACTOR_WITH_PARAMETERS(Reactor_R1, r3, 1 , _r3_out_args[i] ); + + + LF_DEFINE_CHILD_INPUT_ARGS(r4, in, 1, 1); + LF_INITIALIZE_CHILD_REACTOR_WITH_PARAMETERS(Reactor_R2, r4, 1 , _r4_in_args[i] , 10); + // Initialize Timers + // Initialize actions and builtin triggers + + // Initialize ports + + // Initialize connections + LF_INITIALIZE_LOGICAL_CONNECTION(Reactor_Connections, conn_r1_out_0, 1, 1); + LF_INITIALIZE_DELAYED_CONNECTION(Reactor_Connections, conn_r3_out_1, 1, 1); + // Do connections + lf_connect((Connection *) &self->conn_r1_out_0[0][0], (Port *) &self->r1[0].out[0], (Port *) &self->r2[0].in[0]); + lf_connect((Connection *) &self->conn_r3_out_1[0][0], (Port *) &self->r3[0].out[0], (Port *) &self->r4[0].in[0]); + // Initialize Reactions +} diff --git a/examples/posix/static/Connections.h b/examples/posix/static/Connections.h new file mode 100644 index 000000000..c296b6824 --- /dev/null +++ b/examples/posix/static/Connections.h @@ -0,0 +1,66 @@ +#ifndef LFC_GEN_CONNECTIONS_H +#define LFC_GEN_CONNECTIONS_H + +#include "reactor-uc/reactor-uc.h" +#include "_lf_preamble.h" + +// Include instantiated reactors +#include "R1.h" +#include "R2.h" +// Reaction structs + +// Timer structs + + +// Port structs + +// Connection structs +LF_DEFINE_LOGICAL_CONNECTION_STRUCT(Reactor_Connections, conn_r1_out_0, 1); +LF_DEFINE_DELAYED_CONNECTION_STRUCT(Reactor_Connections, conn_r3_out_1, 1, int, 1, MSEC(10)); +//The reactor self struct +typedef struct { + Reactor super; + // Child reactor fields + + LF_CHILD_REACTOR_INSTANCE(Reactor_R1, r1, 1); + + LF_CHILD_OUTPUT_CONNECTIONS(r1, out, 1, 1, 1); + LF_CHILD_OUTPUT_EFFECTS(r1, out, 1, 1, 0); + LF_CHILD_OUTPUT_OBSERVERS(r1, out, 1, 1, 0); + + + LF_CHILD_REACTOR_INSTANCE(Reactor_R2, r2, 1); + + + LF_CHILD_INPUT_SOURCES(r2, in, 1, 1, 0); + + LF_CHILD_REACTOR_INSTANCE(Reactor_R1, r3, 1); + + LF_CHILD_OUTPUT_CONNECTIONS(r3, out, 1, 1, 1); + LF_CHILD_OUTPUT_EFFECTS(r3, out, 1, 1, 0); + LF_CHILD_OUTPUT_OBSERVERS(r3, out, 1, 1, 0); + + + LF_CHILD_REACTOR_INSTANCE(Reactor_R2, r4, 1); + + + LF_CHILD_INPUT_SOURCES(r4, in, 1, 1, 0); + + // Timers + + // Actions and builtin triggers + + // Connections + LF_LOGICAL_CONNECTION_INSTANCE(Reactor_Connections, conn_r1_out_0, 1, 1); + LF_DELAYED_CONNECTION_INSTANCE(Reactor_Connections, conn_r3_out_1, 1, 1); + // Ports + + // State variables + // Reactor parameters + + LF_REACTOR_BOOKKEEPING_INSTANCES(0, 0, 4); +} Reactor_Connections; + +LF_REACTOR_CTOR_SIGNATURE(Reactor_Connections); + +#endif // LFC_GEN_CONNECTIONS_H diff --git a/examples/posix/static/R1.c b/examples/posix/static/R1.c new file mode 100644 index 000000000..7b58b9afe --- /dev/null +++ b/examples/posix/static/R1.c @@ -0,0 +1,62 @@ +#include "R1.h" +// Private preambles + +// Reaction bodies +LF_DEFINE_REACTION_BODY(Reactor_R1, reaction0) { + // Bring self struct, environment, triggers, effects and sources into scope. + LF_SCOPE_SELF(Reactor_R1); + LF_SCOPE_ENV(); + LF_SCOPE_STARTUP(Reactor_R1); + // Start of user-witten reaction deadline handler + printf("Startup!\n"); +} +LF_DEFINE_REACTION_BODY(Reactor_R1, reaction1) { + // Bring self struct, environment, triggers, effects and sources into scope. + LF_SCOPE_SELF(Reactor_R1); + LF_SCOPE_ENV(); + LF_SCOPE_TIMER(Reactor_R1, t); + LF_SCOPE_PORT(Reactor_R1, out); + // Start of user-witten reaction deadline handler + printf("Hello from R1 at %ld\n", env->get_elapsed_logical_time(env)); + lf_set(out, self->cnt++); +} +// Reaction deadline handlers + +// Reaction constructors +LF_DEFINE_REACTION_CTOR(Reactor_R1, reaction0, 0 ); +LF_DEFINE_REACTION_CTOR(Reactor_R1, reaction1, 1 ); +LF_DEFINE_STARTUP_CTOR(Reactor_R1); +// Timer constructors +LF_DEFINE_TIMER_CTOR(Reactor_R1, t, 1, 0); +// Port constructors +LF_DEFINE_OUTPUT_CTOR(Reactor_R1, out, 1); +// Connection constructors + +LF_REACTOR_CTOR_SIGNATURE_WITH_PARAMETERS(Reactor_R1 , OutputExternalCtorArgs *_out_external ) { + LF_REACTOR_CTOR_PREAMBLE(); + LF_REACTOR_CTOR(Reactor_R1); + // Initialize Reactor parameters + + // Initialize State variables + self->cnt = 0; + + // Initialize Timers + LF_INITIALIZE_TIMER(Reactor_R1, t, 0, MSEC(10)); + // Initialize actions and builtin triggers + + LF_INITIALIZE_STARTUP(Reactor_R1); + // Initialize ports + LF_INITIALIZE_OUTPUT(Reactor_R1, out, 1, _out_external); + // Initialize connections + + // Do connections + + // Initialize Reactions + LF_INITIALIZE_REACTION(Reactor_R1, reaction0); + LF_STARTUP_REGISTER_EFFECT(self->reaction0); + + + LF_INITIALIZE_REACTION(Reactor_R1, reaction1); + LF_TIMER_REGISTER_EFFECT(self->t, self->reaction1); + LF_PORT_REGISTER_SOURCE(self->out, self->reaction1, 1); +} diff --git a/examples/posix/static/R1.h b/examples/posix/static/R1.h new file mode 100644 index 000000000..d5b452566 --- /dev/null +++ b/examples/posix/static/R1.h @@ -0,0 +1,43 @@ +#ifndef LFC_GEN_R1_H +#define LFC_GEN_R1_H + +#include "reactor-uc/reactor-uc.h" +#include "_lf_preamble.h" + +// Include instantiated reactors +// Reaction structs +LF_DEFINE_REACTION_STRUCT(Reactor_R1, reaction0, 0); +LF_DEFINE_REACTION_STRUCT(Reactor_R1, reaction1, 1); +// Timer structs +LF_DEFINE_TIMER_STRUCT(Reactor_R1, t, 1, 0); +LF_DEFINE_STARTUP_STRUCT(Reactor_R1, 1, 0); +// Port structs +LF_DEFINE_OUTPUT_STRUCT(Reactor_R1, out, 1, int); +// Connection structs + +//The reactor self struct +typedef struct { + Reactor super; + // Child reactor fields + + LF_REACTION_INSTANCE(Reactor_R1, reaction0); + LF_REACTION_INSTANCE(Reactor_R1, reaction1); + // Timers + LF_TIMER_INSTANCE(Reactor_R1, t); + // Actions and builtin triggers + + LF_STARTUP_INSTANCE(Reactor_R1); + // Connections + + // Ports + LF_PORT_INSTANCE(Reactor_R1, out, 1); + // State variables + int cnt; + // Reactor parameters + + LF_REACTOR_BOOKKEEPING_INSTANCES(2, 3, 0); +} Reactor_R1; + +LF_REACTOR_CTOR_SIGNATURE_WITH_PARAMETERS(Reactor_R1 , OutputExternalCtorArgs *_out_external ); + +#endif // LFC_GEN_R1_H diff --git a/examples/posix/static/R2.c b/examples/posix/static/R2.c new file mode 100644 index 000000000..137ff58c8 --- /dev/null +++ b/examples/posix/static/R2.c @@ -0,0 +1,76 @@ +#include "R2.h" +// Private preambles + +// Reaction bodies +LF_DEFINE_REACTION_BODY(Reactor_R2, reaction0) { + // Bring self struct, environment, triggers, effects and sources into scope. + LF_SCOPE_SELF(Reactor_R2); + LF_SCOPE_ENV(); + LF_SCOPE_STARTUP(Reactor_R2); + // Start of user-witten reaction deadline handler + printf("Hi from other guy at startup\n"); +} +LF_DEFINE_REACTION_BODY(Reactor_R2, reaction1) { + // Bring self struct, environment, triggers, effects and sources into scope. + LF_SCOPE_SELF(Reactor_R2); + LF_SCOPE_ENV(); + LF_SCOPE_PORT(Reactor_R2, in); + // Start of user-witten reaction deadline handler + printf("Got %d\n", in->value); + self->cnt++; +} +LF_DEFINE_REACTION_BODY(Reactor_R2, reaction2) { + // Bring self struct, environment, triggers, effects and sources into scope. + LF_SCOPE_SELF(Reactor_R2); + LF_SCOPE_ENV(); + LF_SCOPE_SHUTDOWN(Reactor_R2); + // Start of user-witten reaction deadline handler + validate(self->cnt == self->expected); +} +// Reaction deadline handlers + +// Reaction constructors +LF_DEFINE_REACTION_CTOR(Reactor_R2, reaction0, 0 ); +LF_DEFINE_REACTION_CTOR(Reactor_R2, reaction1, 1 ); +LF_DEFINE_REACTION_CTOR(Reactor_R2, reaction2, 2 ); +LF_DEFINE_STARTUP_CTOR(Reactor_R2); +LF_DEFINE_SHUTDOWN_CTOR(Reactor_R2); +// Timer constructors + +// Port constructors +LF_DEFINE_INPUT_CTOR(Reactor_R2, in, 1, 0, int, 0); +// Connection constructors + +LF_REACTOR_CTOR_SIGNATURE_WITH_PARAMETERS(Reactor_R2 , InputExternalCtorArgs *_in_external , int expected ) { + LF_REACTOR_CTOR_PREAMBLE(); + LF_REACTOR_CTOR(Reactor_R2); + // Initialize Reactor parameters + + self->expected = expected; + // Initialize State variables + self->cnt = 0; + + // Initialize Timers + // Initialize actions and builtin triggers + + LF_INITIALIZE_STARTUP(Reactor_R2); + LF_INITIALIZE_SHUTDOWN(Reactor_R2); + // Initialize ports + LF_INITIALIZE_INPUT(Reactor_R2, in, 1, _in_external); + // Initialize connections + + // Do connections + + // Initialize Reactions + LF_INITIALIZE_REACTION(Reactor_R2, reaction0); + LF_STARTUP_REGISTER_EFFECT(self->reaction0); + + + LF_INITIALIZE_REACTION(Reactor_R2, reaction1); + LF_PORT_REGISTER_EFFECT(self->in, self->reaction1, 1); + + + LF_INITIALIZE_REACTION(Reactor_R2, reaction2); + LF_SHUTDOWN_REGISTER_EFFECT(self->reaction2); + +} diff --git a/examples/posix/static/R2.h b/examples/posix/static/R2.h new file mode 100644 index 000000000..df5be6abf --- /dev/null +++ b/examples/posix/static/R2.h @@ -0,0 +1,46 @@ +#ifndef LFC_GEN_R2_H +#define LFC_GEN_R2_H + +#include "reactor-uc/reactor-uc.h" +#include "_lf_preamble.h" + +// Include instantiated reactors +// Reaction structs +LF_DEFINE_REACTION_STRUCT(Reactor_R2, reaction0, 0); +LF_DEFINE_REACTION_STRUCT(Reactor_R2, reaction1, 0); +LF_DEFINE_REACTION_STRUCT(Reactor_R2, reaction2, 0); +// Timer structs + +LF_DEFINE_STARTUP_STRUCT(Reactor_R2, 1, 0); +LF_DEFINE_SHUTDOWN_STRUCT(Reactor_R2, 1, 0); +// Port structs +LF_DEFINE_INPUT_STRUCT(Reactor_R2, in, 1, 0, int, 0); +// Connection structs + +//The reactor self struct +typedef struct { + Reactor super; + // Child reactor fields + + LF_REACTION_INSTANCE(Reactor_R2, reaction0); + LF_REACTION_INSTANCE(Reactor_R2, reaction1); + LF_REACTION_INSTANCE(Reactor_R2, reaction2); + // Timers + + // Actions and builtin triggers + + LF_STARTUP_INSTANCE(Reactor_R2);LF_SHUTDOWN_INSTANCE(Reactor_R2); + // Connections + + // Ports + LF_PORT_INSTANCE(Reactor_R2, in, 1); + // State variables + int cnt; + // Reactor parameters + int expected; + LF_REACTOR_BOOKKEEPING_INSTANCES(3, 3, 0); +} Reactor_R2; + +LF_REACTOR_CTOR_SIGNATURE_WITH_PARAMETERS(Reactor_R2 , InputExternalCtorArgs *_in_external , int expected ); + +#endif // LFC_GEN_R2_H diff --git a/examples/posix/static/_lf_preamble.h b/examples/posix/static/_lf_preamble.h new file mode 100644 index 000000000..63c1ac663 --- /dev/null +++ b/examples/posix/static/_lf_preamble.h @@ -0,0 +1,9 @@ +#ifndef LF_GEN_CONNECTIONS_PREAMBLE_H +#define LF_GEN_CONNECTIONS_PREAMBLE_H + +#include "reactor-uc/reactor-uc.h" +// Include the preambles from imported files +#include "_lf_preamble.h" + + +#endif // LF_GEN_CONNECTIONS_PREAMBLE_H diff --git a/examples/posix/static/reactor-uc b/examples/posix/static/reactor-uc new file mode 120000 index 000000000..1b20c9fb8 --- /dev/null +++ b/examples/posix/static/reactor-uc @@ -0,0 +1 @@ +../../../ \ No newline at end of file diff --git a/examples/posix/static/static_schedule.c b/examples/posix/static/static_schedule.c new file mode 100644 index 000000000..3b9fcb599 --- /dev/null +++ b/examples/posix/static/static_schedule.c @@ -0,0 +1,38 @@ +#include "reactor-uc/macros.h" +#include "Connections.h" +#include "reactor-uc/schedulers/static/scheduler.h" +#include "reactor-uc/schedulers/static/instructions.h" + + +Reactor_Connections main_reactor; +Environment env; +Environment *_lf_environment = &env; +void lf_exit(void) { Environment_free(&env); } +void lf_start() { + Environment_ctor(&env, (Reactor *)&main_reactor); + Reactor_Connections_ctor(&main_reactor, NULL, &env); + env.scheduler->duration = NEVER; + env.scheduler->keep_alive = false; + env.fast_mode = false; + env.assemble(&env); +} +void lf_execute() { + env.start(&env); + lf_exit(); +} + +int main() { + lf_start(); + StaticSchedulerState* state = &((StaticScheduler*)_lf_environment->scheduler)->state; + ReactorTagPair reactor_tags[2] = {{&main_reactor, 0}, {&main_reactor.r1, 42}}; + inst_t schedule_0[] = { + {.func=execute_inst_ADDI, .opcode=ADDI, .op1.reg=(reg_t*)&(state->timeout), .op2.reg=(reg_t*)&(state->start_time), .op3.imm=0LL}, + {.func=execute_inst_EXE, .opcode=EXE, .op1.reg=(reg_t*)(main_reactor.r1->reaction0.super.body), .op2.reg=(reg_t*)&(main_reactor.r1->reaction0.super), .op3.imm=0LL}, + {.func=execute_inst_EXE, .opcode=EXE, .op1.reg=(reg_t*)(main_reactor.r1->reaction1.super.body), .op2.reg=(reg_t*)&(main_reactor.r1->reaction1.super), .op3.imm=0LL}, + {.func=execute_inst_STP, .opcode=STP}, + }; + ((StaticScheduler*)_lf_environment->scheduler)->static_schedule = schedule_0; + ((StaticScheduler*)_lf_environment->scheduler)->reactor_tags = reactor_tags; + ((StaticScheduler*)_lf_environment->scheduler)->reactor_tags_size = 2; + lf_execute(); +} \ No newline at end of file diff --git a/include/reactor-uc/environment.h b/include/reactor-uc/environment.h index ee0e542b7..e6ddc26f4 100644 --- a/include/reactor-uc/environment.h +++ b/include/reactor-uc/environment.h @@ -10,6 +10,7 @@ typedef struct Environment Environment; extern Environment *_lf_environment; // NOLINT +typedef struct Scheduler Scheduler; struct Environment { Reactor *main; // The top-level reactor of the program. diff --git a/include/reactor-uc/event.h b/include/reactor-uc/event.h index 6b98669b8..b6993b0ea 100644 --- a/include/reactor-uc/event.h +++ b/include/reactor-uc/event.h @@ -5,7 +5,8 @@ #include "reactor-uc/tag.h" #include -#define EVENT_INIT(Tag, Trigger, Payload) {.tag = Tag, .trigger = Trigger, .payload = Payload} +#define EVENT_INIT(Tag, Trigger, Payload) \ + { .tag = Tag, .trigger = Trigger, .payload = Payload } typedef struct Trigger Trigger; diff --git a/include/reactor-uc/reactor-uc.h b/include/reactor-uc/reactor-uc.h index 041e8cd19..c0e8cb6bd 100644 --- a/include/reactor-uc/reactor-uc.h +++ b/include/reactor-uc/reactor-uc.h @@ -1,14 +1,6 @@ #ifndef REACTOR_UC_REACTOR_UC_H #define REACTOR_UC_REACTOR_UC_H -#if defined(SCHEDULER_DYNAMIC) -#include "./schedulers/dynamic/scheduler.h" -#elif defined(SCHEDULER_STATIC) -#include "schedulers/static/scheduler.h" -#include "schedulers/static/instructions.h" -#else -#endif - #include "reactor-uc/action.h" #include "reactor-uc/builtin_triggers.h" #include "reactor-uc/connection.h" @@ -23,6 +15,7 @@ #include "reactor-uc/util.h" #include "reactor-uc/reaction.h" #include "reactor-uc/reactor.h" +#include "reactor-uc/scheduler.h" #include "reactor-uc/tag.h" #include "reactor-uc/timer.h" #include "reactor-uc/trigger.h" diff --git a/include/reactor-uc/scheduler.h b/include/reactor-uc/scheduler.h index f85defc0d..10343f543 100644 --- a/include/reactor-uc/scheduler.h +++ b/include/reactor-uc/scheduler.h @@ -1,11 +1,13 @@ #ifndef REACTOR_UC_SCHEDULER_H #define REACTOR_UC_SCHEDULER_H +#include "reactor-uc/reactor-uc.h" + typedef struct Scheduler Scheduler; typedef struct Environment Environment; struct Scheduler { - interval_t start_time; + instant_t start_time; interval_t duration; // The duration after which the program should stop. bool keep_alive; // Whether the program should keep running even if there are no more events to process. bool leader; // Whether this scheduler is the leader in a federated program and selects the start tag. @@ -52,4 +54,11 @@ struct Scheduler { Scheduler *Scheduler_new(Environment *env); -#endif \ No newline at end of file +#if defined(SCHEDULER_DYNAMIC) +#include "schedulers/dynamic/scheduler.h" +#elif defined(SCHEDULER_STATIC) +#include "schedulers/static/scheduler.h" +#else +#error "Scheduler not supported" +#endif +#endif // REACTOR_UC_SCHEDULER_H \ No newline at end of file diff --git a/include/reactor-uc/schedulers/static/circular_buffer.h b/include/reactor-uc/schedulers/static/circular_buffer.h new file mode 100644 index 000000000..22aa4f9d7 --- /dev/null +++ b/include/reactor-uc/schedulers/static/circular_buffer.h @@ -0,0 +1,26 @@ +#ifndef CircularBuffer_H +#define CircularBuffer_H + +#include +#include + +typedef struct CircularBuffer +{ + void *buffer; // data buffer + void *buffer_end; // end of data buffer + size_t capacity; // maximum number of items in the buffer + size_t count; // number of items in the buffer + size_t sz; // size of each item in the buffer + void *head; // pointer to head + void *tail; // pointer to tail +} CircularBuffer; + +void cb_init(CircularBuffer *cb, size_t capacity, size_t sz); +void cb_free(CircularBuffer *cb); +void cb_push_back(CircularBuffer *cb, const void *item); +void cb_pop_front(CircularBuffer *cb, void *item); +void cb_remove_front(CircularBuffer *cb); +void* cb_peek(CircularBuffer *cb); +void cb_dump_events(CircularBuffer *cb); + +#endif \ No newline at end of file diff --git a/include/reactor-uc/schedulers/static/instructions.h b/include/reactor-uc/schedulers/static/instructions.h index b0e7953b1..a717c2955 100644 --- a/include/reactor-uc/schedulers/static/instructions.h +++ b/include/reactor-uc/schedulers/static/instructions.h @@ -1,20 +1,69 @@ #ifndef SCHEDULER_STATIC_FUNCTION_H #define SCHEDULER_STATIC_FUNCTION_H -#include "scheduler_instructions.h" #include "reactor-uc/reaction.h" #include "reactor-uc/platform.h" +typedef enum { + ADD, + ADDI, + ADV, + ADVI, + BEQ, + BGE, + BLT, + BNE, + DU, + EXE, + JAL, + JALR, + STP, + WLT, + WU, +} opcode_t; + /** - * @brief Function type with a void* argument. To make this type represent a - * generic function, one can write a wrapper function around the target function - * and use the first argument as a pointer to a struct of input arguments - * and return values. + * @brief Convenient typedefs for the data types used by the C implementation of + * PRET VM. A register is 64bits and an immediate is 64bits. This avoids any + * issue with time and overflow. Arguably it is worth it even for smaller + * platforms. + * */ +typedef volatile uint64_t reg_t; +typedef uint64_t imm_t; -#ifndef LF_PRINT_DEBUG -#define LF_PRINT_DEBUG(A, ...) {}; -#endif +/** + * @brief An union representing a single operand for the PRET VM. A union + * means that we have one piece of memory, which is big enough to fit either + * one of the two members of the union. + * + */ +typedef union { + reg_t *reg; + imm_t imm; +} operand_t; + +/** + * @brief Virtual instruction function pointer + */ +typedef void (*function_virtual_instruction_t)(Platform *platform, size_t worker_number, operand_t op1, operand_t op2, + operand_t op3, bool debug, size_t *program_counter, + Reaction **returned_reaction, bool *exit_loop); + +/** + * @brief This struct represents a PRET VM instruction for C platforms. + * There is an opcode and three operands. The operands are unions so they + * can be either a pointer or an immediate + * + */ +typedef struct inst_t { + function_virtual_instruction_t func; + opcode_t opcode; + operand_t op1; + operand_t op2; + operand_t op3; + bool debug; +} inst_t; /** * @brief Wrapper function for peeking a priority queue. diff --git a/include/reactor-uc/schedulers/static/scheduler.h b/include/reactor-uc/schedulers/static/scheduler.h index 4e5b91bad..7f86c8d6f 100644 --- a/include/reactor-uc/schedulers/static/scheduler.h +++ b/include/reactor-uc/schedulers/static/scheduler.h @@ -7,17 +7,57 @@ #include "reactor-uc/error.h" #include "reactor-uc/queues.h" #include "reactor-uc/scheduler.h" +#include "reactor-uc/schedulers/static/instructions.h" +#include "reactor-uc/schedulers/static/circular_buffer.h" typedef struct StaticScheduler StaticScheduler; +typedef struct StaticSchedulerState StaticSchedulerState; +typedef struct ReactorTagPair ReactorTagPair; +typedef struct TriggerBuffer TriggerBuffer; typedef struct Environment Environment; +/** + * A struct storing registers and other info related to the static scheduler. + */ +struct StaticSchedulerState { + size_t pc; + instant_t start_time; // From application + uint64_t timeout; // From application + size_t num_progress_index; + reg_t time_offset; + reg_t offset_inc; + uint64_t zero; + uint64_t one; + // FIXME: Separate worker state from global state in another struct. + uint64_t progress_index; + reg_t return_addr; + reg_t binary_sema; + reg_t temp0; + reg_t temp1; +}; + +struct ReactorTagPair { + Reactor *reactor; + tag_t tag; +}; + +struct TriggerBuffer { + Event staged_event; // Event popped from the circular buffer to be processed at this tag + Trigger *trigger; // The trigger associated with the circular buffer + CircularBuffer buffer; // The circular buffer for events associated with the trigger +}; + struct StaticScheduler { - Scheduler *super; + Scheduler super; Environment *env; - const inst_t **static_schedule; - size_t *pc; + const inst_t *static_schedule; + StaticSchedulerState state; + ReactorTagPair *reactor_tags; + size_t reactor_tags_size; + TriggerBuffer *trigger_buffers; + size_t trigger_buffers_size; }; -void StaticScheduler_ctor(StaticScheduler *self, Environment *env, const inst_t **static_schedule); +void StaticScheduler_ctor(StaticScheduler *self, Environment *env); #endif // STATIC_SCHEDULER_H diff --git a/include/reactor-uc/schedulers/static/scheduler_instructions.h b/include/reactor-uc/schedulers/static/scheduler_instructions.h deleted file mode 100644 index e4dd34092..000000000 --- a/include/reactor-uc/schedulers/static/scheduler_instructions.h +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @author Shaokai Lin - * @brief Format of the instruction set - */ -#ifndef SCHEDULER_INSTRUCTIONS_H -#define SCHEDULER_INSTRUCTIONS_H - -#include "reactor-uc/reaction.h" -#include "reactor-uc/platform.h" - -typedef enum { - ADD, - ADDI, - ADV, - ADVI, - BEQ, - BGE, - BLT, - BNE, - DU, - EXE, - JAL, - JALR, - STP, - WLT, - WU, -} opcode_t; - -/** - * @brief Convenient typedefs for the data types used by the C implementation of - * PRET VM. A register is 64bits and an immediate is 64bits. This avoids any - * issue with time and overflow. Arguably it is worth it even for smaller - * platforms. - * - */ -typedef volatile uint64_t reg_t; -typedef uint64_t imm_t; - -/** - * @brief An union representing a single operand for the PRET VM. A union - * means that we have one piece of memory, which is big enough to fit either - * one of the two members of the union. - * - */ -typedef union { - reg_t *reg; - imm_t imm; -} operand_t; - -/** - * @brief Virtual instruction function pointer - */ -typedef void (*function_virtual_instruction_t)(Platform *platform, size_t worker_number, operand_t op1, operand_t op2, - operand_t op3, bool debug, size_t *program_counter, - Reaction **returned_reaction, bool *exit_loop); - -/** - * @brief This struct represents a PRET VM instruction for C platforms. - * There is an opcode and three operands. The operands are unions so they - * can be either a pointer or an immediate - * - */ -typedef struct inst_t { - function_virtual_instruction_t func; - opcode_t opcode; - operand_t op1; - operand_t op2; - operand_t op3; - bool debug; -} inst_t; - -#endif \ No newline at end of file diff --git a/include/reactor-uc/tag.h b/include/reactor-uc/tag.h index da5f2065a..d5d698589 100644 --- a/include/reactor-uc/tag.h +++ b/include/reactor-uc/tag.h @@ -38,12 +38,15 @@ #define NEVER_TAG \ (tag_t) { .time = NEVER, .microstep = NEVER_MICROSTEP } // Need a separate initializer expression to comply with some C compilers -#define NEVER_TAG_INITIALIZER {NEVER, NEVER_MICROSTEP} +#define NEVER_TAG_INITIALIZER \ + { NEVER, NEVER_MICROSTEP } #define FOREVER_TAG \ (tag_t) { .time = FOREVER, .microstep = FOREVER_MICROSTEP } // Need a separate initializer expression to comply with some C compilers -#define FOREVER_TAG_INITIALIZER {FOREVER, FOREVER_MICROSTEP} -#define ZERO_TAG (tag_t){.time = 0LL, .microstep = 0u} +#define FOREVER_TAG_INITIALIZER \ + { FOREVER, FOREVER_MICROSTEP } +#define ZERO_TAG \ + (tag_t) { .time = 0LL, .microstep = 0u } // Returns true if timeout has elapsed. #define CHECK_TIMEOUT(start, duration) (lf_time_physical() > ((start) + (duration))) diff --git a/lfc/core/src/main/java/org/lflang/AttributeUtils.java b/lfc/core/src/main/java/org/lflang/AttributeUtils.java index eb5aa0d37..1abecd76a 100644 --- a/lfc/core/src/main/java/org/lflang/AttributeUtils.java +++ b/lfc/core/src/main/java/org/lflang/AttributeUtils.java @@ -152,6 +152,33 @@ public static String getAttributeValue(EObject node, String attrName) { return value; } + /** + * Return the first argument, which has the type Time, specified for the attribute. + * + *

This should be used if the attribute is expected to have a single argument whose type is + * Time. If there is no argument, null is returned. + */ + public static Time getFirstArgumentTime(Attribute attr) { + if (attr == null || attr.getAttrParms().isEmpty()) { + return null; + } + return attr.getAttrParms().get(0).getTime(); + } + + /** + * Search for an attribute with the given name on the given AST node and return its first argument + * as Time. + * + *

This should only be used on attributes that are expected to have a single argument with type + * Time. + * + *

Returns null if the attribute is not found or if it does not have any arguments. + */ + public static Time getAttributeTime(EObject node, String attrName) { + final var attr = findAttributeByName(node, attrName); + return getFirstArgumentTime(attr); + } + /** * Search for an attribute with the given name on the given AST node and return its first argument * as a String. @@ -251,6 +278,15 @@ public static boolean hasCBody(Reaction reaction) { return findAttributeByName(reaction, "_c_body") != null; } + /** Return a time value that represents the WCET of a reaction. */ + public static TimeValue getWCET(Reaction reaction) { + Time wcet = getAttributeTime(reaction, "wcet"); + if (wcet == null) return TimeValue.MAX_VALUE; + int value = wcet.getInterval(); + TimeUnit unit = TimeUnit.fromName(wcet.getUnit()); + return new TimeValue(value, unit); + } + /** Return the declared label of the node, as given by the @label annotation. */ public static String getLabel(EObject node) { return getAttributeValue(node, "label"); diff --git a/lfc/core/src/main/java/org/lflang/LinguaFranca.xtext b/lfc/core/src/main/java/org/lflang/LinguaFranca.xtext index 8565b3fa5..82228ac6a 100644 --- a/lfc/core/src/main/java/org/lflang/LinguaFranca.xtext +++ b/lfc/core/src/main/java/org/lflang/LinguaFranca.xtext @@ -258,7 +258,7 @@ Attribute: ; AttrParm: - (name=ID '=')? value=Literal; + (name=ID '=')? (value=Literal | time=Time); /////////// For target parameters diff --git a/lfc/core/src/main/java/org/lflang/TimeTag.java b/lfc/core/src/main/java/org/lflang/TimeTag.java new file mode 100644 index 000000000..086adf790 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/TimeTag.java @@ -0,0 +1,58 @@ +package org.lflang; + +/** + * Class representing a logical time tag, which is a pair that consists of a timestamp and a + * microstep. + */ +public class TimeTag implements Comparable { + + public static final TimeTag ZERO = new TimeTag(TimeValue.ZERO, 0L); + public static final TimeTag FOREVER = new TimeTag(TimeValue.MAX_VALUE, Long.MAX_VALUE); + + public final TimeValue time; + public final Long microstep; + + /** Constructor */ + public TimeTag(TimeValue time, Long microstep) { + this.time = time; + this.microstep = microstep; + } + + /** Copy constructor */ + public TimeTag(TimeTag that) { + this.time = that.time; + this.microstep = that.microstep; + } + + /** + * Whether this time tag represents FOREVER, which is interpreted as the maximum TimeValue. + * + * @return True if the tag is FOREVER, false otherwise. + */ + public boolean isForever() { + return time.equals(TimeValue.MAX_VALUE); + } + + /** + * When comparing two time tags, first compare their time fields. If they are equal, compare their + * microsteps. + */ + @Override + public int compareTo(TimeTag t) { + if (this.time.compareTo(t.time) < 0) return -1; + else if (this.time.compareTo(t.time) > 0) return 1; + else return this.microstep.compareTo(t.microstep); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof TimeTag t && this.compareTo(t) == 0) return true; + return false; + } + + @Override + public String toString() { + return "( " + this.time + ", " + this.microstep + " )"; + } +} diff --git a/lfc/core/src/main/java/org/lflang/analyses/statespace/Event.java b/lfc/core/src/main/java/org/lflang/analyses/statespace/Event.java new file mode 100644 index 000000000..9774686b5 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/analyses/statespace/Event.java @@ -0,0 +1,52 @@ +package org.lflang.analyses.statespace; + +import org.lflang.TimeTag; +import org.lflang.generator.TriggerInstance; + +/** A class representing a tagged signal, for analytical purposes */ +public class Event implements Comparable { + + private final TriggerInstance trigger; + private TimeTag tag; + + public Event(TriggerInstance trigger, TimeTag tag) { + this.trigger = trigger; + this.tag = tag; + } + + /** + * Compare two events first by tags and, if tags are equal, by trigger names in lexical order. + * This is useful for enforcing a unique order of events in a priority queue of Event instances. + */ + @Override + public int compareTo(Event e) { + // Compare tags first. + int ret = this.tag.compareTo(e.getTag()); + // If tags match, compare trigger names. + if (ret == 0) ret = this.trigger.getFullName().compareTo(e.trigger.getFullName()); + return ret; + } + + /** This method checks if two events have the same triggers. */ + public boolean hasSameTriggers(Object o) { + if (o == null) return false; + if (o instanceof Event) { + Event e = (Event) o; + if (this.trigger.equals(e.trigger)) return true; + } + return false; + } + + @Override + public String toString() { + return "(" + trigger.getFullName() + ", " + tag + ")"; + } + + public TimeTag getTag() { + return tag; + } + + public TriggerInstance getTrigger() { + return trigger; + } +} diff --git a/lfc/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java b/lfc/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java new file mode 100644 index 000000000..f4530e613 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/analyses/statespace/EventQueue.java @@ -0,0 +1,18 @@ +package org.lflang.analyses.statespace; + +import java.util.PriorityQueue; + +/** An event queue for analyzing the logical behavior of an LF program */ +public class EventQueue extends PriorityQueue { + + /** + * Modify the original add() by enforcing uniqueness. There cannot be duplicate events in the + * event queue. + */ + @Override + public boolean add(Event e) { + if (this.contains(e)) return false; + super.add(e); + return true; + } +} diff --git a/lfc/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java new file mode 100644 index 000000000..ef906a3c6 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateInfo.java @@ -0,0 +1,30 @@ +package org.lflang.analyses.statespace; + +import java.util.ArrayList; +import java.util.HashMap; +import org.lflang.TimeTag; + +/** A class that represents information in a step in a counterexample trace */ +public class StateInfo { + + public ArrayList reactions = new ArrayList<>(); + public TimeTag tag; + public HashMap variables = new HashMap<>(); + public HashMap triggers = new HashMap<>(); + public HashMap scheduled = new HashMap<>(); + public HashMap payloads = new HashMap<>(); + + public void display() { + System.out.println( + String.join( + "\n", + "/**************************/", + "reactions: " + reactions, + "tag: " + tag, + "variables: " + variables, + "triggers: " + triggers, + "scheduled: " + scheduled, + "payloads: " + payloads, + "/**************************/")); + } +} diff --git a/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java new file mode 100644 index 000000000..398bd1ed5 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceDiagram.java @@ -0,0 +1,256 @@ +package org.lflang.analyses.statespace; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.lflang.TimeValue; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.ReactionInstance; +import org.lflang.graph.DirectedGraph; +import org.lflang.pretvm.ExecutionPhase; + +/** + * A directed graph representing the state space of an LF program. + * + * @author Shaokai J. Lin + */ +public class StateSpaceDiagram extends DirectedGraph { + + /** The first node of the state space diagram. */ + public StateSpaceNode head; + + /** The last node of the state space diagram. */ + public StateSpaceNode tail; + + /** + * The previously encountered node which the tail node goes back to, i.e. the location where the + * back loop happens. + */ + public StateSpaceNode loopNode; + + /** + * Store the state when the loop node is reached for the 2nd time. This is used to calculate the + * elapsed logical time on the back loop edge. + */ + public StateSpaceNode loopNodeNext; + + /** + * The logical time elapsed for each loop iteration. With the assumption of "logical time = + * physical time," this is also the hyperperiod in physical time. + */ + public long hyperperiod; + + /** The exploration phase in which this diagram is generated */ + public ExecutionPhase phase; + + /** A dot file that represents the diagram */ + private CodeBuilder dot; + + /** A flag that indicates whether we want the dot to be compact */ + private final boolean compactDot = false; + + /** Before adding the node, assign it an index. */ + @Override + public void addNode(StateSpaceNode node) { + node.setIndex(this.nodeCount()); + super.addNode(node); + } + + /** Get the immediately downstream node. */ + public StateSpaceNode getDownstreamNode(StateSpaceNode node) { + Set downstream = this.getDownstreamAdjacentNodes(node); + if (downstream == null || downstream.size() == 0) return null; + return (StateSpaceNode) downstream.toArray()[0]; + } + + /** Pretty print the diagram. */ + public void display() { + System.out.println("*************************************************"); + System.out.println("* Pretty printing worst-case state space diagram:"); + TimeValue time; + StateSpaceNode node = this.head; + if (node == null) { + System.out.println("* EMPTY"); + System.out.println("*************************************************"); + return; + } + while (node != this.tail) { + System.out.print("* State " + node.getIndex() + ": "); + node.display(); + + // Store the tag of the prior step. + time = node.getTag().time; + + // Assume a unique next state. + node = getDownstreamNode(node); + + // Compute time difference + if (node != null) { + TimeValue tsDiff = + TimeValue.fromNanoSeconds(node.getTag().time.toNanoSeconds() - time.toNanoSeconds()); + System.out.println("* => Advance time by " + tsDiff); + } + } + + // Print tail node + System.out.print("* (Tail) state " + node.getIndex() + ": "); + node.display(); + + if (this.loopNode != null) { + // Compute time difference + TimeValue tsDiff = + TimeValue.fromNanoSeconds( + loopNodeNext.getTag().time.toNanoSeconds() - tail.getTag().time.toNanoSeconds()); + System.out.println("* => Advance time by " + tsDiff); + + System.out.println("* Goes back to loop node: state " + this.loopNode.getIndex()); + System.out.print("* Loop node reached 2nd time: "); + this.loopNodeNext.display(); + } + System.out.println("*************************************************"); + } + + /** + * Generate a dot file from the state space diagram. + * + * @return a CodeBuilder with the generated code + */ + public CodeBuilder generateDot() { + if (dot == null) { + dot = new CodeBuilder(); + dot.pr("digraph G {"); + dot.indent(); + if (this.isCyclic()) { + dot.pr("layout=circo;"); + } + dot.pr("rankdir=LR;"); + if (this.compactDot) { + dot.pr("mindist=1.5;"); + dot.pr("overlap=false"); + dot.pr("node [shape=Mrecord fontsize=40]"); + dot.pr("edge [fontsize=40 penwidth=3 arrowsize=2]"); + } else { + dot.pr("node [shape=Mrecord]"); + } + // Generate a node for each state. + if (this.compactDot) { + for (StateSpaceNode n : this.nodes()) { + dot.pr( + "S" + + n.getIndex() + + " [" + + "label = \" {" + + "S" + + n.getIndex() + + " | " + + n.getReactionsInvoked().size() + + " | " + + n.getEventQcopy().size() + + "}" + + " | " + + n.getTag() + + "\"" + + "]"); + } + } else { + for (StateSpaceNode n : this.nodes()) { + List reactions = + n.getReactionsInvoked().stream() + .map(ReactionInstance::getFullName) + .collect(Collectors.toList()); + String reactionsStr = String.join("\\n", reactions); + List events = + n.getEventQcopy().stream().map(Event::toString).collect(Collectors.toList()); + String eventsStr = String.join("\\n", events); + dot.pr( + "S" + + n.getIndex() + + " [" + + "label = \"" + + "S" + + n.getIndex() + + " | " + + n.getTag() + + " | " + + "Reactions invoked:\\n" + + reactionsStr + + " | " + + "Pending events:\\n" + + eventsStr + + "\"" + + "]"); + } + } + + StateSpaceNode current = this.head; + StateSpaceNode next = getDownstreamNode(this.head); + while (current != null && next != null && current != this.tail) { + TimeValue tsDiff = + TimeValue.fromNanoSeconds( + next.getTag().time.toNanoSeconds() - current.getTag().time.toNanoSeconds()); + dot.pr( + "S" + + current.getIndex() + + " -> " + + "S" + + next.getIndex() + + " [label = " + + "\"" + + "+" + + tsDiff + + "\"" + + "]"); + current = next; + next = getDownstreamNode(next); + } + + if (loopNode != null) { + TimeValue tsDiff = + TimeValue.fromNanoSeconds( + loopNodeNext.getTag().time.toNanoSeconds() - tail.getTag().time.toNanoSeconds()); + TimeValue period = TimeValue.fromNanoSeconds(hyperperiod); + dot.pr( + "S" + + current.getIndex() + + " -> " + + "S" + + next.getIndex() + + " [label = " + + "\"" + + "+" + + tsDiff + + " -" + + period + + "\"" + + " weight = 0 " + + "]"); + } + + dot.unindent(); + dot.pr("}"); + } + return this.dot; + } + + public void generateDotFile(Path dir, String filename) { + try { + Path path = dir.resolve(filename); + CodeBuilder dot = generateDot(); + dot.writeToFile(path.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** Check if the diagram is periodic by checking if the loop node is set. */ + public boolean isCyclic() { + return loopNode != null; + } + + /** Check if the diagram is empty. */ + public boolean isEmpty() { + return (head == null); + } +} diff --git a/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java new file mode 100644 index 000000000..f0a1c9579 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceExplorer.java @@ -0,0 +1,582 @@ +package org.lflang.analyses.statespace; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.lflang.TimeTag; +import org.lflang.TimeValue; +import org.lflang.ast.ASTUtils; +import org.lflang.generator.ActionInstance; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.RuntimeRange; +import org.lflang.generator.SendRange; +import org.lflang.generator.TimerInstance; +import org.lflang.generator.TriggerInstance; +import org.lflang.lf.Expression; +import org.lflang.lf.Variable; +import org.lflang.pretvm.ExecutionPhase; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.TimeOutProperty; + +/** + * (EXPERIMENTAL) Explores the state space of an LF program. Use with caution since this is + * experimental code. + * + * @author Shaokai J. Lin + */ +public class StateSpaceExplorer { + + /** + * Explore the state space and populate the state space diagram until the specified horizon (i.e. + * the end tag) is reached OR until the event queue is empty. + * + *

As an optimization, the algorithm tries to find a loop in the state space during + * exploration. If a loop is found (i.e. a previously encountered state is reached again) during + * exploration, the function returns early. + * + *

If the phase is INIT_AND_PERIODIC, the explorer starts with startup triggers and timers' + * initial firings. If the phase is SHUTDOWN_*, the explorer starts with shutdown triggers. + * + *

TODOs: 1. Handle action with 0 minimum delay. 2. Handle hierarchical reactors. + * + *

Note: This is experimental code. Use with caution. + */ + public static StateSpaceDiagram explore( + ReactorInstance main, TimeTag horizon, ExecutionPhase phase, TargetConfig targetConfig) { + if (phase != ExecutionPhase.INIT_AND_PERIODIC + && phase != ExecutionPhase.SHUTDOWN_TIMEOUT + && phase != ExecutionPhase.SHUTDOWN_STARVATION) + throw new RuntimeException("Unsupported phase detected in the explorer."); + + // Variable initilizations + StateSpaceDiagram diagram = new StateSpaceDiagram(); + diagram.phase = phase; + EventQueue eventQ = new EventQueue(); + TimeTag previousTag = null; // TimeTag in the previous loop ITERATION + TimeTag currentTag = null; // TimeTag in the current loop ITERATION + StateSpaceNode currentNode = null; + StateSpaceNode previousNode = null; + HashMap uniqueNodes = new HashMap<>(); + boolean stop = true; + + // Add initial events to the event queue. + eventQ.addAll(addInitialEvents(main, phase, targetConfig)); + + // Check if we should stop already. + if (eventQ.size() > 0) { + stop = false; + currentTag = eventQ.peek().getTag(); + } + + // A list of reactions invoked at the current logical tag + Set reactionsInvoked; + // A temporary list of reactions processed in the current LOOP ITERATION + Set reactionsTemp; + + // Iterate until stop conditions are met. + while (!stop) { + + // Pop the events from the earliest tag off the event queue. + List currentEvents = popCurrentEvents(eventQ, currentTag); + + // Collect all the reactions invoked in this current LOOP ITERATION + // triggered by the earliest events. + reactionsTemp = getReactionsTriggeredByCurrentEvents(currentEvents); + + // For each reaction invoked, compute the new events produced. + List newEvents = createNewEvents(currentEvents, reactionsTemp, currentTag); + // FIXME: Need to make sure that addAll() is using the overridden version + // that makes sure new events added are unique. By default, this should be + // the case. + eventQ.addAll(newEvents); + + // We are at the first iteration. + // Initialize currentNode. + if (previousTag == null) { + //// Now we are done with the node at the previous tag, + //// work on the new node at the current timestamp. + // Copy the reactions in reactionsTemp. + reactionsInvoked = new HashSet<>(reactionsTemp); + + // Create a new state in the SSD for the current tag, + // add the reactions triggered to the state, + // and add a snapshot of the event queue (with new events + // generated by reaction invocations in the curren tag) + // to the state. + + // Initialize currentNode. + currentNode = + new StateSpaceNode( + currentTag, // Current tag + reactionsInvoked, // Reactions invoked at this tag + new ArrayList<>(eventQ) // A snapshot of the event queue + ); + } + // When we advance to a new TIMESTAMP (not a new tag), + // create a new node in the state space diagram + // for everything processed in the previous timestamp. + // This makes sure that the granularity of nodes is + // at the timestamp-level, so that we don't have to + // worry about microsteps. + else if (previousTag != null && currentTag.compareTo(previousTag) > 0) { + // Check if we are in the SHUTDOWN_TIMEOUT mode, + // if so, stop the loop immediately, because TIMEOUT is the last tag. + if (phase == ExecutionPhase.SHUTDOWN_TIMEOUT) { + // Make the hyperperiod for the SHUTDOWN_TIMEOUT phase Long.MAX_VALUE, + // so that this is guaranteed to be feasibile from the perspective of + // the EGS scheduler. + diagram.hyperperiod = Long.MAX_VALUE; + diagram.loopNode = null; // The SHUTDOWN_TIMEOUT phase is acyclic. + break; + } + + // Whenever we finish a tag, check for loops fist. + // If currentNode matches an existing node in uniqueNodes, + // duplicate is set to the existing node. + StateSpaceNode duplicate; + if ((duplicate = uniqueNodes.put(currentNode.hash(), currentNode)) != null) { + + // Mark the loop in the diagram. + diagram.loopNode = duplicate; + diagram.loopNodeNext = currentNode; + diagram.tail = previousNode; + // Loop period is the time difference between the 1st time + // the node is reached and the 2nd time the node is reached. + diagram.hyperperiod = + diagram.loopNodeNext.getTag().time.toNanoSeconds() + - diagram.loopNode.getTag().time.toNanoSeconds(); + diagram.addEdge(diagram.loopNode, diagram.tail); + return diagram; // Exit the while loop early. + } + + // Now we are at a new tag, and a loop is not found, + // add the node to the state space diagram. + // Adding a node to the graph once it is finalized + // because this makes checking duplicate nodes easier. + // We don't have to remove a node from the graph. + diagram.addNode(currentNode); + diagram.tail = currentNode; // Update the current tail. + + // If the head is not empty, add an edge from the previous state + // to the next state. Otherwise initialize the head to the new node. + if (previousNode != null) { + if (previousNode != currentNode) diagram.addEdge(currentNode, previousNode); + } else diagram.head = currentNode; // Initialize the head. + + //// Now we are done with the node at the previous tag, + //// work on the new node at the current timestamp. + // Copy the reactions in reactionsTemp. + reactionsInvoked = new HashSet<>(reactionsTemp); + + // Create a new state in the SSD for the current tag, + // add the reactions triggered to the state, + // and add a snapshot of the event queue (with new events + // generated by reaction invocations in the curren tag) + // to the state. + StateSpaceNode node = + new StateSpaceNode( + currentTag, // Current tag + reactionsInvoked, // Reactions invoked at this tag + new ArrayList<>(eventQ) // A snapshot of the event queue + ); + + // Update the previous node. + previousNode = currentNode; + // Update the current node to the new (potentially incomplete) node. + currentNode = node; + } + // Timestamp does not advance because we are processing + // connections with zero delay. + else if (previousTag != null && currentTag.equals(previousTag)) { + // Add reactions explored in the current loop iteration + // to the existing state space node. + currentNode.getReactionsInvoked().addAll(reactionsTemp); + // Update the eventQ snapshot. + currentNode.setEventQcopy(new ArrayList<>(eventQ)); + } else { + throw new AssertionError("Unreachable"); + } + + // Update the current tag for the next iteration. + if (eventQ.size() > 0) { + previousTag = currentTag; + currentTag = eventQ.peek().getTag(); + } + + // Stop if: + // 1. the event queue is empty, or + // 2. the horizon is reached. + if (eventQ.size() == 0) { + stop = true; + } + // FIXME: If horizon is forever, explore() might not terminate. + // How to set a reasonable upperbound? + else if (!horizon.isForever() && currentTag.compareTo(horizon) > 0) { + stop = true; + } + } + + // Check if the last current node is added to the graph yet. + // If not, add it now. + // This could happen when condition (previousTag == null) + // or (previousTag != null + // && currentTag.compareTo(previousTag) > 0) is true and then + // the simulation ends, leaving a new node dangling. + if (currentNode != null + && (previousNode == null || previousNode.getTag().compareTo(currentNode.getTag()) < 0)) { + diagram.addNode(currentNode); + diagram.tail = currentNode; // Update the current tail. + if (previousNode != null) { + diagram.addEdge(currentNode, previousNode); + } + } + + // At this point if we still don't have a head, + // then it means there is only one node in the diagram. + // Set the current node as the head. + if (diagram.head == null) diagram.head = currentNode; + + return diagram; + } + + ////////////////////////////////////////////////////// + ////////////////// Private Methods + + /** + * Return a (unordered) list of initial events to be given to the state space explorer based on a + * given phase. + * + * @param reactor The reactor wrt which initial events are inferred + * @param phase The phase for which initial events are inferred + * @return A list of initial events + */ + public static List addInitialEvents( + ReactorInstance reactor, ExecutionPhase phase, TargetConfig targetConfig) { + List events = new ArrayList<>(); + addInitialEventsRecursive(reactor, events, phase, targetConfig); + return events; + } + + /** + * Recursively add the first events to the event list for state space exploration. For the + * SHUTDOWN modes, it is okay to create shutdown events at (0,0) because this tag is a relative + * offset wrt to a phase (e.g., the shutdown phase), not the absolute tag at runtime. + */ + public static void addInitialEventsRecursive( + ReactorInstance reactor, + List events, + ExecutionPhase phase, + TargetConfig targetConfig) { + switch (phase) { + case INIT_AND_PERIODIC: + { + // Add the startup trigger, if exists. + var startup = reactor.getStartupTrigger(); + if (startup != null) events.add(new Event(startup, TimeTag.ZERO)); + + // Add the initial timer firings, if exist. + for (TimerInstance timer : reactor.timers) { + events.add( + new Event( + timer, + new TimeTag(TimeValue.fromNanoSeconds(timer.getOffset().toNanoSeconds()), 0L))); + } + break; + } + case SHUTDOWN_TIMEOUT: + { + // To get the state space of the instant at shutdown, + // we over-approximate by assuming all triggers are present at + // (timeout, 0). This could generate unnecessary instructions + // for reactions that are not meant to trigger at (timeout, 0), + // but they will be treated as NOPs at runtime. + + // Add the shutdown trigger, if exists. + var shutdown = reactor.getShutdownTrigger(); + if (shutdown != null) events.add(new Event(shutdown, TimeTag.ZERO)); + + // Check for timers that fire at (timeout, 0). + for (TimerInstance timer : reactor.timers) { + // If timeout = timer.offset + N * timer.period for some non-negative + // integer N, add a timer event. + Long offset = timer.getOffset().toNanoSeconds(); + Long period = timer.getPeriod().toNanoSeconds(); + Long timeout = targetConfig.get(TimeOutProperty.INSTANCE).toNanoSeconds(); + if (period != 0 && (timeout - offset) % period == 0) { + // The tag is set to (0,0) because, again, this is relative to the + // shutdown phase, not the actual absolute tag at runtime. + events.add(new Event(timer, TimeTag.ZERO)); + } + } + + // Assume all input ports and logical actions present. + // FIXME: Also physical action. Will add it later. + for (PortInstance input : reactor.inputs) { + events.add(new Event(input, TimeTag.ZERO)); + } + for (ActionInstance logicalAction : + reactor.actions.stream().filter(it -> !it.isPhysical()).toList()) { + events.add(new Event(logicalAction, TimeTag.ZERO)); + } + break; + } + case SHUTDOWN_STARVATION: + { + // Add the shutdown trigger, if exists. + var shutdown = reactor.getShutdownTrigger(); + if (shutdown != null) events.add(new Event(shutdown, TimeTag.ZERO)); + break; + } + default: + throw new RuntimeException("UNREACHABLE"); + } + + // Recursion + for (var child : reactor.children) { + addInitialEventsRecursive(child, events, phase, targetConfig); + } + } + + /** Pop events with currentTag off an eventQ */ + private static List popCurrentEvents(EventQueue eventQ, TimeTag currentTag) { + List currentEvents = new ArrayList<>(); + // FIXME: Use stream methods here? + while (eventQ.size() > 0 && eventQ.peek().getTag().compareTo(currentTag) == 0) { + Event e = eventQ.poll(); + currentEvents.add(e); + } + return currentEvents; + } + + /** + * Return a list of reaction instances triggered by a list of current events. The events must + * carry the same tag. Using a hash set here to make sure the reactions invoked are unique. + * Sometimes multiple events can trigger the same reaction, and we do not want to record duplicate + * reaction invocations. + */ + private static Set getReactionsTriggeredByCurrentEvents( + List currentEvents) { + Set reactions = new HashSet<>(); + for (Event e : currentEvents) { + Set dependentReactions = e.getTrigger().getDependentReactions(); + reactions.addAll(dependentReactions); + } + return reactions; + } + + /** + * Create a list of new events from reactions invoked at current tag. These new events should be + * able to trigger reactions, which means that the method needs to compute how events propagate + * downstream. + * + *

FIXME: This function does not handle port hierarchies, or the lack of them, yet. It should + * be updated with a new implementation that uses eventualDestinations() from PortInstance.java. + * But the challenge is to also get the delays. Perhaps eventualDestinations() should be extended + * to collect delays. + */ + private static List createNewEvents( + List currentEvents, Set reactions, TimeTag currentTag) { + + List newEvents = new ArrayList<>(); + + // If the event is a timer firing, enqueue the next firing. + for (Event e : currentEvents) { + if (e.getTrigger() instanceof TimerInstance) { + TimerInstance timer = (TimerInstance) e.getTrigger(); + newEvents.add( + new Event( + timer, + new TimeTag( + TimeValue.fromNanoSeconds( + e.getTag().time.toNanoSeconds() + timer.getPeriod().toNanoSeconds()), + 0L // A time advancement resets microstep to 0. + ))); + } + } + + // For each reaction invoked, compute the new events produced + // that can immediately trigger reactions. + for (ReactionInstance reaction : reactions) { + // Iterate over all the effects produced by this reaction. + // If the effect is a port, obtain the downstream port along + // a connection and enqueue a future event for that port. + // If the effect is an action, enqueue a future event for + // this action. + for (TriggerInstance effect : reaction.effects) { + // If the reaction writes to a port. + if (effect instanceof PortInstance) { + + for (SendRange senderRange : ((PortInstance) effect).getDependentPorts()) { + + for (RuntimeRange destinationRange : senderRange.destinations) { + PortInstance downstreamPort = destinationRange.instance; + + // Getting delay from connection + Expression delayExpr = senderRange.connection.getDelay(); + Long delay = ASTUtils.getDelay(delayExpr); + if (delay == null) delay = 0L; + + // Create and enqueue a new event. + Event e = + new Event( + downstreamPort, + new TimeTag( + TimeValue.fromNanoSeconds(currentTag.time.toNanoSeconds() + delay), 0L)); + newEvents.add(e); + } + } + } + // Ensure we only generate new events for LOGICAL actions. + else if (effect instanceof ActionInstance && !((ActionInstance) effect).isPhysical()) { + // Get the minimum delay of this action. + long min_delay = ((ActionInstance) effect).getMinDelay().toNanoSeconds(); + long microstep = 0; + if (min_delay == 0) { + microstep = currentTag.microstep + 1; + } + // Create and enqueue a new event. + Event e = + new Event( + effect, + new TimeTag( + TimeValue.fromNanoSeconds(currentTag.time.toNanoSeconds() + min_delay), + microstep)); + newEvents.add(e); + } + } + } + return newEvents; + } + + /** + * Generate a list of state space fragments for an LF program. This function calls + * generateStateSpaceDiagram() multiple times to capture the full behavior of the LF + * program. + */ + public static List generateStateSpaceDiagrams( + ReactorInstance reactor, TargetConfig targetConfig, Path graphDir) { + + // Initialize variables + List SSDs; + + /* Initialization and Periodic phase */ + + // Generate a state space diagram for the initialization and periodic phase + // of an LF program. + StateSpaceDiagram stateSpaceInitAndPeriodic = + explore(reactor, TimeTag.FOREVER, ExecutionPhase.INIT_AND_PERIODIC, targetConfig); + stateSpaceInitAndPeriodic.generateDotFile( + graphDir, "state_space_" + ExecutionPhase.INIT_AND_PERIODIC + ".dot"); + + // Split the graph into a list of diagrams. + List splittedDiagrams = + splitInitAndPeriodicDiagrams(stateSpaceInitAndPeriodic); + + // Convert the diagrams into fragments (i.e., having a notion of upstream & + // downstream and carrying object file) and add them to the fragments list. + SSDs = splittedDiagrams; + + // Checking abnomalies. + // FIXME: For some reason, the message reporter does not work here. + if (SSDs.size() == 0) { + throw new RuntimeException( + "No behavior found. The program is not schedulable. Please provide an initial trigger."); + } + if (SSDs.size() > 2) { + throw new RuntimeException( + "More than two fragments detected when splitting the initialization and periodic phase!"); + } + + /* Shutdown phase */ + + // Scenario 1: TIMEOUT + // Generate a state space diagram for the timeout scenario of the + // shutdown phase. + if (targetConfig.get(TimeOutProperty.INSTANCE) != null) { + StateSpaceDiagram stateSpaceShutdownTimeout = + explore(reactor, TimeTag.FOREVER, ExecutionPhase.SHUTDOWN_TIMEOUT, targetConfig); + stateSpaceInitAndPeriodic.generateDotFile( + graphDir, "state_space_" + ExecutionPhase.SHUTDOWN_TIMEOUT + ".dot"); + SSDs.add(stateSpaceShutdownTimeout); + } + + // Scenario 2: STARVATION + // TODO: Generate a state space diagram for the starvation scenario of the + // shutdown phase. + + return SSDs; + } + + /** + * Identify an initialization phase and a periodic phase of the state space diagram, and create + * two different state space diagrams. + */ + public static ArrayList splitInitAndPeriodicDiagrams( + StateSpaceDiagram stateSpace) { + + ArrayList diagrams = new ArrayList<>(); + StateSpaceNode current = stateSpace.head; + StateSpaceNode previous = null; + + // Create an initialization phase diagram. + if (stateSpace.head != stateSpace.loopNode) { + StateSpaceDiagram initPhase = new StateSpaceDiagram(); + initPhase.head = current; + while (current != stateSpace.loopNode) { + // Add node and edges to diagram. + initPhase.addNode(current); + initPhase.addEdge(current, previous); + + // Update current and previous pointer. + previous = current; + current = stateSpace.getDownstreamNode(current); + } + initPhase.tail = previous; + if (stateSpace.loopNode != null) + initPhase.hyperperiod = stateSpace.loopNode.getTime().toNanoSeconds(); + else initPhase.hyperperiod = 0; + initPhase.phase = ExecutionPhase.INIT; + diagrams.add(initPhase); + } + + // Create a periodic phase diagram. + if (stateSpace.isCyclic()) { + + // State this assumption explicitly. + assert current == stateSpace.loopNode : "Current is not pointing to loopNode."; + + StateSpaceDiagram periodicPhase = new StateSpaceDiagram(); + periodicPhase.head = current; + periodicPhase.addNode(current); // Add the first node. + if (current == stateSpace.tail) { + periodicPhase.addEdge(current, current); // Add edges to diagram. + } + while (current != stateSpace.tail) { + // Update current and previous pointer. + // We bring the updates before addNode() because + // we need to make sure tail is added. + // For the init diagram, we do not want to add loopNode. + previous = current; + current = stateSpace.getDownstreamNode(current); + + // Add node and edges to diagram. + periodicPhase.addNode(current); + periodicPhase.addEdge(current, previous); + } + periodicPhase.tail = current; + periodicPhase.loopNode = stateSpace.loopNode; + periodicPhase.addEdge(periodicPhase.loopNode, periodicPhase.tail); // Add loop. + periodicPhase.loopNodeNext = stateSpace.loopNodeNext; + periodicPhase.hyperperiod = stateSpace.hyperperiod; + periodicPhase.phase = ExecutionPhase.PERIODIC; + diagrams.add(periodicPhase); + } + + return diagrams; + } +} diff --git a/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java new file mode 100644 index 000000000..2e0e88930 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/analyses/statespace/StateSpaceNode.java @@ -0,0 +1,107 @@ +package org.lflang.analyses.statespace; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import org.lflang.TimeTag; +import org.lflang.TimeValue; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.TriggerInstance; + +/** A node in the state space diagram representing a step in the execution of an LF program. */ +public class StateSpaceNode { + + private int index; // An integer ID for this node + private TimeTag tag; + private Set reactionsInvoked; + private ArrayList eventQcopy; // A snapshot of the eventQ represented as an ArrayList + + public StateSpaceNode( + TimeTag tag, Set reactionsInvoked, ArrayList eventQcopy) { + this.tag = tag; + this.eventQcopy = eventQcopy; + this.reactionsInvoked = reactionsInvoked; + } + + /** Copy constructor */ + public StateSpaceNode(StateSpaceNode that) { + this.tag = new TimeTag(that.tag); + this.eventQcopy = new ArrayList<>(that.eventQcopy); + this.reactionsInvoked = new HashSet<>(that.reactionsInvoked); + } + + /** Two methods for pretty printing */ + public void display() { + System.out.println("(" + this.tag.time + ", " + reactionsInvoked + ", " + eventQcopy + ")"); + } + + public String toString() { + return "(" + this.tag.time + ", " + reactionsInvoked + ", " + eventQcopy + ")"; + } + + /** + * Generate hash for the node. This hash function is used for checking whether two nodes are + * analogous, meaning that 1) they have the same reactions invoked at their tags, 2) the queued + * events have the same triggers, and 3) the time offsets between future events' tags of one node + * and its tag are the same as the time offsets between future events' tags of the other node and + * the other node's tag. The hash() method is not meant to replace the hashCode() method because + * doing so changes the way nodes are inserted in the state space diagram. + */ + public int hash() { + // Initial value + int result = 17; + + // Generate hash for the reactions invoked. + result = 31 * result + reactionsInvoked.hashCode(); + + // Generate hash for the triggers in the queued events. + int eventsHash = + this.getEventQcopy().stream() + .map(Event::getTrigger) + .map(TriggerInstance::getFullName) + .mapToInt(Object::hashCode) + .reduce(1, (a, b) -> 31 * a + b); + result = 31 * result + eventsHash; + + // Generate hash for the time differences. + long timeDiffHash = + this.getEventQcopy().stream() + .mapToLong(e -> e.getTag().time.toNanoSeconds() - this.tag.time.toNanoSeconds()) + .reduce(1, (a, b) -> 31 * a + b); + result = 31 * result + (int) timeDiffHash; + + return result; + } + + public int getIndex() { + return index; + } + + public void setIndex(int i) { + index = i; + } + + public TimeTag getTag() { + return tag; + } + + public void setTag(TimeTag newTag) { + tag = newTag; + } + + public TimeValue getTime() { + return tag.time; + } + + public Set getReactionsInvoked() { + return reactionsInvoked; + } + + public ArrayList getEventQcopy() { + return eventQcopy; + } + + public void setEventQcopy(ArrayList list) { + eventQcopy = list; + } +} diff --git a/lfc/core/src/main/java/org/lflang/ast/ASTUtils.java b/lfc/core/src/main/java/org/lflang/ast/ASTUtils.java index 3856aeecb..bc52cc030 100644 --- a/lfc/core/src/main/java/org/lflang/ast/ASTUtils.java +++ b/lfc/core/src/main/java/org/lflang/ast/ASTUtils.java @@ -59,10 +59,10 @@ import org.eclipse.xtext.xbase.lib.IteratorExtensions; import org.eclipse.xtext.xbase.lib.StringExtensions; import org.lflang.InferredType; +import org.lflang.MessageReporter; import org.lflang.TimeUnit; import org.lflang.TimeValue; -import org.lflang.generator.CodeMap; -import org.lflang.generator.InvalidSourceException; +import org.lflang.generator.*; import org.lflang.lf.Action; import org.lflang.lf.Assignment; import org.lflang.lf.Code; @@ -98,6 +98,7 @@ import org.lflang.lf.WidthSpec; import org.lflang.lf.WidthTerm; import org.lflang.target.Target; +import org.lflang.target.TargetConfig; import org.lflang.util.StringUtil; /** @@ -509,14 +510,52 @@ public static List allModes(Reactor definition) { return ASTUtils.collectElements(definition, featurePackage.getReactor_Modes()); } - // public static List recursiveChildren(ReactorInstance r) { - // List ret = new ArrayList<>(); - // ret.add(r); - // for (var child : r.children) { - // ret.addAll(recursiveChildren(child)); - // } - // return ret; - // } + /** + * A recursive method for returning all reactor instances + * + * @param r The reactor at which the search begins + * @return A list of reactors, including r and the recursively nested children of r + */ + public static List allReactorInstances(ReactorInstance r) { + List ret = new ArrayList<>(); + ret.add(r); + for (var child : r.children) { + ret.addAll(allReactorInstances(child)); + } + return ret; + } + + /** + * A recursive method for returning all reaction instances under a parent reactor + * + * @param r The reactor at which the search begins + * @return A list of reactions, including those within r and in the recursively nested children of + * r + */ + public static List allReactionInstances(ReactorInstance r) { + List ret = new ArrayList<>(); + ret.addAll(r.reactions); + for (var child : r.children) { + ret.addAll(allReactionInstances(child)); + } + return ret; + } + + /** + * A recursive method for returning all port instances under a parent reactor + * + * @param r The reactor at which the search begins + * @return A list of ports, including those within r and in the recursively nested children of r + */ + public static List allPortInstances(ReactorInstance r) { + List ret = new ArrayList<>(); + ret.addAll(r.inputs); + ret.addAll(r.outputs); + for (var child : r.children) { + ret.addAll(allPortInstances(child)); + } + return ret; + } /** * Return all the superclasses of the specified reactor in deepest-first order. For example, if A @@ -604,39 +643,28 @@ public static List collectElements( * This will also assign levels to reactions, then, if the program is federated, perform an AST * transformation to disconnect connections between federates. */ - // public static ReactorInstance createMainReactorInstance( - // Instantiation mainDef, - // List reactors, - // MessageReporter messageReporter, - // TargetConfig targetConfig) { - // if (mainDef != null) { - // // Recursively build instances. - // ReactorInstance main = - // new ReactorInstance(toDefinition(mainDef.getReactorClass()), messageReporter, - // reactors); - // var reactionInstanceGraph = main.assignLevels(); - // if (reactionInstanceGraph.nodeCount() > 0) { - // messageReporter - // .nowhere() - // .error("Main reactor has causality cycles. Skipping code generation."); - // return null; - // } - // // Inform the run-time of the breadth/parallelism of the reaction graph - // var breadth = reactionInstanceGraph.getBreadth(); - // if (breadth == 0) { - // messageReporter.nowhere().warning("The program has no reactions"); - // } else { - // CompileDefinitionsProperty.INSTANCE.update( - // targetConfig, - // Map.of( - // "LF_REACTION_GRAPH_BREADTH", - // String.valueOf(reactionInstanceGraph.getBreadth()))); - // } - // return main; - // } - // return null; - // } - // + public static ReactorInstance createMainReactorInstance( + Instantiation mainDef, + List reactors) { + if (mainDef != null) { + // Recursively build instances. + ReactorInstance main = + new ReactorInstance(toDefinition(mainDef.getReactorClass()), null, reactors); + var reactionInstanceGraph = main.assignLevels(); + if (reactionInstanceGraph.nodeCount() > 0) { + System.err.println("Main reactor has causality cycles. Skipping code generation."); + return null; + } + // Inform the run-time of the breadth/parallelism of the reaction graph + var breadth = reactionInstanceGraph.getBreadth(); + if (breadth == 0) { + System.err.println("The program has no reactions"); + } + return main; + } + return null; + } + /** * Adds the elements into the given list at a location matching to their textual position. * diff --git a/lfc/core/src/main/java/org/lflang/generator/ActionInstance.java b/lfc/core/src/main/java/org/lflang/generator/ActionInstance.java new file mode 100644 index 000000000..b749a2642 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/ActionInstance.java @@ -0,0 +1,100 @@ +/* Instance of an action. */ + +/************* + * Copyright (c) 2019, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import org.lflang.TimeValue; +import org.lflang.lf.Action; +import org.lflang.lf.ActionOrigin; + +/** + * Instance of an action. + * + * @author Edward A. Lee + * @author Marten Lohstroh + */ +public class ActionInstance extends TriggerInstance { + + /** The constant default for a minimum delay. */ + public static final TimeValue DEFAULT_MIN_DELAY = TimeValue.ZERO; + + /** The minimum delay for this action. */ + private TimeValue minDelay = DEFAULT_MIN_DELAY; + + /** The minimum spacing between action events. */ + private TimeValue minSpacing = null; + + /** The replacement policy for when minimum spacing is violated. */ + private String policy = null; + + /** Indicator of whether the action is physical. */ + private boolean physical; + + /** + * Create a new action instance. If the definition is null, then this is a shutdown action. + * + * @param definition The AST definition, or null for startup. + * @param parent The parent reactor. + */ + public ActionInstance(Action definition, ReactorInstance parent) { + super(definition, parent); + if (parent == null) { + throw new InvalidSourceException("Cannot create an ActionInstance with no parent."); + } + if (definition != null) { + if (definition.getMinDelay() != null) { + this.minDelay = parent.getTimeValue(definition.getMinDelay()); + } + if (definition.getMinSpacing() != null) { + this.minSpacing = parent.getTimeValue(definition.getMinSpacing()); + } + if (definition.getOrigin() == ActionOrigin.PHYSICAL) { + physical = true; + } + policy = definition.getPolicy(); + } + } + + /** Return the minimum delay for this action. */ + public TimeValue getMinDelay() { + return minDelay; + } + + /** Return the minimum spacing between action events. */ + public TimeValue getMinSpacing() { + return minSpacing; + } + + /** Return the replacement policy for when minimum spacing is violated. */ + public String getPolicy() { + return policy; + } + + /** Return the indicator of whether the action is physical. */ + public boolean isPhysical() { + return physical; + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/CodeBuilder.java b/lfc/core/src/main/java/org/lflang/generator/CodeBuilder.java new file mode 100644 index 000000000..af82d7051 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/CodeBuilder.java @@ -0,0 +1,585 @@ +package org.lflang.generator; + +import static org.lflang.generator.c.CMixedRadixGenerator.*; +import static org.lflang.util.StringUtil.joinObjects; + +import java.io.IOException; +import java.nio.file.Path; +import org.eclipse.emf.common.CommonPlugin; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.lflang.ast.ASTUtils; +import org.lflang.generator.c.CUtil; +import org.lflang.lf.Code; +import org.lflang.util.FileUtil; + +/** + * Helper class for printing code with indentation. This class is backed by a StringBuilder and is + * used to accumulate code to be printed to a file. Its main function is to handle indentation. + * + * @author Edward A. Lee + * @author Peter Donovan + */ +public class CodeBuilder { + + private static final String END_SOURCE_LINE_NUMBER_TAG = + "/* END PR SOURCE LINE NUMBER 9sD0aiwE01RcMWl */"; + + /** Construct a new empty code emitter. */ + public CodeBuilder() {} + + /** + * Construct a new code emitter with the text and indentation of the specified code emitter. + * + * @param model The model code emitter. + */ + public CodeBuilder(CodeBuilder model) { + indentation = model.indentation; + code.append(model); + } + + ///////////////////////////////////////////// + ///// Public methods. + + /** + * Get the code produced so far. + * + * @return The code produced so far as a String. + */ + public String getCode() { + return code.toString(); + } + + /** Increase the indentation of the output code produced. */ + public void indent() { + indentation += " "; + } + + /** + * Insert the specified text at the specified position. + * + * @param position The position. + * @param text The text. + */ + public void insert(int position, String text) { + code.insert(position, text); + } + + /** Return the length of the code in characters. */ + public int length() { + return code.length(); + } + + /** Add a new line. */ + public void newLine() { + this.pr(""); + } + + /** + * Append the specified text plus a final newline. + * + * @param format A format string to be used by {@code String.format} or the text to append if no + * further arguments are given. + * @param args Additional arguments to pass to the formatter. + */ + public void pr(String format, Object... args) { + pr((args != null && args.length > 0) ? String.format(format, args) : format); + } + + /** Append the given text to the code buffer at the current indentation level. */ + public void pr(CharSequence text) { + // append newline on empty input string + if (text.toString().equals("")) code.append("\n"); + for (String line : (Iterable) () -> text.toString().lines().iterator()) { + code.append(indentation).append(line).append("\n"); + } + } + + /** + * Print the #line compiler directive with the line number of the specified object. + * + * @param eObject The node. + * @param suppress Do nothing if true. + */ + public void prSourceLineNumber(EObject eObject, boolean suppress) { + if (suppress) { + return; + } + var node = NodeModelUtils.getNode(eObject); + if (node != null) { + // For code blocks (delimited by {= ... =}, unfortunately, + // we have to adjust the offset by the number of newlines before {=. + // Unfortunately, this is complicated because the code has been + // tokenized. + var offset = 0; + if (eObject instanceof Code code) { + var text = ASTUtils.toOriginalText(code).lines().findFirst(); + if (text.isPresent()) { + var firstLine = text.get().trim(); + var exact = NodeModelUtils.getNode(code).getText().replaceAll("\\{=", ""); + for (var line : (Iterable) () -> exact.lines().iterator()) { + if (line.trim().equals(firstLine)) { + break; + } + offset += 1; + } + } + } + // Extract the filename from eResource, an astonishingly difficult thing to do. + String filePath = + CommonPlugin.resolve(eObject.eResource().getURI()).path().replace("\\", "\\\\"); + pr("#line " + (node.getStartLine() + offset) + " \"" + filePath + "\""); + } + } + + /** + * Print a tag marking the end of a block corresponding to the source LF file. + * + * @param suppress Do nothing if true. + */ + public void prEndSourceLineNumber(boolean suppress) { + if (!suppress) pr(END_SOURCE_LINE_NUMBER_TAG); + } + + /** + * Append a single-line, C-style comment to the code. + * + * @param comment The comment. + */ + public void prComment(String comment) { + pr("// " + comment); + } + + /** + * Remove all lines that start with the specified prefix and return a new CodeBuilder with the + * result. + * + * @param prefix The prefix. + */ + public CodeBuilder removeLines(String prefix) { + String separator = "\n"; + String[] lines = toString().split(separator); + + CodeBuilder builder = new CodeBuilder(); + + for (String line : lines) { + String trimmedLine = line.trim(); + if (!trimmedLine.startsWith(prefix)) { + builder.pr(line); + } + } + return builder; + } + + /** + * Start a scoped block, which is a section of code surrounded by curley braces and indented. This + * must be followed by an {@link #endScopedBlock()}. + */ + public void startScopedBlock() { + pr("{"); + indent(); + } + + /** + * Start a scoped block for the specified reactor. If the reactor is a bank, then this starts a + * for loop that iterates over the bank members using a standard index variable whose name is that + * returned by {@link CUtil#bankIndex(ReactorInstance)}. If the reactor is null or is not a bank, + * then this simply starts a scoped block by printing an opening curly brace. This also adds a + * declaration of a pointer to the self struct of the reactor or bank member. + * + *

This block is intended to be nested, where each block is put within a similar block for the + * reactor's parent. This ensures that all (possibly nested) bank index variables are defined + * within the block. + * + *

This must be followed by an {@link #endScopedBlock()}. + * + * @param reactor The reactor instance. + */ + public void startScopedBlock(ReactorInstance reactor) { + if (reactor != null && reactor.isBank()) { + var index = CUtil.bankIndexName(reactor); + pr("// Reactor is a bank. Iterate over bank members."); + pr("for (int " + index + " = 0; " + index + " < " + reactor.width + "; " + index + "++) {"); + indent(); + } else { + startScopedBlock(); + } + } + + /** + * If the specified port is a multiport, then start a specified iteration over the channels of the + * multiport using as the channel index the variable name returned by {@link + * CUtil#channelIndex(PortInstance)}. If the port is not a multiport, do nothing. This is required + * to be followed by {@link #endChannelIteration(PortInstance)}. + * + * @param port The port. + */ + public void startChannelIteration(PortInstance port) { + if (port.isMultiport) { + var channel = CUtil.channelIndexName(port); + pr("// Port " + port.getFullName() + " is a multiport. Iterate over its channels."); + pr( + "for (int " + + channel + + " = 0; " + + channel + + " < " + + port.width + + "; " + + channel + + "++) {"); + indent(); + } + } + + /** + * Start a scoped block to iterate over bank members and channels for the specified port with a + * variable with the name given by count counting the iterations. If this port is a multiport, + * then the channel index variable name is that returned by {@link + * CUtil#channelIndex(PortInstance)}. + * + *

This block is intended to be nested, where each block is put within a similar block for the + * reactor's parent. + * + *

This is required to be followed by a call to {@link + * #endScopedBankChannelIteration(PortInstance, String)}. + * + * @param port The port. + * @param count The variable name to use for the counter, or null to not provide a counter. + */ + public void startScopedBankChannelIteration(PortInstance port, String count) { + if (count != null) { + startScopedBlock(); + pr("int " + count + " = 0;"); + } + startScopedBlock(port.parent); + startChannelIteration(port); + } + + /** + * Start a scoped block that iterates over the specified range of port channels. + * + *

This must be followed by a call to {@link #endScopedRangeBlock(RuntimeRange)}. + * + *

This block should NOT be nested, where each block is put within a similar block for the + * reactor's parent. Within the created block, every use of {@link + * CUtil#reactorRef(ReactorInstance, String)} must provide the second argument, a runtime index + * variable name, that must match the runtimeIndex parameter given here. + * + * @param range The range of port channels. + * @param runtimeIndex A variable name to use to index the runtime instance of either port's + * parent or the port's parent's parent (if nested is true), or null to use the default, + * "runtime_index". + * @param bankIndex A variable name to use to index the bank of the port's parent or null to use + * the default, the string returned by {@link CUtil#bankIndexName(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to use the default, the + * string returned by {@link CUtil#channelIndexName(PortInstance)}. + * @param nested If true, then the runtimeIndex variable will be set to the bank index of the + * port's parent's parent rather than the port's parent. + */ + public void startScopedRangeBlock( + RuntimeRange range, + String runtimeIndex, + String bankIndex, + String channelIndex, + boolean nested) { + + pr("// Iterate over range " + range.toString() + "."); + var ri = (runtimeIndex == null) ? "runtime_index" : runtimeIndex; + var ci = (channelIndex == null) ? CUtil.channelIndexName(range.instance) : channelIndex; + var bi = (bankIndex == null) ? CUtil.bankIndexName(range.instance.parent) : bankIndex; + var rangeMR = range.startMR(); + var sizeMR = rangeMR.getDigits().size(); + var nestedLevel = (nested) ? 2 : 1; + + startScopedBlock(); + if (range.width > 1) { + pr( + String.join( + "\n", + "int range_start[] = { " + joinObjects(rangeMR.getDigits(), ", ") + " };", + "int range_radixes[] = { " + joinObjects(rangeMR.getRadixes(), ", ") + " };", + "int permutation[] = { " + joinObjects(range.permutation(), ", ") + " };", + "mixed_radix_int_t range_mr = {", + " " + sizeMR + ",", + " range_start,", + " range_radixes,", + " permutation", + "};", + "for (int range_count = " + + range.start + + "; range_count < " + + range.start + + " + " + + range.width + + "; range_count++) {")); + indent(); + pr( + String.join( + "\n", + "int " + + ri + + " = mixed_radix_parent(&range_mr, " + + nestedLevel + + "); // Runtime index.", + "SUPPRESS_UNUSED_WARNING(" + ri + ");", + "int " + ci + " = range_mr.digits[0]; // Channel index.", + "SUPPRESS_UNUSED_WARNING(" + ci + ");", + "int " + bi + " = " + (sizeMR <= 1 ? "0" : "range_mr.digits[1]") + "; // Bank index.", + "SUPPRESS_UNUSED_WARNING(" + bi + ");")); + + } else { + var ciValue = rangeMR.getDigits().get(0); + var riValue = rangeMR.get(nestedLevel); + var biValue = (sizeMR > 1) ? rangeMR.getDigits().get(1) : 0; + pr( + String.join( + "\n", + "int " + + ri + + " = " + + riValue + + "; SUPPRESS_UNUSED_WARNING(" + + ri + + "); // Runtime index.", + "int " + + ci + + " = " + + ciValue + + "; SUPPRESS_UNUSED_WARNING(" + + ci + + "); // Channel index.", + "int " + + bi + + " = " + + biValue + + "; SUPPRESS_UNUSED_WARNING(" + + bi + + "); // Bank index.", + "int range_count = 0; SUPPRESS_UNUSED_WARNING(range_count);")); + } + } + + /** + * Start a scoped block that iterates over the specified pair of ranges. The destination range can + * be wider than the source range, in which case the source range is reused until the destination + * range is filled. The following integer variables will be defined within the scoped block: + * + *

    + *
  • src_channel: The channel index for the source. + *
  • src_bank: The bank index of the source port's parent. + *
  • src_runtime: The runtime index of the source port's parent or the parent's parent + * (if the source is an input). + *
+ * + *
    + *
  • dst_channel: The channel index for the destination. + *
  • dst_bank: The bank index of the destination port's parent. + *
  • dst_runtime: The runtime index of the destination port's parent or the parent's + * parent (if destination is an output). + *
+ * + *

For convenience, the above variable names are defined in the private class variables sc, sb, + * sr, and dc, db, dr. + * + *

This block should NOT be nested, where each block is put within a similar block for the + * reactor's parent. Within the created block, every use of {@link + * CUtil#reactorRef(ReactorInstance, String)} and related functions must provide the above + * variable names. + * + *

This must be followed by a call to {@link #endScopedRangeBlock(SendRange, RuntimeRange)}.x + * + * @param srcRange The send range. + * @param dstRange The destination range. + */ + public void startScopedRangeBlock(SendRange srcRange, RuntimeRange dstRange) { + var srcRangeMR = srcRange.startMR(); + var srcSizeMR = srcRangeMR.getRadixes().size(); + var srcNestedLevel = (srcRange.instance.isInput()) ? 2 : 1; + var dstNested = dstRange.instance.isOutput(); + + pr("// Iterate over ranges " + srcRange + " and " + dstRange + "."); + startScopedBlock(); + if (srcRange.width > 1) { + pr( + String.join( + "\n", + "int src_start[] = { " + joinObjects(srcRangeMR.getDigits(), ", ") + " };", + "int src_value[] = { " + + joinObjects(srcRangeMR.getDigits(), ", ") + + " }; // Will be incremented.", + "int src_radixes[] = { " + joinObjects(srcRangeMR.getRadixes(), ", ") + " };", + "int src_permutation[] = { " + joinObjects(srcRange.permutation(), ", ") + " };", + "mixed_radix_int_t src_range_mr = {", + " " + srcSizeMR + ",", + " src_value,", + " src_radixes,", + " src_permutation", + "};")); + } else { + var ciValue = srcRangeMR.getDigits().get(0); + var biValue = (srcSizeMR > 1) ? srcRangeMR.getDigits().get(1) : 0; + var riValue = srcRangeMR.get(srcNestedLevel); + pr( + String.join( + "\n", + "int " + sr + " = " + riValue + "; // Runtime index.", + "SUPPRESS_UNUSED_WARNING(" + sr + ");", + "int " + sc + " = " + ciValue + "; // Channel index.", + "SUPPRESS_UNUSED_WARNING(" + sc + ");", + "int " + sb + " = " + biValue + "; // Bank index.", + "SUPPRESS_UNUSED_WARNING(" + sb + ");")); + } + + startScopedRangeBlock(dstRange, dr, db, dc, dstNested); + + if (srcRange.width > 1) { + pr( + String.join( + "\n", + "int " + + sr + + " = mixed_radix_parent(&src_range_mr, " + + srcNestedLevel + + "); // Runtime index.", + "SUPPRESS_UNUSED_WARNING(" + sr + ");", + "int " + sc + " = src_range_mr.digits[0]; // Channel index.", + "SUPPRESS_UNUSED_WARNING(" + sc + ");", + "int " + + sb + + " = " + + (srcSizeMR <= 1 ? "0" : "src_range_mr.digits[1]") + + "; // Bank index.", + "SUPPRESS_UNUSED_WARNING(" + sb + ");")); + } + } + + public void endScopedBlock() { + unindent(); + pr("}"); + } + + /** + * If the specified port is a multiport, then start a specified iteration over the channels of the + * multiport using as the channel index the variable name returned by {@link + * CUtil#channelIndex(PortInstance)}. If the port is not a multiport, do nothing. + * + * @param port The port. + */ + public void endChannelIteration(PortInstance port) { + if (port.isMultiport) { + unindent(); + pr("}"); + } + } + + /** + * End a scoped block to iterate over bank members and channels for the specified port with a + * variable with the name given by count counting the iterations. + * + * @param port The port. + * @param count The variable name to use for the counter, or null to not provide a counter. + */ + public void endScopedBankChannelIteration(PortInstance port, String count) { + if (count != null) { + pr(count + "++;"); + } + endChannelIteration(port); + endScopedBlock(); + if (count != null) { + endScopedBlock(); + } + } + + /** + * End a scoped block for the specified range. + * + * @param range The send range. + */ + public void endScopedRangeBlock(RuntimeRange range) { + if (range.width > 1) { + pr("mixed_radix_incr(&range_mr);"); + endScopedBlock(); // Terminate for loop. + } + endScopedBlock(); + } + + /** + * End a scoped block that iterates over the specified pair of ranges. + * + * @param srcRange The send range. + * @param dstRange The destination range. + */ + public void endScopedRangeBlock(SendRange srcRange, RuntimeRange dstRange) { + if (srcRange.width > 1) { + pr( + String.join( + "\n", + "mixed_radix_incr(&src_range_mr);", + "if (mixed_radix_to_int(&src_range_mr) >= " + + srcRange.start + + " + " + + srcRange.width + + ") {", + " // Start over with the source.", + " for (int i = 0; i < src_range_mr.size; i++) {", + " src_range_mr.digits[i] = src_start[i];", + " }", + "}")); + } + if (dstRange.width > 1) { + pr("mixed_radix_incr(&range_mr);"); + endScopedBlock(); // Terminate for loop. + } + // Terminate unconditional scope block in startScopedRangeBlock calls. + endScopedBlock(); + endScopedBlock(); + } + + /** Return the code as a string. */ + @Override + public String toString() { + return code.toString(); + } + + /** Reduce the indentation by one level for generated code/ */ + public void unindent() { + indentation = indentation.substring(0, Math.max(0, indentation.length() - 4)); + } + + /** + * Write the text to a file. + * + * @param path The file to write the code to. + */ + public CodeMap writeToFile(String path) throws IOException { + String s = code.toString(); + int lineNumber = 1; + StringBuilder out = new StringBuilder(); + for (var line : (Iterable) () -> s.lines().iterator()) { + lineNumber++; + if (line.contains(END_SOURCE_LINE_NUMBER_TAG) && !path.endsWith(".ino")) { + out.append("#line ") + .append(lineNumber) + .append(" \"") + .append(path.replace("\\", "\\\\")) + .append("\""); + } else { + out.append(line); + } + out.append('\n'); + } + CodeMap ret = CodeMap.fromGeneratedCode(out.toString()); + FileUtil.writeToFile(ret.getGeneratedCode(), Path.of(path), true); + return ret; + } + + //////////////////////////////////////////// + //// Private fields. + + /** Place to store the code. */ + private final StringBuilder code = new StringBuilder(); + + /** Current indentation. */ + private String indentation = ""; +} diff --git a/lfc/core/src/main/java/org/lflang/generator/DeadlineInstance.java b/lfc/core/src/main/java/org/lflang/generator/DeadlineInstance.java new file mode 100644 index 000000000..e73f6b8f9 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/DeadlineInstance.java @@ -0,0 +1,67 @@ +/** A data structure for a deadline instance. */ + +/************* + * Copyright (c) 2019, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import org.lflang.TimeValue; +import org.lflang.lf.Deadline; + +/** + * Instance of a deadline. Upon creation the actual delay is converted into a proper time value. If + * a parameter is referenced, it is looked up in the given (grand)parent reactor instance. + * + * @author Marten Lohstroh + * @author Edward A. Lee + */ +public class DeadlineInstance { + + /** Create a new deadline instance associated with the given reaction instance. */ + public DeadlineInstance(Deadline definition, ReactionInstance reaction) { + if (definition.getDelay() != null) { + this.maxDelay = reaction.parent.getTimeValue(definition.getDelay()); + } else { + this.maxDelay = TimeValue.ZERO; + } + } + + ////////////////////////////////////////////////////// + //// Public fields. + + /** + * The delay D associated with this deadline. If physical time T < logical time t + D, the + * deadline is met, otherwise, it is violated. + */ + public final TimeValue maxDelay; + + ////////////////////////////////////////////////////// + //// Public methods. + + @Override + public String toString() { + return "DeadlineInstance " + maxDelay.toString(); + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/DelayBodyGenerator.java b/lfc/core/src/main/java/org/lflang/generator/DelayBodyGenerator.java new file mode 100644 index 000000000..240eba7bc --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/DelayBodyGenerator.java @@ -0,0 +1,52 @@ +package org.lflang.generator; + +import org.lflang.lf.Action; +import org.lflang.lf.Reaction; +import org.lflang.lf.VarRef; + +public interface DelayBodyGenerator { + + /** Constant that specifies how to name generated delay reactors. */ + String GEN_DELAY_CLASS_NAME = "_lf_GenDelay"; + + /** + * Generate code for the body of a reaction that takes an input and schedules an action with the + * value of that input. + * + * @param action the action to schedule + * @param port the port to read from + */ + String generateDelayBody(Action action, VarRef port); + + /** + * Generate code for the body of a reaction that is triggered by the given action and writes its + * value to the given port. + * + * @param action the action that triggers the reaction + * @param port the port to write to + */ + String generateForwardBody(Action action, VarRef port); + + /** + * Generate code for the generic type to be used in the class definition of a generated delay + * reactor. + */ + String generateDelayGeneric(); + + /** + * Indicates whether delay banks generated from after delays should have a variable length width. + * + *

If this is true, any delay reactors that are inserted for after delays on multiport + * connections will have an unspecified variable length width. The code generator is then + * responsible for inferring the correct width of the delay bank, which is only possible if the + * precise connection width is known at compile time. + * + *

If this is false, the width specification of the generated bank will list all the ports + * listed on the right side of the connection. This gives the code generator the information + * needed to infer the correct width at runtime. + */ + boolean generateAfterDelaysWithVariableWidth(); + + /** Used to optionally apply additional transformations to the generated reactions */ + default void finalizeReactions(Reaction delayReaction, Reaction forwardReaction) {} +} diff --git a/lfc/core/src/main/java/org/lflang/generator/EnclaveInfo.java b/lfc/core/src/main/java/org/lflang/generator/EnclaveInfo.java new file mode 100644 index 000000000..45e99973c --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/EnclaveInfo.java @@ -0,0 +1,20 @@ +package org.lflang.generator; + +public class EnclaveInfo { + public int numIsPresentFields = 0; + public int numStartupReactions = 0; + public int numShutdownReactions = 0; + public int numTimerTriggers = 0; + public int numResetReactions = 0; + public int numWorkers = 1; + public int numModalReactors = 0; + public int numModalResetStates = 0; + public int numWatchdogs = 0; + + public String traceFileName = null; + private ReactorInstance instance; + + public EnclaveInfo(ReactorInstance inst) { + instance = inst; + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/MixedRadixInt.java b/lfc/core/src/main/java/org/lflang/generator/MixedRadixInt.java new file mode 100644 index 000000000..aee756ec6 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/MixedRadixInt.java @@ -0,0 +1,284 @@ +/* A representation for a mixed radix number. */ + +/* +Copyright (c) 2019-2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package org.lflang.generator; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * Representation of a permuted mixed radix (PMR) integer. A mixed radix number is a number + * representation where each digit can have a distinct radix. The radixes are given by a list of + * numbers, r0, r1, ... , rn, where r0 is the radix of the lowest-order digit and rn is the radix of + * the highest order digit that has a specified radix. + * + *

A PMR is a mixed radix number that, when incremented, increments the digits in the order given + * by the permutation matrix. For an ordinary mixed radix number, the permutation matrix is [0, 1, + * ..., n-1]. The permutation matrix may be any permutation of these digits, [d0, d1, ..., dn-1], in + * which case, when incremented, the d0 digit will be incremented first. If it overflows, it will be + * set to 0 and the d1 digit will be incremented. If it overflows, the next digit is incremented. If + * the last digit overflows, then the number wraps around so that all digits become zero. + * + *

This implementation realizes a finite set of numbers, where incrementing past the end of the + * range wraps around to the beginning. As a consequence, the increment() function from any starting + * point is guaranteed to eventually cover all possible values. + * + *

The {@link #toString()} method gives a string representation of the number where each digit is + * represented by the string "d%r", where d is the digit and r is the radix. For example, the number + * "1%2, 2%3, 1%4" has value 11, 1 + (2*2) + (1*2*3). + * + * @author Edward A. Lee + */ +public class MixedRadixInt { + + /** + * Create a mixed radix number with the specified digits and radixes, which are given low-order + * digits first. If there is one more digit than radixes, throw an exception. + * + * @param digits The digits, or null to get a zero-valued number. + * @param radixes The radixes. + * @param permutation The permutation matrix, or null for the default permutation. + */ + public MixedRadixInt(List digits, List radixes, List permutation) { + if (radixes == null + || (digits != null && digits.size() > radixes.size()) + || (permutation != null && permutation.size() != radixes.size())) { + throw new IllegalArgumentException("Invalid constructor arguments."); + } + this.radixes = ImmutableList.copyOf(radixes); + if (digits != null) { + this.digits = digits; + } else { + this.digits = new ArrayList<>(1); + this.digits.add(0); + } + if (permutation != null) { + // Check the permutation matrix. + Set indices = new HashSet<>(); + for (int p : permutation) { + if (p < 0 || p >= radixes.size() || indices.contains(p)) { + throw new IllegalArgumentException( + "Permutation list is required to be a permutation of [0, 1, ... , n-1]."); + } + indices.add(p); + } + this.permutation = permutation; + } + } + + /** A zero-valued mixed radix number with just one digit will radix 1. */ + public static final MixedRadixInt ZERO = new MixedRadixInt(null, List.of(1), null); + + ////////////////////////////////////////////////////////// + //// Public methods + + /** Get the value as an integer. */ + public int get() { + return get(0); + } + + /** + * Get the value as an integer after dropping the first n digits. + * + * @param n The number of digits to drop. + */ + public int get(int n) { + int result = 0; + int scale = 1; + if (n < 0) n = 0; + for (int i = n; i < radixes.size(); i++) { + if (i >= digits.size()) return result; + result += digits.get(i) * scale; + scale *= radixes.get(i); + } + return result; + } + + /** Return the digits. This is assured of returning as many digits as there are radixes. */ + public List getDigits() { + while (digits.size() < radixes.size()) { + digits.add(0); + } + return digits; + } + + /** Return the permutation list. */ + public List getPermutation() { + if (permutation == null) { + // Construct a default permutation. + permutation = new ArrayList<>(radixes.size()); + for (int i = 0; i < radixes.size(); i++) { + permutation.add(i); + } + } + return permutation; + } + + /** Return the radixes. */ + public List getRadixes() { + return radixes; + } + + /** + * Increment the number by one, using the permutation vector to determine the order in which the + * digits are incremented. If an overflow occurs, then a radix-infinity digit will be added to the + * digits array if there isn't one there already. + */ + public void increment() { + int i = 0; + while (i < radixes.size()) { + int digit_to_increment = getPermutation().get(i); + while (digit_to_increment >= digits.size()) { + digits.add(0); + } + digits.set(digit_to_increment, digits.get(digit_to_increment) + 1); + if (digits.get(digit_to_increment) >= radixes.get(digit_to_increment)) { + digits.set(digit_to_increment, 0); + i++; + } else { + return; // All done. + } + } + } + + /** + * Return the magnitude of this PMR, which is defined to be the number of times that increment() + * would need to invoked starting with zero before the value returned by {@link #get()} would be + * reached. + */ + public int magnitude() { + int factor = 1; + int result = 0; + List p = getPermutation(); + for (int i = 0; i < radixes.size(); i++) { + if (digits.size() <= i) return result; + result += factor * digits.get(p.get(i)); + factor *= radixes.get(p.get(i)); + } + return result; + } + + /** + * Return the number of digits in this mixed radix number. This is the size of the radixes list. + */ + public int numDigits() { + return radixes.size(); + } + + /** + * Set the value of this number to equal that of the specified integer. + * + * @param v The ordinary integer value of this number. + */ + public void set(int v) { + // it does not make sense to call set + int temp = v; + int count = 0; + for (int radix : radixes) { + var digit = radix == 0 ? 0 : temp % radix; + if (count >= digits.size()) { + digits.add(digit); + } else { + digits.set(count, digit); + } + count++; + temp = temp == 0 ? temp : temp / radix; + } + } + + /** + * Set the magnitude of this number to equal that of the specified integer, which is the number of + * times that increment must be invoked from zero for the value returned by {@link #get()} to + * equal v. + * + * @param v The new magnitude of this number. + */ + public void setMagnitude(int v) { + int temp = v; + for (int i = 0; i < radixes.size(); i++) { + int p = getPermutation().get(i); + while (digits.size() < p + 1) digits.add(0); + var r = radixes.get(p); + if (r == 0 && v == 0) { + digits.set(p, 0); // zero does not make sense here, but we have to put something. + } else { + digits.set(p, temp % r); + temp = temp / r; + } + } + } + + /** + * Give a string representation of the number, where each digit is represented as n%r, where r is + * the radix. + */ + @Override + public String toString() { + List pieces = new LinkedList<>(); + Iterator radixIterator = radixes.iterator(); + for (int digit : digits) { + if (!radixIterator.hasNext()) { + pieces.add(digit + "%infinity"); + } else { + pieces.add(digit + "%" + radixIterator.next()); + } + } + return String.join(", ", pieces); + } + + @Override + public int hashCode() { + int sum = 0; + for (var radix : radixes) sum = sum * 31 + radix; + for (var digit : digits) sum = sum * 31 + digit; + for (var p : permutation) sum = sum * 31 + p; + return sum; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof MixedRadixInt mri + && radixes.equals(mri.radixes) + && digits.equals(mri.digits) + && permutation.equals(mri.permutation); + } + + public MixedRadixInt copy() { + return new MixedRadixInt(List.copyOf(digits), List.copyOf(radixes), List.copyOf(permutation)); + } + + ////////////////////////////////////////////////////////// + //// Private variables + + private final ImmutableList radixes; + private final List digits; + private List permutation; +} diff --git a/lfc/core/src/main/java/org/lflang/generator/ModeInstance.java b/lfc/core/src/main/java/org/lflang/generator/ModeInstance.java new file mode 100644 index 000000000..932683f0e --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/ModeInstance.java @@ -0,0 +1,206 @@ +/************* + * Copyright (c) 2021, Kiel University. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import java.util.LinkedList; +import java.util.List; +import org.lflang.lf.Mode; +import org.lflang.lf.ModeTransition; +import org.lflang.lf.VarRef; + +/** + * Representation of a runtime instance of a mode. + * + * @author Alexander Schulz-Rosengarten + */ +public class ModeInstance extends NamedInstance { + + /** + * Create a new reaction instance from the specified definition within the specified parent. This + * constructor should be called only by the ReactorInstance class after all other contents + * (reactions, etc.) are registered because this constructor call will look them up. + * + * @param definition A mode definition. + * @param parent The parent reactor instance, which cannot be null. + */ + protected ModeInstance(Mode definition, ReactorInstance parent) { + super(definition, parent); + + collectMembers(); + } + + //////////////////////////////////////////////////// + // Member fields. + + /** The action instances belonging to this mode instance. */ + public List actions = new LinkedList(); + + /** The reactor instances belonging to this mode instance, in order of declaration. */ + public List instantiations = new LinkedList(); + + /** List of reaction instances for this reactor instance. */ + public List reactions = new LinkedList(); + + /** The timer instances belonging to this reactor instance. */ + public List timers = new LinkedList(); + + /** The outgoing transitions of this mode. */ + public List transitions = new LinkedList(); + + //////////////////////////////////////////////////// + // Public methods. + + /** + * Return the name of this mode. + * + * @return The name of this mode. + */ + @Override + public String getName() { + return this.definition.getName(); + } + + /** {@inheritDoc} */ + @Override + public ReactorInstance root() { + return parent.root(); + } + + /** Return a descriptive string. */ + @Override + public String toString() { + return getName() + " of " + parent.getFullName(); + } + + /** Returns true iff this mode is the initial mode of this reactor instance. */ + public boolean isInitial() { + return definition.isInitial(); + } + + /** + * Sets up all transitions that leave this mode. Requires that all mode instances and other + * contents (reactions, etc.) of the parent reactor are created. + */ + public void setupTranstions() { + transitions.clear(); + for (var reaction : reactions) { + for (var effect : reaction.definition.getEffects()) { + if (effect instanceof VarRef) { + var target = effect.getVariable(); + if (target instanceof Mode) { + transitions.add( + new Transition(this, parent.lookupModeInstance((Mode) target), reaction, effect)); + } + } + } + } + } + + /** Returns true iff this mode contains the given instance. */ + public boolean contains(NamedInstance instance) { + if (instance instanceof TimerInstance) { + return timers.contains(instance); + } else if (instance instanceof ActionInstance) { + return actions.contains(instance); + } else if (instance instanceof ReactorInstance) { + return instantiations.contains(instance); + } else if (instance instanceof ReactionInstance) { + return reactions.contains(instance); + } else { + return false; + } + } + + //////////////////////////////////////////////////// + // Private methods. + + private void collectMembers() { + // Collect timers + for (var decl : definition.getTimers()) { + var instance = parent.lookupTimerInstance(decl); + if (instance != null) { + this.timers.add(instance); + } + } + + // Collect actions + for (var decl : definition.getActions()) { + var instance = parent.lookupActionInstance(decl); + if (instance != null) { + this.actions.add(instance); + } + } + + // Collect reactor instantiation + for (var decl : definition.getInstantiations()) { + var instance = parent.lookupReactorInstance(decl); + if (instance != null) { + this.instantiations.add(instance); + } + } + + // Collect reactions + for (var decl : definition.getReactions()) { + var instance = parent.lookupReactionInstance(decl); + if (instance != null) { + this.reactions.add(instance); + } + } + } + + //////////////////////////////////////////////////// + // Data class. + + public static class Transition extends NamedInstance { + public final ModeInstance source; + public final ModeInstance target; + public final ReactionInstance reaction; + public final ModeTransition type; + + Transition( + ModeInstance source, ModeInstance target, ReactionInstance reaction, VarRef definition) { + super(definition, source.parent); + this.source = source; + this.target = target; + this.reaction = reaction; + this.type = + definition.getTransition() == null ? ModeTransition.RESET : definition.getTransition(); + } + + @Override + public String getName() { + return this.source.getName() + " -> " + this.target + " by " + this.reaction.getName(); + } + + @Override + public ReactorInstance root() { + return this.parent.root(); + } + + public ModeTransition getType() { + return type; + } + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/NamedInstance.java b/lfc/core/src/main/java/org/lflang/generator/NamedInstance.java new file mode 100644 index 000000000..51b55f026 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/NamedInstance.java @@ -0,0 +1,316 @@ +/* Base class for instances with names in Lingua Franca. */ + +/************* + * Copyright (c) 2019, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.eclipse.emf.ecore.EObject; + +/** + * Base class for compile-time instances with names in Lingua Franca. An instance of concrete + * subclasses of this class represents one or more runtime instances of a reactor, port, reaction, + * etc. There will be more than one runtime instance if the object or any of its parents is a bank + * of reactors. + * + * @author Marten Lohstroh + * @author Edward A. Lee + */ +public abstract class NamedInstance { + + /** + * Construct a new instance with the specified definition and parent. E.g., for a reactor + * instance, the definition is Instantiation, and for a port instance, it is Port. These are nodes + * in the AST. This is protected because only subclasses should be constructed. + * + * @param definition The definition in the AST for this instance. + * @param parent The reactor instance that creates this instance. + */ + protected NamedInstance(T definition, ReactorInstance parent) { + this.definition = definition; + this.parent = parent; + + // Calculate the depth. + this.depth = 0; + ReactorInstance p = parent; + while (p != null) { + p = p.parent; + this.depth++; + } + } + + ////////////////////////////////////////////////////// + //// Public fields. + + /** A limit on the number of characters returned by uniqueID. */ + public static int identifierLengthLimit = 40; + + ////////////////////////////////////////////////////// + //// Public methods. + + /** Return the definition, which is the AST node for this object. */ + public T getDefinition() { + return definition; + } + + /** + * Get the depth of the reactor instance. This is 0 for the main reactor, 1 for reactors + * immediately contained therein, etc. + */ + public int getDepth() { + return depth; + } + + /** + * Return the full name of this instance, which has the form "a.b.c", where "c" is the name of + * this instance, "b" is the name of its container, and "a" is the name of its container, stopping + * at the container in main. If any reactor in the hierarchy is in a bank of reactors then, it + * will appear as a[index]. Similarly, if c is a port in a multiport, it will appear as c[index]. + * + * @return The full name of this instance. + */ + public String getFullName() { + return getFullNameWithJoiner("."); + } + + /** + * Return a string of the form "a.b.c", where "." is replaced by the specified joiner, "c" is the + * name of this instance, "b" is the name of its container, and "a" is the name of its container, + * stopping at the container in main. + * + * @return A string representing this instance. + */ + public String getFullNameWithJoiner(String joiner) { + // This is not cached because _uniqueID is cached. + if (parent == null) { + return this.getName(); + } else if (getMode(true) != null) { + return parent.getFullNameWithJoiner(joiner) + + joiner + + getMode(true).getName() + + joiner + + this.getName(); + } else { + return parent.getFullNameWithJoiner(joiner) + joiner + this.getName(); + } + } + + /** + * Return the name of this instance as given in its definition. Note that this is unique only + * relative to other instances with the same parent. + * + * @return The name of this instance within its parent. + */ + public abstract String getName(); + + /** Return the parent or null if this is a top-level reactor. */ + public ReactorInstance getParent() { + return parent; + } + + /** + * Return the parent at the given depth or null if there is no parent at the given depth. + * + * @param d The depth. + */ + public ReactorInstance getParent(int d) { + if (d >= depth || d < 0) return null; + ReactorInstance p = parent; + while (p != null) { + if (p.depth == d) return p; + p = p.parent; + } + return null; + } + + /** + * Return the width of this instance, which in this base class is 1. Subclasses PortInstance and + * ReactorInstance change this to the multiport and bank widths respectively. + */ + public int getWidth() { + return width; + } + + /** + * Return true if this instance has the specified parent (possibly indirectly, anywhere up the + * hierarchy). + */ + public boolean hasParent(ReactorInstance container) { + + ReactorInstance p = parent; + + while (p != null) { + if (p == container) return true; + p = p.parent; + } + return false; + } + + /** Return a list of all the parents starting with the root(). */ + public List parents() { + List result = new ArrayList(depth + 1); + if (this instanceof ReactorInstance && parent == null) { + // This is the top level, so it must be a reactor. + result.add((ReactorInstance) this); + } + ReactorInstance container = parent; + while (container != null) { + result.add(container); + container = container.parent; + } + return result; + } + + /** + * Return the root reactor, which is the top-level parent. + * + * @return The top-level parent. + */ + public ReactorInstance root() { + if (parent != null) { + return parent.root(); + } else { + return (ReactorInstance) this; + } + } + + /** + * Set the width. This method is here for testing only and should not be used for any other + * purpose. + * + * @param width The new width. + */ + public void setWidth(int width) { + this.width = width; + } + + /** + * Return an identifier for this instance, which has the form "a_b_c" or "a_b_c_n", where "c" is + * the name of this instance, "b" is the name of its container, and "a" is the name of its + * container, stopping at the container in main. All names are converted to lower case. The suffix + * _n is usually omitted, but it is possible to get name collisions using the above scheme, in + * which case _n will be an increasing integer until there is no collision. If the length of the + * root of the name as calculated above (the root is without the _n suffix) is longer than the + * static variable identifierLengthLimit, then the name will be truncated. The returned name will + * be the tail of the name calculated above with the prefix '_'. + * + * @return An identifier for this instance that is guaranteed to be unique within the top-level + * parent. + */ + public String uniqueID() { + if (_uniqueID == null) { + // Construct the unique ID only if it has not been + // previously constructed. + String prefix = getFullNameWithJoiner("_").toLowerCase(); + + // Replace all non-alphanumeric (Latin) characters with underscore. + prefix = prefix.replaceAll("[^A-Za-z0-9]", "_"); + + // Truncate, if necessary. + if (prefix.length() > identifierLengthLimit) { + prefix = '_' + prefix.substring(prefix.length() - identifierLengthLimit + 1); + } + + // Ensure uniqueness. + ReactorInstance toplevel = root(); + if (toplevel.uniqueIDCount == null) { + toplevel.uniqueIDCount = new HashMap(); + } + var count = toplevel.uniqueIDCount.get(prefix); + if (count == null) { + toplevel.uniqueIDCount.put(prefix, 1); + _uniqueID = prefix; + } else { + toplevel.uniqueIDCount.put(prefix, count + 1); + // NOTE: The length of this name could exceed + // identifierLengthLimit. Is this OK? + _uniqueID = prefix + '_' + (count + 1); + } + } + return _uniqueID; + } + + /** + * Returns the directly/indirectly enclosing mode. + * + * @param direct flag whether to check only for direct enclosing mode or also consider modes of + * parent reactor instances. + * @return The mode, if any, null otherwise. + */ + public ModeInstance getMode(boolean direct) { + ModeInstance mode = null; + if (parent != null) { + if (!parent.modes.isEmpty()) { + mode = parent.modes.stream().filter(it -> it.contains(this)).findFirst().orElse(null); + } + if (mode == null && !direct) { + mode = parent.getMode(false); + } + } + return mode; + } + + ////////////////////////////////////////////////////// + //// Protected fields. + + /** The Instantiation AST object from which this was created. */ + T definition; + + /** The reactor instance that creates this instance. */ + ReactorInstance parent; + + /** + * Map from a name of the form a_b_c to the number of unique IDs with that prefix that have been + * already assigned. If none have been assigned, then there is no entry in this map. This map + * should be non-null only for the main reactor (the top level). + */ + HashMap uniqueIDCount; + + /** + * The width of this instance. This is 1 for everything except a PortInstance representing a + * multiport and a ReactorInstance representing a bank. + */ + int width = 1; + + ////////////////////////////////////////////////////// + //// Protected fields. + + /** + * The depth in the hierarchy of this instance. This is 0 for main or federated, 1 for the + * reactors immediately contained, etc. + */ + protected int depth = 0; + + ////////////////////////////////////////////////////// + //// Private fields. + + /** The full name of this instance. */ + private String _fullName = null; + + /** Unique ID for this instance. This is null until uniqueID() is called. */ + private String _uniqueID = null; +} diff --git a/lfc/core/src/main/java/org/lflang/generator/ParameterInstance.java b/lfc/core/src/main/java/org/lflang/generator/ParameterInstance.java new file mode 100644 index 000000000..92a1b03ed --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/ParameterInstance.java @@ -0,0 +1,118 @@ +/** A data structure for a parameter instance. */ + +/************* + * Copyright (c) 2019, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import java.util.List; +import java.util.Optional; +import org.lflang.InferredType; +import org.lflang.ast.ASTUtils; +import org.lflang.lf.Assignment; +import org.lflang.lf.Initializer; +import org.lflang.lf.Parameter; + +/** + * Representation of a compile-time instance of a parameter. Upon creation, it is checked whether + * this parameter is overridden by an assignment in the instantiation that this parameter instance + * is a result of. If it is overridden, the parameter gets initialized using the value looked up in + * the instantiation hierarchy. + * + * @author Marten Lohstroh + * @author Edward A. Lee + */ +public class ParameterInstance extends NamedInstance { + + /** + * Create a runtime instance from the specified definition and with the specified parent that + * instantiated it. + * + * @param definition The declaration in the AST. + * @param parent The reactor instance this parameter is a part of. + */ + public ParameterInstance(Parameter definition, ReactorInstance parent) { + super(definition, parent); + if (parent == null) { + throw new InvalidSourceException("Cannot create a ParameterInstance with no parent."); + } + + this.type = ASTUtils.getInferredType(definition); + } + + ///////////////////////////////////////////// + //// Public Fields + + public InferredType type; + + ///////////////////////////////////////////// + //// Public Methods + + /** Get the initial value of this parameter. */ + private Initializer getInitialValue() { + return definition.getInit(); + } + + /** + * Return the (possibly overridden) value of this parameter in the containing instance. Parameter + * references are resolved to actual expressions. + */ + public Initializer getActualValue() { + Assignment override = getOverride(); + Initializer init; + if (override != null) { + init = override.getRhs(); + } else { + init = getInitialValue(); + } + return init; + } + + /** + * Return the name of this parameter. + * + * @return The name of this parameter. + */ + public String getName() { + return this.definition.getName(); + } + + /** + * Return the assignment that overrides this parameter in the parent's instantiation or null if + * there is no override. + */ + public Assignment getOverride() { + List assignments = parent.definition.getParameters(); + Optional assignment = + assignments.stream().filter(it -> it.getLhs() == definition).findFirst(); + return assignment.orElse(null); + } + + /** Return a descriptive string. */ + @Override + public String toString() { + return "ParameterInstance " + getFullName(); + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/PortInstance.java b/lfc/core/src/main/java/org/lflang/generator/PortInstance.java new file mode 100644 index 000000000..a8be67741 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/PortInstance.java @@ -0,0 +1,485 @@ +/** A data structure for a port instance. */ + +/************* + * Copyright (c) 2019-2022, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import org.lflang.MessageReporter; +import org.lflang.lf.Input; +import org.lflang.lf.Output; +import org.lflang.lf.Parameter; +import org.lflang.lf.Port; +import org.lflang.lf.WidthSpec; +import org.lflang.lf.WidthTerm; + +/** + * Representation of a compile-time instance of a port. Like {@link ReactorInstance}, if one or more + * parents of this port is a bank of reactors, then there will be more than one runtime instance + * corresponding to this compile-time instance. + * + *

This may be a single port or a multiport. If it is a multiport, then one instance of this + * PortInstance class represents all channels. If in addition any parent is a bank, then it + * represents all channels of all bank members. The {@link #eventualDestinations()} and {@link + * #eventualSources()} functions report the connectivity of all such channels as lists of {@link + * SendRange} and {@link RuntimeRange} objects. + * + * @author Marten Lohstroh + * @author Edward A. Lee + */ +public class PortInstance extends TriggerInstance { + + /** + * Create a runtime instance from the specified definition and with the specified parent that + * instantiated it. + * + * @param definition The declaration in the AST. + * @param parent The parent. + */ + public PortInstance(Port definition, ReactorInstance parent) { + this(definition, parent, null); + } + + /** + * Create a port instance from the specified definition and with the specified parent that + * instantiated it. + * + * @param definition The declaration in the AST. + * @param parent The parent. + * @param messageReporter An error reporter, or null to throw exceptions. + */ + public PortInstance(Port definition, ReactorInstance parent, MessageReporter messageReporter) { + super(definition, parent); + + if (parent == null) { + throw new NullPointerException("Cannot create a PortInstance with no parent."); + } + + setInitialWidth(messageReporter); + } + + ////////////////////////////////////////////////////// + //// Public methods + + /** + * Clear cached information about the connectivity of this port. In particular, {@link + * #eventualDestinations()} and {@link #eventualSources()} cache the lists they return. To force + * those methods to recompute their lists, call this method. This method also clears the caches of + * any ports that are listed as eventual destinations and sources. + */ + public void clearCaches() { + if (clearingCaches) return; // Prevent stack overflow. + clearingCaches = true; + try { + if (eventualSourceRanges != null) { + for (RuntimeRange sourceRange : eventualSourceRanges) { + sourceRange.instance.clearCaches(); + } + } + if (eventualDestinationRanges != null) { + for (SendRange sendRange : eventualDestinationRanges) { + for (RuntimeRange destinationRange : sendRange.destinations) { + destinationRange.instance.clearCaches(); + } + } + } + eventualDestinationRanges = null; + eventualSourceRanges = null; + } finally { + clearingCaches = false; + } + } + + /** + * Return a list of ranges of this port, where each range sends to a list of destination ports + * that receive data from the range of this port. Each destination port is annotated with the + * channel range on which it receives data. The ports listed are only ports that are sources for + * reactions, not relay ports that the data may go through on the way. Also, if there is an + * "after" delay anywhere along the path, then the destination is not in the resulting list. + * + *

If this port itself has dependent reactions, then this port will be included as a + * destination in all items on the returned list. + * + *

Each item in the returned list has the following fields: + * + *

    + *
  • {@code startRange}: The starting channel index of this port. + *
  • {@code rangeWidth}: The number of channels sent to the destinations. + *
  • {@code destinations}: A list of port ranges for destination ports, each of which has the + * same width as {@code rangeWidth}. + *
+ * + * Each item also has a method, {@link SendRange#getNumberOfDestinationReactors()}, that returns + * the total number of unique destination reactors for its range. This is not necessarily the same + * as the number of ports in its destinations field because some of the ports may share the same + * container reactor. + */ + public List eventualDestinations() { + if (eventualDestinationRanges != null) { + return eventualDestinationRanges; + } + + // Construct the full range for this port. + RuntimeRange range = new RuntimeRange.Port(this); + eventualDestinationRanges = eventualDestinations(range, true); + return eventualDestinationRanges; + } + + /** + * Similar to eventualDestinations(), this method returns a list of ranges of this port, where + * each range sends to a list of destination ports that receive data from the range of this port. + * Each destination port is annotated with the channel range on which it receives data. The ports + * listed are only ports that are sources for reactions, not relay ports that the data may go + * through on the way. + * + *

Different than eventualDestinations(), this method includes destinations with after delays + * in between. + */ + public List eventualDestinationsWithAfterDelays() { + if (eventualDestinationRangesWithAfterDelays != null) { + return eventualDestinationRangesWithAfterDelays; + } + + // Construct the full range for this port. + RuntimeRange range = new RuntimeRange.Port(this); + eventualDestinationRangesWithAfterDelays = eventualDestinations(range, false); + return eventualDestinationRangesWithAfterDelays; + } + + /** + * Return a list of ranges of ports that send data to this port. If this port is directly written + * to by one more more reactions, then it is its own eventual source and only this port will be + * represented in the result. + * + *

If this is not a multiport and is not within a bank, then the list will have only one item + * and the range will have a total width of one. Otherwise, it will have enough items so that the + * range widths add up to the width of this multiport multiplied by the total number of instances + * within containing banks. + * + *

The ports listed are only ports that are written to by reactions, not relay ports that the + * data may go through on the way. + */ + public List> eventualSources() { + return eventualSources(new RuntimeRange.Port(this)); + } + + /** + * Return the list of ranges of this port together with the downstream ports that are connected to + * this port. The total with of the ranges in the returned list is a multiple N >= 0 of the total + * width of this port. + */ + public List getDependentPorts() { + return dependentPorts; + } + + /** + * Return the list of upstream ports that are connected to this port, or an empty set if there are + * none. For an ordinary port, this list will have length 0 or 1. For a multiport, it can have a + * larger size. + */ + public List> getDependsOnPorts() { + return dependsOnPorts; + } + + /** Return true if the port is an input. */ + public boolean isInput() { + return (definition instanceof Input); + } + + /** Return true if this is a multiport. */ + public boolean isMultiport() { + return isMultiport; + } + + /** Return true if the port is an output. */ + public boolean isOutput() { + return (definition instanceof Output); + } + + @Override + public String toString() { + return "PortInstance " + getFullName(); + } + + /** + * Record that the {@code index}th sub-port of this has a dependent reaction of level {@code + * level}. + */ + public void hasDependentReactionWithLevel(MixedRadixInt index, int level) { + levelUpperBounds.put( + index, Math.min(levelUpperBounds.getOrDefault(index, Integer.MAX_VALUE), level)); + } + + /** Return the minimum of the levels of the reactions that are downstream of this port. */ + public int getLevelUpperBound(MixedRadixInt index) { + // It should be uncommon for Integer.MAX_VALUE to be used and using it can mask bugs. + // It makes sense when there is no downstream reaction. + return levelUpperBounds.getOrDefault(index, Integer.MAX_VALUE); + } + + ////////////////////////////////////////////////////// + //// Protected fields. + + /** + * Ranges of this port together with downstream ports that are connected directly to this port. + * When there are multiple destinations, the destinations are listed in the order they appear in + * connections in the parent reactor instance of this port (inside connections), followed by the + * order in which they appear in the parent's parent (outside connections). The total of the + * widths of these SendRanges is an integer multiple N >= 0 of the width of this port (this is + * checked by the validator). Each channel of this port will be broadcast to N recipients (or, if + * there are no connections to zero recipients). + */ + List dependentPorts = new ArrayList<>(); + + /** + * Upstream ports that are connected directly to this port, if there are any. For an ordinary + * port, this set will have size 0 or 1. For a multiport, it can have a larger size. This + * initially has capacity 1 because that is by far the most common case. + */ + List> dependsOnPorts = new ArrayList<>(1); + + /** Indicator of whether this is a multiport. */ + boolean isMultiport = false; + + ////////////////////////////////////////////////////// + //// Private methods. + + /** + * Given a RuntimeRange, return a list of SendRange that describes the eventual destinations of + * the given range. The sum of the total widths of the send ranges on the returned list will be an + * integer multiple N of the total width of the specified range. Each returned SendRange has a + * list of destination RuntimeRanges, each of which represents a port that has dependent + * reactions. Intermediate ports with no dependent reactions are not listed. + * + * @param srcRange The source range. + */ + private static List eventualDestinations( + RuntimeRange srcRange, boolean skipAfterDelays) { + + // Getting the destinations is more complex than getting the sources + // because of multicast, where there is more than one connection statement + // for a source of data. The strategy we follow here is to first get all + // the ports that this port eventually sends to. Then, if needed, split + // the resulting ranges so that the resulting list covers exactly + // srcRange, possibly in pieces. We make two passes. First, we build + // a queue of ranges that may overlap, then we split those ranges + // and consolidate their destinations. + + List result = new ArrayList<>(); + PriorityQueue queue = new PriorityQueue<>(); + PortInstance srcPort = srcRange.instance; + + // Start with, if this port has dependent reactions, then add it to + // every range of the result. + if (!srcRange.instance.dependentReactions.isEmpty()) { + // This will be the final result if there are no connections. + SendRange candidate = + new SendRange( + srcRange.instance, + srcRange.start, + srcRange.width, + null, // No interleaving for this range. + null // No connection for this range. + ); + candidate.destinations.add(srcRange); + queue.add(candidate); + } + + // Need to find send ranges that overlap with this srcRange. + for (SendRange wSendRange : srcPort.dependentPorts) { + + if (skipAfterDelays) { + if (wSendRange.connection != null + && (wSendRange.connection.getDelay() != null || wSendRange.connection.isPhysical())) { + continue; + } + } + + wSendRange = wSendRange.overlap(srcRange); + if (wSendRange == null) { + // This send range does not overlap with the desired range. Try the next one. + continue; + } + for (RuntimeRange dstRange : wSendRange.destinations) { + // Recursively get the send ranges of that destination port. + List dstSendRanges = eventualDestinations(dstRange, skipAfterDelays); + int sendRangeStart = 0; + for (SendRange dstSend : dstSendRanges) { + queue.add(dstSend.newSendRange(wSendRange, sendRangeStart)); + sendRangeStart += dstSend.width; + } + } + } + + // Now check for overlapping ranges, constructing a new result. + SendRange candidate = queue.poll(); + SendRange next = queue.poll(); + while (candidate != null) { + if (next == null) { + // No more candidates. We are done. + result.add(candidate); + break; + } + if (candidate.start == next.start) { + // Ranges have the same starting point. Need to merge them. + if (candidate.width <= next.width) { + // Can use all of the channels of candidate. + // Import the destinations of next and split it. + for (RuntimeRange destination : next.destinations) { + candidate.destinations.add(destination.head(candidate.width)); + } + if (candidate.width < next.width) { + // The next range has more channels connected to this sender. + // Put it back on the queue an poll for a new next. + queue.add(next.tail(candidate.width)); + next = queue.poll(); + } else { + // We are done with next and can discard it. + next = queue.poll(); + } + } else { + // candidate is wider than next. Switch them and continue. + SendRange temp = candidate; + candidate = next; + next = temp; + } + } else { + // Because the result list is sorted, next starts at + // a higher channel than candidate. + if (candidate.start + candidate.width <= next.start) { + // Can use candidate as is and make next the new candidate. + result.add(candidate); + candidate = next; + next = queue.poll(); + } else { + // Ranges overlap. Can use a truncated candidate and make its + // truncated version the new candidate. + result.add(candidate.head(next.start)); + candidate = candidate.tail(next.start); + } + } + } + + return result; + } + + /** + * Return a list of ranges of ports that send data to this port within the specified range. If + * this port is directly written to by one more more reactions, then it is its own eventual source + * and only this port will be represented in the result. + * + *

If this is not a multiport and is not within a bank, then the list will have only one item + * and the range will have a total width of one. Otherwise, it will have enough items so that the + * range widths add up to the width of this multiport multiplied by the total number of instances + * within containing banks. + * + *

The ports listed are only ports that are written to by reactions, not relay ports that the + * data may go through on the way. + */ + private List> eventualSources(RuntimeRange range) { + if (eventualSourceRanges == null) { + // Cached result has not been created. + eventualSourceRanges = new ArrayList<>(); + + if (!dependsOnReactions.isEmpty()) { + eventualSourceRanges.add(new RuntimeRange.Port(this)); + } else { + var channelsCovered = 0; + for (RuntimeRange sourceRange : dependsOnPorts) { + // Check whether the sourceRange overlaps with the range. + if (channelsCovered + sourceRange.width >= range.start + && channelsCovered < range.start + range.width) { + eventualSourceRanges.addAll(sourceRange.instance.eventualSources(sourceRange)); + } + channelsCovered += sourceRange.width; + } + } + } + return eventualSourceRanges; + } + + /** + * Set the initial multiport width, if this is a multiport, from the widthSpec in the definition. + * This will be set to -1 if the width cannot be determined. + * + * @param messageReporter For reporting errors. + */ + private void setInitialWidth(MessageReporter messageReporter) { + // If this is a multiport, determine the width. + WidthSpec widthSpec = definition.getWidthSpec(); + + if (widthSpec != null) { + if (widthSpec.isOfVariableLength()) { + String message = "Variable-width multiports not supported (yet): " + definition.getName(); + messageReporter.at(definition).error(message); + } else { + isMultiport = true; + + // Determine the initial width, if possible. + // The width may be given by a parameter or even sum of parameters. + width = 0; + for (WidthTerm term : widthSpec.getTerms()) { + Parameter parameter = term.getParameter(); + if (parameter != null) { + Integer parameterValue = parent.initialIntParameterValue(parameter); + // Only a Literal is supported. + if (parameterValue != null) { + width += parameterValue; + } else { + width = -1; + return; + } + } else if (term.getWidth() != 0) { + width += term.getWidth(); + } else { + width = -1; + return; + } + } + } + } + } + + ////////////////////////////////////////////////////// + //// Private fields. + + /** Cached list of destination ports with channel ranges. */ + private List eventualDestinationRanges; + + /** Cached list of destination ports with channel ranges including after delays. */ + private List eventualDestinationRangesWithAfterDelays; + + /** Cached list of source ports with channel ranges. */ + private List> eventualSourceRanges; + + /** Indicator that we are clearing the caches. */ + private boolean clearingCaches = false; + + /** The levels of the sub-ports of this. */ + private final Map levelUpperBounds = new HashMap<>(); +} diff --git a/lfc/core/src/main/java/org/lflang/generator/ReactionInstance.java b/lfc/core/src/main/java/org/lflang/generator/ReactionInstance.java new file mode 100644 index 000000000..29dbcba33 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/ReactionInstance.java @@ -0,0 +1,583 @@ +/************* + * Copyright (c) 2019-2022, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.eclipse.xtext.xbase.lib.StringExtensions; +import org.lflang.AttributeUtils; +import org.lflang.TimeValue; +import org.lflang.ast.ASTUtils; +import org.lflang.lf.Action; +import org.lflang.lf.BuiltinTriggerRef; +import org.lflang.lf.Port; +import org.lflang.lf.Reaction; +import org.lflang.lf.Timer; +import org.lflang.lf.TriggerRef; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; +import org.lflang.lf.Watchdog; + +/** + * Representation of a compile-time instance of a reaction. Like {@link ReactorInstance}, if one or + * more parents of this reaction is a bank of reactors, then there will be more than one runtime + * instance corresponding to this compile-time instance. The {@link #getRuntimeInstances()} method + * returns a list of these runtime instances, each an instance of the inner class {@link + * ReactionInstance.Runtime}. Each runtime instance has a "level", which is its depth an acyclic + * precedence graph representing the dependencies between reactions at a tag. + * + * @author Edward A. Lee + * @author Marten Lohstroh + */ +public class ReactionInstance extends NamedInstance { + + /** + * Create a new reaction instance from the specified definition within the specified parent. This + * constructor should be called only by the ReactorInstance class, but it is public to enable unit + * tests. + * + * @param definition A reaction definition. + * @param parent The parent reactor instance, which cannot be null. + * @param index The index of the reaction within the reactor (0 for the first reaction, 1 for the + * second, etc.). + */ + public ReactionInstance(Reaction definition, ReactorInstance parent, int index) { + super(definition, parent); + this.index = index; + + String body = ASTUtils.toText(definition.getCode()); + + // Identify the dependencies for this reaction. + // First handle the triggers. + for (TriggerRef trigger : definition.getTriggers()) { + if (trigger instanceof VarRef) { + Variable variable = ((VarRef) trigger).getVariable(); + if (variable instanceof Port) { + PortInstance portInstance = parent.lookupPortInstance((Port) variable); + // If the trigger is the port of a contained bank, then the + // portInstance will be null and we have to instead search for + // each port instance in the bank. + if (portInstance != null) { + this.sources.add(portInstance); + portInstance.dependentReactions.add(this); + this.triggers.add(portInstance); + } else if (((VarRef) trigger).getContainer() != null) { + // Port belongs to a contained reactor or bank. + ReactorInstance containedReactor = + parent.lookupReactorInstance(((VarRef) trigger).getContainer()); + if (containedReactor != null) { + portInstance = containedReactor.lookupPortInstance((Port) variable); + if (portInstance != null) { + this.sources.add(portInstance); + portInstance.dependentReactions.add(this); + this.triggers.add(portInstance); + } + } + } + } else if (variable instanceof Action) { + var actionInstance = + parent.lookupActionInstance((Action) ((VarRef) trigger).getVariable()); + this.triggers.add(actionInstance); + actionInstance.dependentReactions.add(this); + this.sources.add(actionInstance); + } else if (variable instanceof Timer) { + var timerInstance = parent.lookupTimerInstance((Timer) ((VarRef) trigger).getVariable()); + this.triggers.add(timerInstance); + timerInstance.dependentReactions.add(this); + this.sources.add(timerInstance); + } else if (variable instanceof Watchdog) { + var watchdogInstance = + parent.lookupWatchdogInstance((Watchdog) ((VarRef) trigger).getVariable()); + this.triggers.add(watchdogInstance); + } + } else if (trigger instanceof BuiltinTriggerRef) { + var builtinTriggerInstance = parent.getOrCreateBuiltinTrigger((BuiltinTriggerRef) trigger); + this.triggers.add(builtinTriggerInstance); + builtinTriggerInstance.dependentReactions.add(this); + } + } + // Next handle the ports that this reaction reads. + for (VarRef source : definition.getSources()) { + Variable variable = source.getVariable(); + if (variable instanceof Port) { + var portInstance = parent.lookupPortInstance((Port) variable); + + // If the trigger is the port of a contained bank, then the + // portInstance will be null and we have to instead search for + // each port instance in the bank. + if (portInstance != null) { + this.sources.add(portInstance); + this.reads.add(portInstance); + portInstance.dependentReactions.add(this); + } else if (source.getContainer() != null) { + ReactorInstance containedReactor = parent.lookupReactorInstance(source.getContainer()); + if (containedReactor != null) { + portInstance = containedReactor.lookupPortInstance((Port) variable); + if (portInstance != null) { + this.sources.add(portInstance); + portInstance.dependentReactions.add(this); + this.triggers.add(portInstance); + } + } + } + } + } + + // Finally, handle the effects. + for (VarRef effect : definition.getEffects()) { + Variable variable = effect.getVariable(); + if (variable instanceof Port) { + var portInstance = parent.lookupPortInstance(effect); + if (portInstance != null) { + this.effects.add(portInstance); + portInstance.dependsOnReactions.add(this); + } else { + throw new InvalidSourceException( + "Unexpected effect. Cannot find port " + variable.getName()); + } + } else if (variable instanceof Action) { + // Effect is an Action. + var actionInstance = parent.lookupActionInstance((Action) variable); + this.effects.add(actionInstance); + actionInstance.dependsOnReactions.add(this); + } else if (variable instanceof Watchdog) { + var watchdogInstance = parent.lookupWatchdogInstance((Watchdog) variable); + this.effects.add(watchdogInstance); + } else { + // Effect is either a mode or an unresolved reference. + // Do nothing, transitions will be set up by the ModeInstance. + } + } + // Create a deadline instance if one has been defined. + if (this.definition.getDeadline() != null) { + this.declaredDeadline = new DeadlineInstance(this.definition.getDeadline(), this); + } + // If @wcet annotation is specified, update the wcet. + this.wcet = AttributeUtils.getWCET(this.definition); + } + + ////////////////////////////////////////////////////// + //// Public fields. + + /** The ports or actions that this reaction may write to. */ + public Set> effects = new LinkedHashSet<>(); + + /** The ports, actions, or timers that this reaction is triggered by or uses. */ + public Set> sources = new LinkedHashSet<>(); + + // FIXME: Above sources is misnamed because in the grammar, + // "sources" are only the inputs a reaction reads without being + // triggered by them. The name "reads" used here would be a better + // choice in the grammar. + + /** Deadline for this reaction instance, if declared. */ + public DeadlineInstance declaredDeadline; + + /** + * Index of order of occurrence within the reactor definition. The first reaction has index 0, the + * second index 1, etc. + */ + public int index; + + /** The ports that this reaction reads but that do not trigger it. */ + public Set> reads = new LinkedHashSet<>(); + + /** + * The trigger instances (input ports, timers, and actions that trigger reactions) that trigger + * this reaction. + */ + public Set> triggers = new LinkedHashSet<>(); + + /** + * The worst-case execution time (WCET) of the reaction. Note that this is platform dependent. If + * the WCET is unknown, set it to the maximum value. + */ + public TimeValue wcet = TimeValue.MAX_VALUE; + + ////////////////////////////////////////////////////// + //// Public methods. + + /** + * Clear caches used in reporting dependentReactions() and dependsOnReactions(). This method + * should be called if any changes are made to triggers, sources, or effects. + * + * @param includingRuntimes If false, leave the runtime instances intact. This is useful for + * federated execution where levels are computed using the top-level connections, but then + * those connections are discarded. + */ + public void clearCaches(boolean includingRuntimes) { + dependentReactionsCache = null; + dependsOnReactionsCache = null; + if (includingRuntimes) runtimeInstances = null; + } + + /** + * Return the set of immediate downstream reactions, which are reactions that receive data + * produced by this reaction plus at most one reaction in the same reactor whose definition + * lexically follows this one. + */ + public Set dependentReactions() { + // Cache the result. + if (dependentReactionsCache != null) return dependentReactionsCache; + dependentReactionsCache = new LinkedHashSet<>(); + + // First, add the next lexical reaction, if appropriate. + if (parent.reactions.size() > index + 1) { + // Find the next reaction in the parent's reaction list. + dependentReactionsCache.add(parent.reactions.get(index + 1)); + } + + // Next, add reactions that get data from this one via a port. + for (TriggerInstance effect : effects) { + if (effect instanceof PortInstance) { + for (SendRange senderRange : ((PortInstance) effect).eventualDestinations()) { + for (RuntimeRange destinationRange : senderRange.destinations) { + dependentReactionsCache.addAll(destinationRange.instance.dependentReactions); + } + } + } + } + return dependentReactionsCache; + } + + /** + * Return the set of immediate upstream reactions, which are reactions that send data to this one + * plus at most one reaction in the same reactor whose definition immediately precedes the + * definition of this one + */ + public Set dependsOnReactions() { + // Cache the result. + if (dependsOnReactionsCache != null) return dependsOnReactionsCache; + dependsOnReactionsCache = new LinkedHashSet<>(); + + // First, add the previous lexical reaction, if appropriate. + if (index > 0) { + // Find the previous ordered reaction in the parent's reaction list. + int earlierIndex = index - 1; + ReactionInstance earlierOrderedReaction = parent.reactions.get(earlierIndex); + while (--earlierIndex >= 0) { + earlierOrderedReaction = parent.reactions.get(earlierIndex); + } + if (earlierIndex >= 0) { + dependsOnReactionsCache.add(parent.reactions.get(index - 1)); + } + } + + // Next, add reactions that send data to this one. + for (TriggerInstance source : sources) { + if (source instanceof PortInstance) { + // First, add reactions that send data through an intermediate port. + for (RuntimeRange senderRange : ((PortInstance) source).eventualSources()) { + dependsOnReactionsCache.addAll(senderRange.instance.dependsOnReactions); + } + // Then, add reactions that send directly to this port. + dependsOnReactionsCache.addAll(source.dependsOnReactions); + } + } + return dependsOnReactionsCache; + } + + /** + * Return the set of downstream reactions, which are reactions that receive data produced by this + * reaction, paired with an associated delay along a connection. + * + *

FIXME: Add caching. + * + *

FIXME: The use of `port.dependentPorts` here restricts the supported LF programs to a single + * hierarchy. More needs to be done to relax this. + * + *

FIXME: How to get the accumulated delays? + */ + public Map downstreamReactions() { + Map downstreamReactions = new HashMap<>(); + // Add reactions that get data from this one via a port, coupled with the + // delay value. + for (TriggerInstance effect : effects) { + if (effect instanceof PortInstance port) { + for (SendRange senderRange : port.eventualDestinationsWithAfterDelays()) { + Long delay = 0L; + if (senderRange.connection == null) continue; + var delayExpr = senderRange.connection.getDelay(); + if (delayExpr != null) delay = ASTUtils.getDelay(senderRange.connection.getDelay()); + for (RuntimeRange destinationRange : senderRange.destinations) { + for (var dependentReaction : destinationRange.instance.dependentReactions) { + downstreamReactions.put(dependentReaction, delay); + } + } + } + } + } + return downstreamReactions; + } + + /** + * Return a set of levels that runtime instances of this reaction have. A ReactionInstance may + * have more than one level if it lies within a bank and its dependencies on other reactions pass + * through multiports. + */ + public Set getLevels() { + Set result = new LinkedHashSet<>(); + // Force calculation of levels if it has not been done. + // FIXME: Comment out this as I think it is redundant. + // If it is NOT redundant then deadline propagation is not correct + // parent.assignLevels(); + for (Runtime runtime : runtimeInstances) { + result.add(runtime.level); + } + return result; + } + + /** + * Return a set of deadlines that runtime instances of this reaction have. A ReactionInstance may + * have more than one deadline if it lies within. + */ + public Set getInferredDeadlines() { + Set result = new LinkedHashSet<>(); + for (Runtime runtime : runtimeInstances) { + result.add(runtime.deadline); + } + return result; + } + + /** + * Return a list of levels that runtime instances of this reaction have. The size of this list is + * the total number of runtime instances. A ReactionInstance may have more than one level if it + * lies within a bank and its dependencies on other reactions pass through multiports. + */ + public List getLevelsList() { + List result = new LinkedList<>(); + // Force calculation of levels if it has not been done. + // FIXME: Comment out this as I think it is redundant. + // If it is NOT redundant then deadline propagation is not correct + // parent.assignLevels(); + for (Runtime runtime : runtimeInstances) { + result.add(runtime.level); + } + return result; + } + + /** + * Return a list of the deadlines that runtime instances of this reaction have. The size of this + * list is the total number of runtime instances. A ReactionInstance may have more than one + * deadline if it lies within + */ + public List getInferredDeadlinesList() { + List result = new LinkedList<>(); + for (Runtime runtime : runtimeInstances) { + result.add(runtime.deadline); + } + return result; + } + + /** + * Return the name of this reaction. + * + *

If the reaction is not named, this returns 'reaction_n', where n is replaced by the reaction + * index + 1. + * + * @return The name of this reaction. + */ + @Override + public String getName() { + if (StringExtensions.isNullOrEmpty(getDefinition().getName())) { + return "reaction_" + (this.index + 1); + } else { + return getDefinition().getName(); + } + } + + /** + * Return an array of runtime instances of this reaction in a natural order, defined as + * follows. The position within the returned list of the runtime instance is given by a + * mixed-radix number where the low-order digit is the bank index within the container reactor (or + * {@code 0} if it is not a bank), the second low order digit is the bank index of the container's + * container (or {@code 0} if it is not a bank), etc., until the container that is directly + * contained by the top level (the top-level reactor need not be included because its index is + * always {@code 0}). + * + *

The size of the returned array is the product of the widths of all the container {@link + * ReactorInstance} objects. If none of these is a bank, then the size will be {@code 1}. + * + *

This method creates this array the first time it is called, but then holds on to it. The + * array is used by {@link ReactionInstanceGraph} to determine and record levels and deadline for + * runtime instances of reactors. + */ + public List getRuntimeInstances() { + if (runtimeInstances != null) return runtimeInstances; + int size = parent.getTotalWidth(); + // If the width cannot be determined, assume there is only one instance. + if (size < 0) size = 1; + runtimeInstances = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Runtime r = new Runtime(i); + if (declaredDeadline != null) { + r.deadline = declaredDeadline.maxDelay; + } + runtimeInstances.add(r); + } + return runtimeInstances; + } + + /** Return a descriptive string. */ + @Override + public String toString() { + return getName() + " of " + parent.getFullName(); + } + + /** + * Determine logical execution time for each reaction during compile time based on immediate + * downstream logical delays (after delays and actions) and label each reaction with the minimum + * of all such delays. + */ + public TimeValue assignLogicalExecutionTime() { + if (this.let != null) { + return this.let; + } + + if (this.parent.isGeneratedDelay()) { + return this.let = TimeValue.ZERO; + } + + TimeValue let = null; + + // Iterate over effect and find minimum delay. + for (TriggerInstance effect : effects) { + if (effect instanceof PortInstance) { + var afters = + this.parent.getParent().children.stream() + .filter( + c -> { + if (c.isGeneratedDelay()) { + return c.inputs + .get(0) + .getDependsOnPorts() + .get(0) + .instance + .equals((PortInstance) effect); + } + return false; + }) + .map(c -> c.actions.get(0).getMinDelay()) + .min(TimeValue::compare); + + if (afters.isPresent()) { + if (let == null) { + let = afters.get(); + } else { + let = TimeValue.min(afters.get(), let); + } + } + } else if (effect instanceof ActionInstance) { + var action = ((ActionInstance) effect).getMinDelay(); + if (let == null) { + let = action; + } else { + let = TimeValue.min(action, let); + } + } + } + + if (let == null) { + let = TimeValue.ZERO; + } + return this.let = let; + } + + ////////////////////////////////////////////////////// + //// Private variables. + + /** Cache of the set of downstream reactions. */ + private Set dependentReactionsCache; + + /** Cache of the set of upstream reactions. */ + private Set dependsOnReactionsCache; + + /** + * Array of runtime instances of this reaction. This has length 1 unless the reaction is contained + * by one or more banks. Suppose that this reaction has depth 3, with full name r0.r1.r2.r. The + * top-level reactor is r0, which contains r1, which contains r2, which contains this reaction r. + * Suppose the widths of the containing reactors are w0, w1, and w2, and we are interested in the + * instance at bank indexes b0, b1, and b2. That instance is in this array at location given by + * the natural ordering, which is the mixed radix number b2%w2; b1%w1. + */ + private List runtimeInstances; + + private TimeValue let = null; + + /////////////////////////////////////////////////////////// + //// Inner classes + + public record SourcePort(PortInstance port, int index) {} + + /** Inner class representing a runtime instance of a reaction. */ + public class Runtime { + public TimeValue deadline; + // If this reaction instance depends on exactly one upstream + // reaction (via a port), then the "dominating" field will + // point to that upstream reaction. + public Runtime dominating; + + /** ID ranging from 0 to parent.getTotalWidth() - 1. */ + public final int id; + + public int level; + + /** The ports that directly or transitively send to this reaction. */ + public List sourcePorts = new ArrayList<>(); + + public ReactionInstance getReaction() { + return ReactionInstance.this; + } + + @Override + public String toString() { + String result = ReactionInstance.this + "(level: " + level; + if (deadline != null && deadline != TimeValue.MAX_VALUE) { + result += ", deadline: " + deadline; + } + if (dominating != null) { + result += ", dominating: " + dominating.getReaction(); + } + result += ")"; + return result; + } + + public Runtime(int id) { + this.dominating = null; + this.id = id; + this.level = 0; + if (ReactionInstance.this.declaredDeadline != null) { + this.deadline = ReactionInstance.this.declaredDeadline.maxDelay; + } else { + this.deadline = TimeValue.MAX_VALUE; + } + } + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/ReactionInstanceGraph.java b/lfc/core/src/main/java/org/lflang/generator/ReactionInstanceGraph.java new file mode 100644 index 000000000..ceb240453 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/ReactionInstanceGraph.java @@ -0,0 +1,504 @@ +/************* + * Copyright (c) 2021, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import java.util.*; +import java.util.stream.Collectors; +import org.lflang.generator.ReactionInstance.Runtime; +import org.lflang.generator.c.CUtil; +import org.lflang.graph.PrecedenceGraph; +import org.lflang.lf.Variable; + +/** + * This class analyzes the dependencies between reaction runtime instances. For each + * ReactionInstance, there may be more than one runtime instance because the ReactionInstance may be + * nested within one or more banks. In the worst case, of these runtime instances may have distinct + * dependencies, and hence distinct levels in the graph. Moreover, some of these instances may be + * involved in cycles while others are not. + * + *

Upon construction of this class, the runtime instances are created if necessary, stored each + * ReactionInstance, and assigned levels (maximum number of upstream reaction instances), deadlines, + * and single dominating reactions. + * + *

After creation, the resulting graph will be empty unless there are causality cycles, in which + * case, the resulting graph is a graph of runtime reaction instances that form cycles. + * + * @author Marten Lohstroh + * @author Edward A. Lee + */ +public class ReactionInstanceGraph extends PrecedenceGraph { + + /** + * Create a new graph by traversing the maps in the named instances embedded in the hierarchy of + * the program. + */ + public ReactionInstanceGraph(ReactorInstance main) { + this.main = main; + rebuild(); + } + + /////////////////////////////////////////////////////////// + //// Public fields + + /** The main reactor instance that this graph is associated with. */ + public final ReactorInstance main; + + /** + * Rebuild this graph by clearing and repeating the traversal that adds all the nodes and edges. + * Note that after this executes, the graph is empty unless it has causality cycles, in which case + * it contains only those causality cycles. + */ + private void rebuild() { + this.clear(); + addNodesAndEdges(main); + addEdgesForTpoLevels(main); + assignInferredDeadlines(); + + // Assign a level to each reaction. + // If there are cycles present in the graph, it will be detected here. + // This will destroy the graph, leaving only nodes in cycles. + assignLevels(); + // Do not throw an exception when nodeCount != 0 so that cycle visualization can proceed. + } + + /* + * Get an array of non-negative integers representing the number of reactions + * per each level, where levels are indices of the array. + */ + public Integer[] getNumReactionsPerLevel() { + return numReactionsPerLevel.toArray(new Integer[0]); + } + + /** Return the max breadth of the reaction dependency graph */ + public int getBreadth() { + var maxBreadth = 0; + for (Integer breadth : numReactionsPerLevel) { + if (breadth > maxBreadth) { + maxBreadth = breadth; + } + } + return maxBreadth; + } + + /////////////////////////////////////////////////////////// + //// Protected methods + + /** + * Add to the graph edges between the given reaction and all the reactions that depend on the + * specified port. + * + * @param port The port that the given reaction has as an effect. + * @param reaction The reaction to relate downstream reactions to. + */ + protected void addDownstreamReactions(PortInstance port, ReactionInstance reaction) { + // Use mixed-radix numbers to increment over the ranges. + List srcRuntimes = reaction.getRuntimeInstances(); + List eventualDestinations = port.eventualDestinations(); + + int srcDepth = (port.isInput()) ? 2 : 1; + + for (SendRange sendRange : eventualDestinations) { + for (RuntimeRange dstRange : sendRange.destinations) { + + int dstDepth = (dstRange.instance.isOutput()) ? 2 : 1; + MixedRadixInt dstRangePosition = dstRange.startMR(); + int dstRangeCount = 0; + + MixedRadixInt sendRangePosition = sendRange.startMR(); + int sendRangeCount = 0; + + while (dstRangeCount++ < dstRange.width) { + int srcIndex = sendRangePosition.get(srcDepth); + int dstIndex = dstRangePosition.get(dstDepth); + for (ReactionInstance dstReaction : dstRange.instance.dependentReactions) { + List dstRuntimes = dstReaction.getRuntimeInstances(); + Runtime srcRuntime = srcRuntimes.get(srcIndex); + Runtime dstRuntime = dstRuntimes.get(dstIndex); + // Only add this dependency if the reactions are not in modes at all or in the same mode + // or in modes of separate reactors + // This allows modes to break cycles since modes are always mutually exclusive. + if (srcRuntime.getReaction().getMode(true) == null + || dstRuntime.getReaction().getMode(true) == null + || srcRuntime.getReaction().getMode(true) == dstRuntime.getReaction().getMode(true) + || srcRuntime.getReaction().getParent() != dstRuntime.getReaction().getParent()) { + addEdge(dstRuntime, srcRuntime); + } + + // Propagate the deadlines, if any. + if (srcRuntime.deadline.compareTo(dstRuntime.deadline) > 0) { + srcRuntime.deadline = dstRuntime.deadline; + } + + // If this seems to be a single dominating reaction, set it. + // If another upstream reaction shows up, then this will be + // reset to null. + if (this.getUpstreamAdjacentNodes(dstRuntime).size() == 1 + && (dstRuntime.getReaction().index == 0)) { + dstRuntime.dominating = srcRuntime; + } else { + dstRuntime.dominating = null; + } + } + dstRangePosition.increment(); + sendRangePosition.increment(); + sendRangeCount++; + if (sendRangeCount >= sendRange.width) { + // Reset to multicast. + sendRangeCount = 0; + sendRangePosition = sendRange.startMR(); + } + } + } + } + } + + /** + * Build the graph by adding nodes and edges based on the given reactor instance. + * + * @param reactor The reactor on the basis of which to add nodes and edges. + */ + protected void addNodesAndEdges(ReactorInstance reactor) { + ReactionInstance previousReaction = null; + for (ReactionInstance reaction : reactor.reactions) { + List runtimes = reaction.getRuntimeInstances(); + + // Add reactions of this reactor. + for (Runtime runtime : runtimes) { + this.addNode(runtime); + } + + // If there is an earlier reaction in this same reactor, then + // create a link in the reaction graph for all runtime instances. + if (previousReaction != null) { + List previousRuntimes = previousReaction.getRuntimeInstances(); + int count = 0; + for (Runtime runtime : runtimes) { + // Only add the reaction order edge if previous reaction is outside of a mode or both are + // in the same mode + // This allows modes to break cycles since modes are always mutually exclusive. + if (runtime.getReaction().getMode(true) == null + || runtime.getReaction().getMode(true) == reaction.getMode(true)) { + this.addEdge(runtime, previousRuntimes.get(count)); + count++; + } + } + } + previousReaction = reaction; + + // Add downstream reactions. Note that this is sufficient. + // We don't need to also add upstream reactions because this reaction + // will be downstream of those upstream reactions. + for (TriggerInstance effect : reaction.effects) { + if (effect instanceof PortInstance) { + addDownstreamReactions((PortInstance) effect, reaction); + } + } + } + // Recursively add nodes and edges from contained reactors. + for (ReactorInstance child : reactor.children) { + addNodesAndEdges(child); + } + registerPortInstances(reactor); + } + + /** + * Add edges that encode the precedence relations induced by the TPO levels. TPO is total port + * order. See + * https://github.com/icyphy/lf-pubs/blob/54af48a97cc95058dbfb3333b427efb70294f66c/federated/TOMACS/paper.tex#L1353 + */ + private void addEdgesForTpoLevels(ReactorInstance main) { + var constrainedReactions = getConstrainedReactions(main); + for (var i : constrainedReactions.keySet()) { + var nextKey = constrainedReactions.higherKey(i); + if (nextKey == null) continue; + for (var r : constrainedReactions.get(i)) { + for (var rr : constrainedReactions.get(nextKey)) { + addEdge(rr, r); + } + } + } + } + + /** + * Get those reactions contained directly or transitively by the children of {@code main} whose + * TPO levels are specified. + * + * @return A map from TPO levels to reactions that are constrained to have the TPO levels. + */ + private NavigableMap> getConstrainedReactions(ReactorInstance main) { + NavigableMap> constrainedReactions = new TreeMap<>(); + for (var child : main.children) { + if (child.tpoLevel != null) { + if (!constrainedReactions.containsKey(child.tpoLevel)) { + constrainedReactions.put(child.tpoLevel, new ArrayList<>()); + } + getAllContainedReactions(constrainedReactions.get(child.tpoLevel), child); + } + } + return constrainedReactions; + } + + /** Add all reactions contained directly or transitively by {@code r}. */ + private void getAllContainedReactions(List runtimeReactions, ReactorInstance r) { + for (var reaction : r.reactions) { + runtimeReactions.addAll(reaction.getRuntimeInstances()); + } + for (var child : r.children) getAllContainedReactions(runtimeReactions, child); + } + + /////////////////////////////////////////////////////////// + //// Private fields + + /** + * Number of reactions per level, represented as a list of integers where the indices are the + * levels. + */ + private final List numReactionsPerLevel = new ArrayList<>(List.of(0)); + + /////////////////////////////////////////////////////////// + //// Private methods + + /** A port and an index of a reaction relative to the port. */ + public record MriPortPair(MixedRadixInt index, PortInstance port) {} + + /** + * For each port in {@code reactor}, add that port to its downstream reactions, together with the + * {@code MixedRadixInt} that is the index of the downstream reaction relative to the port and the + * intervening ports. + */ + private void registerPortInstances(ReactorInstance reactor) { + var allPorts = new ArrayList(); + allPorts.addAll(reactor.inputs); + allPorts.addAll(reactor.outputs); + for (var port : allPorts) { + List eventualDestinations = port.eventualDestinations(); + + for (SendRange sendRange : eventualDestinations) { + for (RuntimeRange dstRange : sendRange.destinations) { + + int dstDepth = (dstRange.instance.isOutput()) ? 2 : 1; + MixedRadixInt dstRangePosition = dstRange.startMR(); + int dstRangeCount = 0; + + MixedRadixInt sendRangePosition = sendRange.startMR(); + int sendRangeCount = 0; + + while (dstRangeCount++ < dstRange.width) { + int dstIndex = dstRangePosition.get(dstDepth); + for (ReactionInstance dstReaction : dstRange.instance.dependentReactions) { + List dstRuntimes = dstReaction.getRuntimeInstances(); + Runtime dstRuntime = dstRuntimes.get(dstIndex); + dstRuntime.sourcePorts.add(new MriPortPair(sendRangePosition.copy(), port)); + } + dstRangePosition.increment(); + sendRangePosition.increment(); + sendRangeCount++; + if (sendRangeCount >= sendRange.width) { + // Reset to multicast. + sendRangeCount = 0; + sendRangePosition = sendRange.startMR(); + } + } + } + } + } + } + + /** + * Analyze the dependencies between reactions and assign each reaction instance a level. This + * method removes nodes from this graph as it assigns levels. Any remaining nodes are part of + * causality cycles. + * + *

This procedure is based on Kahn's algorithm for topological sorting. Rather than + * establishing a total order, we establish a partial order. In this order, the level of each + * reaction is the least upper bound of the levels of the reactions it depends on. + */ + private void assignLevels() { + List start = new ArrayList<>(rootNodes()); + + // All root nodes start with level 0. + for (Runtime origin : start) { + origin.level = 0; + } + + // No need to do any of this if there are no root nodes; + // the graph must be cyclic. + while (!start.isEmpty()) { + Runtime origin = start.remove(0); + Set toRemove = new LinkedHashSet<>(); + Set downstreamAdjacentNodes = getDownstreamAdjacentNodes(origin); + + // Visit effect nodes. + for (Runtime effect : downstreamAdjacentNodes) { + // Stage edge between origin and effect for removal. + toRemove.add(effect); + + // Update level of downstream node. + effect.level = origin.level + 1; + } + // Remove visited edges. + for (Runtime effect : toRemove) { + removeEdge(effect, origin); + // If the effect node has no more incoming edges, + // then move it in the start set. + if (getUpstreamAdjacentNodes(effect).isEmpty()) { + start.add(effect); + } + } + + // Remove visited origin. + removeNode(origin); + assignPortLevel(origin); + + // Update numReactionsPerLevel info + incrementNumReactionsPerLevel(origin.level); + } + } + + /** + * Update the level of the source ports of {@code current} to be at most that of {@code current}. + */ + private void assignPortLevel(Runtime current) { + for (var sp : current.sourcePorts) { + sp.port().hasDependentReactionWithLevel(sp.index(), current.level); + } + } + + /** + * This function assigns inferred deadlines to all the reactions in the graph. It is modeled after + * {@code assignLevels} but it starts at the leaf nodes, but it does not destroy the graph. + */ + private void assignInferredDeadlines() { + List start = new ArrayList<>(leafNodes()); + Set visited = new HashSet<>(); + + // All leaf nodes have deadline initialized to their declared deadline or MAX_VALUE + while (!start.isEmpty()) { + Runtime origin = start.remove(0); + visited.add(origin); + Set upstreamAdjacentNodes = getUpstreamAdjacentNodes(origin); + + // Visit upstream nodes. + for (Runtime upstream : upstreamAdjacentNodes) { + // If the upstream node has been visited, then we have a cycle, which will be + // an error condition. Skip it. + if (visited.contains(upstream)) continue; + // Update deadline of upstream node if origins deadline is earlier. + if (origin.deadline.isEarlierThan(upstream.deadline)) { + upstream.deadline = origin.deadline; + } + // Determine whether the upstream node is now a leaf node. + var isLeaf = true; + for (Runtime downstream : getDownstreamAdjacentNodes(upstream)) { + if (!visited.contains(downstream)) isLeaf = false; + } + if (isLeaf) start.add(upstream); + } + } + } + + /** + * Adjust {@link #numReactionsPerLevel} at index level by + * adding to the previously recorded number valueToAdd. + * If there is no previously recorded number for this level, then + * create one with index level and value valueToAdd. + * @param level The level. + */ + private void incrementNumReactionsPerLevel(int level) { + if (numReactionsPerLevel.size() > level) { + numReactionsPerLevel.set(level, numReactionsPerLevel.get(level) + 1); + } else { + while (numReactionsPerLevel.size() < level) { + numReactionsPerLevel.add(0); + } + numReactionsPerLevel.add(1); + } + } + + /** Return the DOT (GraphViz) representation of the graph. */ + @Override + public String toDOT() { + var dotRepresentation = new CodeBuilder(); + var edges = new StringBuilder(); + + // Start the digraph with a left-write rank + dotRepresentation.pr( + """ + digraph { + rankdir=LF; + graph [compound=True, rank=LR, rankdir=LR]; + node [fontname=Times, shape=rectangle]; + edge [fontname=Times]; + """); + + var nodes = nodes(); + // Group nodes by levels + var groupedNodes = nodes.stream().collect(Collectors.groupingBy(it -> it.level)); + + dotRepresentation.indent(); + // For each level + for (var level : groupedNodes.keySet()) { + // Create a subgraph + dotRepresentation.pr("subgraph cluster_level_" + level + " {"); + dotRepresentation.pr(" graph [compound=True, label = \"level " + level + "\"];"); + + // Get the nodes at the current level + var currentLevelNodes = groupedNodes.get(level); + for (var node : currentLevelNodes) { + // Draw the node + var label = + CUtil.getName(node.getReaction().getParent().tpr) + "." + node.getReaction().getName(); + // Need a positive number to name the nodes in GraphViz + var labelHashCode = label.hashCode() & 0xfffffff; + dotRepresentation.pr(" node_" + labelHashCode + " [label=\"" + label + "\"];"); + + // Draw the edges + var downstreamNodes = getDownstreamAdjacentNodes(node); + for (var downstreamNode : downstreamNodes) { + var downstreamLabel = + CUtil.getName(downstreamNode.getReaction().getParent().tpr) + + "." + + downstreamNode.getReaction().getName(); + edges + .append(" node_") + .append(labelHashCode) + .append(" -> node_") + .append(downstreamLabel.hashCode() & 0xfffffff) + .append(";\n"); + } + } + // Close the subgraph + dotRepresentation.pr("}"); + } + dotRepresentation.unindent(); + // Add the edges to the definition of the graph at the bottom + dotRepresentation.pr(edges); + // Close the digraph + dotRepresentation.pr("}"); + + // Return the DOT representation + return dotRepresentation.toString(); + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/ReactorInstance.java b/lfc/core/src/main/java/org/lflang/generator/ReactorInstance.java new file mode 100644 index 000000000..db4b301d2 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/ReactorInstance.java @@ -0,0 +1,1206 @@ +/************* + * Copyright (c) 2019-2022, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import static org.lflang.AttributeUtils.isEnclave; +import static org.lflang.ast.ASTUtils.getLiteralTimeValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.lflang.MessageReporter; +import org.lflang.TimeValue; +import org.lflang.ast.ASTUtils; +import org.lflang.generator.TriggerInstance.BuiltinTriggerVariable; +import org.lflang.generator.c.TypeParameterizedReactor; +import org.lflang.lf.Action; +import org.lflang.lf.Assignment; +import org.lflang.lf.BuiltinTrigger; +import org.lflang.lf.BuiltinTriggerRef; +import org.lflang.lf.Connection; +import org.lflang.lf.Expression; +import org.lflang.lf.Initializer; +import org.lflang.lf.Input; +import org.lflang.lf.Instantiation; +import org.lflang.lf.Mode; +import org.lflang.lf.Output; +import org.lflang.lf.Parameter; +import org.lflang.lf.ParameterReference; +import org.lflang.lf.Port; +import org.lflang.lf.Reaction; +import org.lflang.lf.Reactor; +import org.lflang.lf.ReactorDecl; +import org.lflang.lf.StateVar; +import org.lflang.lf.Timer; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; +import org.lflang.lf.Watchdog; +import org.lflang.lf.WidthSpec; + +/** + * Representation of a compile-time instance of a reactor. If the reactor is instantiated as a bank + * of reactors, or if any of its parents is instantiated as a bank of reactors, then one instance of + * this ReactorInstance class represents all the runtime instances within these banks. The {@link + * #getTotalWidth()} method returns the number of such runtime instances, which is the product of + * the bank width of this reactor instance and the bank widths of all of its parents. There is + * exactly one instance of this ReactorInstance class for each graphical rendition of a reactor in + * the diagram view. + * + *

For the main reactor, which has no parent, once constructed, this object represents the entire + * Lingua Franca program. If the program has causality loops (a programming error), then {@link + * #hasCycles()} will return true and {@link #getCycles()} will return the ports and reaction + * instances involved in the cycles. + * + * @author Marten Lohstroh + * @author Edward A. Lee + */ +public class ReactorInstance extends NamedInstance { + + /** + * Create a new instantiation hierarchy that starts with the given top-level reactor. + * + * @param reactor The top-level reactor. + * @param reporter The error reporter. + */ + public ReactorInstance(Reactor reactor, MessageReporter reporter, List reactors) { + this(ASTUtils.createInstantiation(reactor), null, reporter, -1, reactors); + assert !reactors.isEmpty(); + } + + /** + * Create a new instantiation hierarchy that starts with the given top-level reactor. + * + * @param reactor The top-level reactor. + * @param reporter The error reporter. + */ + public ReactorInstance(Reactor reactor, MessageReporter reporter) { + this(ASTUtils.createInstantiation(reactor), null, reporter, -1, List.of()); + } + + /** + * Create a new instantiation hierarchy that starts with the given top-level reactor but only + * creates contained reactors up to the specified depth. + * + * @param reactor The top-level reactor. + * @param reporter The error reporter. + * @param desiredDepth The depth to which to go, or -1 to construct the full hierarchy. + */ + public ReactorInstance(Reactor reactor, MessageReporter reporter, int desiredDepth) { + this(ASTUtils.createInstantiation(reactor), null, reporter, desiredDepth, List.of()); + } + + /** + * Create a new instantiation with the specified parent. This constructor is here to allow for + * unit tests. It should not be used for any other purpose. + * + * @param reactor The top-level reactor. + * @param parent The parent reactor instance. + * @param reporter The error reporter. + */ + public ReactorInstance(Reactor reactor, ReactorInstance parent, MessageReporter reporter) { + this(ASTUtils.createInstantiation(reactor), parent, reporter, -1, List.of()); + } + + ////////////////////////////////////////////////////// + //// Public fields. + + /** The action instances belonging to this reactor instance. */ + public final List actions = new ArrayList<>(); + + /** + * The contained reactor instances, in order of declaration. For banks of reactors, this includes + * both the bank definition Reactor (which has bankIndex == -2) followed by each of the bank + * members (which have bankIndex >= 0). + */ + public final List children = new ArrayList<>(); + + /** The input port instances belonging to this reactor instance. */ + public final List inputs = new ArrayList<>(); + + /** The output port instances belonging to this reactor instance. */ + public final List outputs = new ArrayList<>(); + + /** The state variable instances belonging to this reactor instance. */ + public final List states = new ArrayList<>(); + + /** The parameters of this instance. */ + public final List parameters = new ArrayList<>(); + + /** List of reaction instances for this reactor instance. */ + public final List reactions = new ArrayList<>(); + + /** List of watchdog instances for this reactor instance. */ + public final List watchdogs = new ArrayList<>(); + + /** The timer instances belonging to this reactor instance. */ + public final List timers = new ArrayList<>(); + + /** The mode instances belonging to this reactor instance. */ + public final List modes = new ArrayList<>(); + + /** The reactor declaration in the AST. This is either an import or Reactor declaration. */ + public final ReactorDecl reactorDeclaration; + + /** The reactor after imports are resolve. */ + public final Reactor reactorDefinition; + + /** Indicator that this reactor has itself as a parent, an error condition. */ + public final boolean recursive; + + // An enclave object if this ReactorInstance is an enclave. null if not + public EnclaveInfo enclaveInfo = null; + public TypeParameterizedReactor tpr; + + /** + * The TPO level with which {@code this} was annotated, or {@code null} if there is no TPO + * annotation. TPO is total port order. See + * https://github.com/icyphy/lf-pubs/blob/54af48a97cc95058dbfb3333b427efb70294f66c/federated/TOMACS/paper.tex#L1353 + */ + public final Integer tpoLevel; + + ////////////////////////////////////////////////////// + //// Public methods. + + /** + * Assign levels to all reactions within the same root as this reactor. The level of a reaction r + * is equal to the length of the longest chain of reactions that must have the opportunity to + * execute before r at each tag. This returns a non-empty graph if a causality cycle exists. + * + *

This method uses a variant of Kahn's algorithm, which is linear in V + E, where V is the + * number of vertices (reactions) and E is the number of edges (dependencies between reactions). + * + * @return An empty graph if successful and otherwise a graph with runtime reaction instances that + * form cycles. + */ + public ReactionInstanceGraph assignLevels() { + if (depth != 0) return root().assignLevels(); + if (cachedReactionLoopGraph == null) { + cachedReactionLoopGraph = new ReactionInstanceGraph(this); + } + return cachedReactionLoopGraph; + } + + /** + * Return the instance of a child rector created by the specified definition or null if there is + * none. + * + * @param definition The definition of the child reactor ("new" statement). + */ + public ReactorInstance getChildReactorInstance(Instantiation definition) { + for (ReactorInstance child : this.children) { + if (child.definition == definition) { + return child; + } + } + return null; + } + + /** + * Clear any cached data in this reactor and its children. This is useful if a mutation has been + * realized. + */ + public void clearCaches() { + clearCaches(true); + } + + /** + * Clear any cached data in this reactor and its children. This is useful if a mutation has been + * realized. + * + * @param includingRuntimes If false, leave the runtime instances of reactions intact. This is + * useful for federated execution where levels are computed using the top-level connections, + * but then those connections are discarded. + */ + public void clearCaches(boolean includingRuntimes) { + if (includingRuntimes) cachedReactionLoopGraph = null; + for (ReactorInstance child : children) { + child.clearCaches(includingRuntimes); + } + for (PortInstance port : inputs) { + port.clearCaches(); + } + for (PortInstance port : outputs) { + port.clearCaches(); + } + for (ReactionInstance reaction : reactions) { + reaction.clearCaches(includingRuntimes); + } + cachedCycles = null; + } + + /** + * Return the set of ReactionInstance and PortInstance that form causality loops in the topmost + * parent reactor in the instantiation hierarchy. This will return an empty set if there are no + * causality loops. + */ + public Set> getCycles() { + if (depth != 0) return root().getCycles(); + if (cachedCycles != null) return cachedCycles; + cachedCycles = new LinkedHashSet<>(); + + ReactionInstanceGraph reactionRuntimes = assignLevels(); + if (reactionRuntimes.nodes().size() > 0) { + Set reactions = new LinkedHashSet<>(); + Set ports = new LinkedHashSet<>(); + // There are cycles. But the nodes set includes not + // just the cycles, but also nodes that are downstream of the + // cycles. Use Tarjan's algorithm to get just the cycles. + var cycleNodes = reactionRuntimes.getCycles(); + for (var cycle : cycleNodes) { + for (ReactionInstance.Runtime runtime : cycle) { + reactions.add(runtime.getReaction()); + } + } + // Need to figure out which ports are involved in the cycles. + // It may not be all ports that depend on this reaction. + for (ReactionInstance r : reactions) { + for (TriggerInstance p : r.effects) { + if (p instanceof PortInstance) { + findPaths((PortInstance) p, reactions, ports); + } + } + } + cachedCycles.addAll(reactions); + cachedCycles.addAll(ports); + } + + return cachedCycles; + } + + /** + * Return the specified input by name or null if there is no such input. + * + * @param name The input name. + */ + public PortInstance getInput(String name) { + for (PortInstance port : inputs) { + if (port.getName().equals(name)) { + return port; + } + } + return null; + } + + /** + * Override the base class to append [i_d], where d is the depth, if this reactor is in a bank of + * reactors. + * + * @return The name of this instance. + */ + @Override + public String getName() { + return this.definition.getName(); + } + + /** + * @see NamedInstance#uniqueID() + *

Append {@code _main} to the name of the main reactor to allow instantiations within that + * reactor to have the same name. + */ + @Override + public String uniqueID() { + if (this.isMainOrFederated()) { + if (reactorDefinition.isFederated() && !super.uniqueID().startsWith("federate__")) + return "federate__" + super.uniqueID() + "_main"; + return super.uniqueID() + "_main"; + } + return super.uniqueID(); + } + + /** + * Return the specified output by name or null if there is no such output. + * + * @param name The output name. + */ + public PortInstance getOutput(String name) { + for (PortInstance port : outputs) { + if (port.getName().equals(name)) { + return port; + } + } + return null; + } + + /** + * Return a parameter matching the specified name if the reactor has one and otherwise return + * null. + * + * @param name The parameter name. + */ + public ParameterInstance getParameter(String name) { + for (ParameterInstance parameter : parameters) { + if (parameter.getName().equals(name)) { + return parameter; + } + } + return null; + } + + /** Return the startup trigger or null if not used in any reaction. */ + public TriggerInstance getStartupTrigger() { + return builtinTriggers.get(BuiltinTrigger.STARTUP); + } + + /** Return the shutdown trigger or null if not used in any reaction. */ + public TriggerInstance getShutdownTrigger() { + return builtinTriggers.get(BuiltinTrigger.SHUTDOWN); + } + + /** + * If this reactor is a bank or any of its parents is a bank, return the total number of runtime + * instances, which is the product of the widths of all the parents. Return -1 if the width cannot + * be determined. + */ + public int getTotalWidth() { + return getTotalWidth(0); + } + + /** + * If this reactor is a bank or any of its parents is a bank, return the total number of runtime + * instances, which is the product of the widths of all the parents. Return -1 if the width cannot + * be determined. + * + * @param atDepth The depth at which to determine the width. Use 0 to get the total number of + * instances. Use 1 to get the number of instances within a single top-level bank member (this + * is useful for federates). + */ + public int getTotalWidth(int atDepth) { + if (width <= 0) return -1; + if (depth <= atDepth) return 1; + int result = width; + ReactorInstance p = parent; + while (p != null && p.depth > atDepth) { + if (p.width <= 0) return -1; + result *= p.width; + p = p.parent; + } + return result; + } + + /** + * Return the trigger instances (input ports, timers, and actions that trigger reactions) + * belonging to this reactor instance. + */ + public Set> getTriggers() { + // FIXME: Cache this. + var triggers = new LinkedHashSet>(); + for (ReactionInstance reaction : this.reactions) { + triggers.addAll(reaction.triggers); + } + return triggers; + } + + /** + * Return the trigger instances (input ports, timers, and actions that trigger reactions) together + * the ports that the reaction reads but that don't trigger it. + * + * @return The trigger instances belonging to this reactor instance. + */ + public Set> getTriggersAndReads() { + // FIXME: Cache this. + var triggers = new LinkedHashSet>(); + for (ReactionInstance reaction : this.reactions) { + triggers.addAll(reaction.triggers); + triggers.addAll(reaction.reads); + } + return triggers; + } + + /** Return true if the top-level parent of this reactor has causality cycles. */ + public boolean hasCycles() { + return assignLevels().nodeCount() != 0; + } + + /** + * Given a parameter definition for this reactor, return the initial integer value of the + * parameter. If the parameter is overridden when instantiating this reactor or any of its + * containing reactors, use that value. Otherwise, use the default value in the reactor + * definition. If the parameter cannot be found or its value is not an integer, return null. + * + * @param parameter The parameter definition (a syntactic object in the AST). + * @return An integer value or null. + */ + public Integer initialIntParameterValue(Parameter parameter) { + return ASTUtils.initialValueInt(parameter, instantiations()); + } + + public Expression resolveParameters(Expression e) { + return LfExpressionVisitor.dispatch(e, this, ParameterInliner.INSTANCE); + } + + private static final class ParameterInliner + extends LfExpressionVisitor.LfExpressionDeepCopyVisitor { + static final ParameterInliner INSTANCE = new ParameterInliner(); + + @Override + public Expression visitParameterRef(ParameterReference expr, ReactorInstance instance) { + if (!ASTUtils.belongsTo(expr.getParameter(), instance.definition)) { + throw new IllegalArgumentException( + "Parameter " + + expr.getParameter().getName() + + " is not a parameter of reactor instance " + + instance.getName() + + "."); + } + + Optional assignment = + instance.definition.getParameters().stream() + .filter(it -> it.getLhs().equals(expr.getParameter())) + .findAny(); // There is at most one + + if (assignment.isPresent()) { + // replace the parameter with its value. + Expression value = assignment.get().getRhs().getExpr(); + // recursively resolve parameters + return instance.getParent().resolveParameters(value); + } else { + // In that case use the default value. Default values + // cannot use parameter values, so they don't need to + // be recursively resolved. + Initializer init = expr.getParameter().getInit(); + Expression defaultValue = init.getExpr(); + if (defaultValue == null) { + // this is a problem + return super.visitParameterRef(expr, instance); + } + return defaultValue; + } + } + } + + /** + * Return a list of Instantiation objects for evaluating parameter values. The first object in the + * list is the AST Instantiation that created this reactor instance, the second is the AST + * instantiation that created the containing reactor instance, and so on until there are no more + * containing reactor instances. This will return an empty list if this reactor instance is at the + * top level (is main). + */ + public List instantiations() { + if (_instantiations == null) { + _instantiations = new ArrayList<>(); + if (definition != null) { + _instantiations.add(definition); + if (parent != null) { + _instantiations.addAll(parent.instantiations()); + } + } + } + return _instantiations; + } + + /** + * Returns true if this is a bank of reactors. + * + * @return true if a reactor is a bank, false otherwise + */ + public boolean isBank() { + return definition.getWidthSpec() != null; + } + + /** + * Returns whether this is a main or federated reactor. + * + * @return true if reactor definition is marked as main or federated, false otherwise. + */ + public boolean isMainOrFederated() { + return reactorDefinition != null + && (reactorDefinition.isMain() || reactorDefinition.isFederated()); + } + + /** + * Return true if the specified reactor instance is either equal to this reactor instance or a + * parent of it. + * + * @param r The reactor instance. + */ + public boolean isParent(ReactorInstance r) { + ReactorInstance p = this; + while (p != null) { + if (p == r) return true; + p = p.getParent(); + } + return false; + } + + /////////////////////////////////////////////////// + //// Methods for finding instances in this reactor given an AST node. + + /** + * Return the action instance within this reactor instance corresponding to the specified action + * reference. + * + * @param action The action as an AST node. + * @return The corresponding action instance or null if the action does not belong to this + * reactor. + */ + public ActionInstance lookupActionInstance(Action action) { + for (ActionInstance actionInstance : actions) { + if (actionInstance.definition == action) { + return actionInstance; + } + } + return null; + } + + /** + * Given a parameter definition, return the parameter instance corresponding to that definition, + * or null if there is no such instance. + * + * @param parameter The parameter definition (a syntactic object in the AST). + * @return A parameter instance, or null if there is none. + */ + public ParameterInstance lookupParameterInstance(Parameter parameter) { + for (ParameterInstance param : parameters) { + if (param.definition == parameter) { + return param; + } + } + return null; + } + + /** + * Given a port definition, return the port instance corresponding to that definition, or null if + * there is no such instance. + * + * @param port The port definition (a syntactic object in the AST). + * @return A port instance, or null if there is none. + */ + public PortInstance lookupPortInstance(Port port) { + // Search one of the inputs and outputs sets. + List ports = null; + if (port instanceof Input) { + ports = this.inputs; + } else if (port instanceof Output) { + ports = this.outputs; + } + for (PortInstance portInstance : ports) { + if (portInstance.definition == port) { + return portInstance; + } + } + return null; + } + + /** + * Given a reference to a port belonging to this reactor instance, return the port instance. + * Return null if there is no such instance. + * + * @param reference The port reference. + * @return A port instance, or null if there is none. + */ + public PortInstance lookupPortInstance(VarRef reference) { + if (!(reference.getVariable() instanceof Port)) { + // Trying to resolve something that is not a port + return null; + } + if (reference.getContainer() == null) { + // Handle local reference + return lookupPortInstance((Port) reference.getVariable()); + } else { + // Handle hierarchical reference + var containerInstance = getChildReactorInstance(reference.getContainer()); + if (containerInstance == null) return null; + return containerInstance.lookupPortInstance((Port) reference.getVariable()); + } + } + + /** + * Return the reaction instance within this reactor instance corresponding to the specified + * reaction. + * + * @param reaction The reaction as an AST node. + * @return The corresponding reaction instance or null if the reaction does not belong to this + * reactor. + */ + public ReactionInstance lookupReactionInstance(Reaction reaction) { + for (ReactionInstance reactionInstance : reactions) { + if (reactionInstance.definition == reaction) { + return reactionInstance; + } + } + return null; + } + + /** + * Return the reactor instance within this reactor that has the specified instantiation. Note that + * this may be a bank of reactors. Return null if there is no such reactor instance. + */ + public ReactorInstance lookupReactorInstance(Instantiation instantiation) { + for (ReactorInstance reactorInstance : children) { + if (reactorInstance.definition == instantiation) { + return reactorInstance; + } + } + return null; + } + + /** + * Return the timer instance within this reactor instance corresponding to the specified timer + * reference. + * + * @param timer The timer as an AST node. + * @return The corresponding timer instance or null if the timer does not belong to this reactor. + */ + public TimerInstance lookupTimerInstance(Timer timer) { + for (TimerInstance timerInstance : timers) { + if (timerInstance.definition == timer) { + return timerInstance; + } + } + return null; + } + + /** + * Returns the mode instance within this reactor instance corresponding to the specified mode + * reference. + * + * @param mode The mode as an AST node. + * @return The corresponding mode instance or null if the mode does not belong to this reactor. + */ + public ModeInstance lookupModeInstance(Mode mode) { + for (ModeInstance modeInstance : modes) { + if (modeInstance.definition == mode) { + return modeInstance; + } + } + return null; + } + + /** + * Return the watchdog instance within this reactor instance corresponding to the specified + * watchdog reference. + * + * @param watchdog The watchdog as an AST node. + * @return The corresponding watchdog instance or null if the watchdog does not belong to this + * reactor. + */ + public WatchdogInstance lookupWatchdogInstance(Watchdog watchdog) { + for (WatchdogInstance watchdogInstance : watchdogs) { + if (watchdogInstance.getDefinition() == watchdog) { + return watchdogInstance; + } + } + return null; + } + + /** Return a descriptive string. */ + @Override + public String toString() { + return "ReactorInstance " + getFullName(); + } + + /** + * Assuming that the given expression denotes a valid time, return a time value. + * + *

If the value is given as a parameter reference, this will look up the precise time value + * assigned to this reactor instance. + */ + public TimeValue getTimeValue(Expression expr) { + Expression resolved = resolveParameters(expr); + return getLiteralTimeValue(resolved); + } + + ////////////////////////////////////////////////////// + //// Protected fields. + + /** The generator that created this reactor instance. */ + protected MessageReporter reporter; // FIXME: This accumulates a lot of redundant references + + /** The map of used built-in triggers. */ + protected Map> builtinTriggers = + new HashMap<>(); + + /** The nested list of instantiations that created this reactor instance. */ + protected List _instantiations; + + ////////////////////////////////////////////////////// + //// Protected methods. + + /** + * Create all the reaction instances of this reactor instance and record the dependencies and + * antidependencies between ports, actions, and timers and reactions. This also records the + * dependencies between reactions that follows from the order in which they are defined. + */ + protected void createReactionInstances() { + List reactions = ASTUtils.allReactions(reactorDefinition); + if (reactions != null) { + int count = 0; + + // Check for startup and shutdown triggers. + for (Reaction reaction : reactions) { + // Create the reaction instance. + var reactionInstance = new ReactionInstance(reaction, this, count++); + // Add the reaction instance to the map of reactions for this + // reactor. + this.reactions.add(reactionInstance); + } + } + } + + /** Returns the built-in trigger or create a new one if none exists. */ + protected TriggerInstance getOrCreateBuiltinTrigger( + BuiltinTriggerRef trigger) { + return builtinTriggers.computeIfAbsent( + trigger.getType(), ref -> TriggerInstance.builtinTrigger(trigger, this)); + } + + /** Create all the watchdog instances of this reactor instance. */ + protected void createWatchdogInstances() { + List watchdogs = ASTUtils.allWatchdogs(reactorDefinition); + if (watchdogs != null) { + for (Watchdog watchdog : watchdogs) { + // Create the watchdog instance. + var watchdogInstance = new WatchdogInstance(watchdog, this); + + // Add the watchdog instance to the list of watchdogs for this + // reactor. + this.watchdogs.add(watchdogInstance); + } + } + } + + //////////////////////////////////////// + //// Private constructors + + /** + * Create a runtime instance from the specified definition and with the specified parent that + * instantiated it. + * + * @param definition The instantiation statement in the AST. + * @param parent The parent, or null for the main rector. + * @param reporter An error reporter. + * @param desiredDepth The depth to which to expand the hierarchy. + */ + public ReactorInstance( + Instantiation definition, + ReactorInstance parent, + MessageReporter reporter, + int desiredDepth, + List reactors) { + super(definition, parent); + this.tpoLevel = + definition.getAttributes().stream() + .filter(it -> it.getAttrName().equals("_tpoLevel")) + .map(it -> it.getAttrParms().stream().findAny().orElseThrow()) + .map(it -> Integer.parseInt(it.getValue())) + .findFirst() + .orElse(null); + this.reporter = reporter; + this.reactorDeclaration = definition.getReactorClass(); + this.reactorDefinition = ASTUtils.toDefinition(reactorDeclaration); + this.tpr = + parent == null + ? new TypeParameterizedReactor(definition, reactors) + : new TypeParameterizedReactor(definition, parent.tpr); + + // If this instance is an enclave (or the main reactor). Create an + // enclaveInfo object to track information about the enclave needed for + // later code-generation + if (isEnclave(definition) || this.isMainOrFederated()) { + enclaveInfo = new EnclaveInfo(this); + } + + // check for recursive instantiation + var currentParent = parent; + var foundSelfAsParent = false; + do { + if (currentParent != null) { + if (currentParent.reactorDefinition == this.reactorDefinition) { + foundSelfAsParent = true; + currentParent = null; // break + } else { + currentParent = currentParent.parent; + } + } + } while (currentParent != null); + + this.recursive = foundSelfAsParent; + if (recursive) { + reporter.at(definition).error("Recursive reactor instantiation."); + } + + // If the reactor definition is null, give up here. Otherwise, diagram generation + // will fail an NPE. + if (reactorDefinition == null) { + reporter.at(definition).error("Reactor instantiation has no matching reactor definition."); + return; + } + + setInitialWidth(); + + // Apply overrides and instantiate parameters for this reactor instance. + for (Parameter parameter : ASTUtils.allParameters(reactorDefinition)) { + this.parameters.add(new ParameterInstance(parameter, this)); + } + + // Instantiate inputs for this reactor instance. + for (Input inputDecl : ASTUtils.allInputs(reactorDefinition)) { + this.inputs.add(new PortInstance(inputDecl, this, reporter)); + } + + // Instantiate outputs for this reactor instance. + for (Output outputDecl : ASTUtils.allOutputs(reactorDefinition)) { + this.outputs.add(new PortInstance(outputDecl, this, reporter)); + } + + // Instantiate state variables for this reactor instance. + for (StateVar state : ASTUtils.allStateVars(reactorDefinition)) { + this.states.add(new StateVariableInstance(state, this, reporter)); + } + + // Do not process content (except interface above) if recursive. + if (!recursive && (desiredDepth < 0 || this.depth < desiredDepth)) { + // Instantiate children for this reactor instance. + // While doing this, assign an index offset to each. + for (Instantiation child : ASTUtils.allInstantiations(reactorDefinition)) { + var childInstance = new ReactorInstance(child, this, reporter, desiredDepth, reactors); + this.children.add(childInstance); + } + + // Instantiate timers for this reactor instance + for (Timer timerDecl : ASTUtils.allTimers(reactorDefinition)) { + this.timers.add(new TimerInstance(timerDecl, this)); + } + + // Instantiate actions for this reactor instance + for (Action actionDecl : ASTUtils.allActions(reactorDefinition)) { + this.actions.add(new ActionInstance(actionDecl, this)); + } + + createWatchdogInstances(); + + establishPortConnections(); + + // Create the reaction instances in this reactor instance. + // This also establishes all the implied dependencies. + // Note that this can only happen _after_ the children, + // port, action, and timer instances have been created. + createReactionInstances(); + + // Instantiate modes for this reactor instance + // This must come after the child elements (reactions, etc) of this reactor + // are created in order to allow their association with modes + for (Mode modeDecl : ASTUtils.allModes(reactorDefinition)) { + this.modes.add(new ModeInstance(modeDecl, this)); + } + for (ModeInstance mode : this.modes) { + mode.setupTranstions(); + } + } + } + + ////////////////////////////////////////////////////// + //// Private methods. + + /** + * Connect the given left port range to the given right port range. + * + *

NOTE: This method is public to enable its use in unit tests. Otherwise, it should be + * private. This is why it is defined here, in the section labeled "Private methods." + * + * @param src The source range. + * @param dst The destination range. + * @param connection The connection establishing this relationship. + */ + public static void connectPortInstances( + RuntimeRange src, RuntimeRange dst, Connection connection) { + SendRange range = new SendRange(src, dst, src._interleaved, connection); + src.instance.dependentPorts.add(range); + dst.instance.dependsOnPorts.add(src); + } + + /** + * Populate connectivity information in the port instances. Note that this can only happen _after_ + * the children and port instances have been created. Unfortunately, we have to do some + * complicated things here to support multiport-to-multiport, multiport-to-bank, and + * bank-to-multiport communication. The principle being followed is: in each connection statement, + * for each port instance on the left, connect to the next available port on the right. + */ + private void establishPortConnections() { + for (Connection connection : ASTUtils.allConnections(reactorDefinition)) { + List> leftPorts = + listPortInstances(connection.getLeftPorts(), connection); + Iterator> srcRanges = leftPorts.iterator(); + List> rightPorts = + listPortInstances(connection.getRightPorts(), connection); + Iterator> dstRanges = rightPorts.iterator(); + + // Check for empty lists. + if (!srcRanges.hasNext()) { + if (dstRanges.hasNext()) { + reporter.at(connection).warning("No sources to provide inputs."); + } + return; + } else if (!dstRanges.hasNext()) { + reporter.at(connection).warning("No destination. Outputs will be lost."); + return; + } + + RuntimeRange src = srcRanges.next(); + RuntimeRange dst = dstRanges.next(); + + while (true) { + if (dst.width <= -1 || src.width <= -1) { + // The width on one side or the other is not known. Make all possible connections. + connectPortInstances(src, dst, connection); + if (dstRanges.hasNext()) { + dst = dstRanges.next(); + } else if (srcRanges.hasNext()) { + src = srcRanges.next(); + } else { + break; + } + } else if (dst.width == src.width) { + connectPortInstances(src, dst, connection); + if (!dstRanges.hasNext()) { + if (srcRanges.hasNext()) { + // Should not happen (checked by the validator). + reporter + .at(connection) + .warning("Source is wider than the destination. Outputs will be lost."); + } + break; + } + if (!srcRanges.hasNext()) { + if (connection.isIterated()) { + srcRanges = leftPorts.iterator(); + } else { + if (dstRanges.hasNext()) { + // Should not happen (checked by the validator). + reporter + .at(connection) + .warning("Destination is wider than the source. Inputs will be missing."); + } + break; + } + } + dst = dstRanges.next(); + src = srcRanges.next(); + } else if (dst.width < src.width) { + // Split the left (src) range in two. + connectPortInstances(src.head(dst.width), dst, connection); + src = src.tail(dst.width); + if (!dstRanges.hasNext()) { + // Should not happen (checked by the validator). + reporter + .at(connection) + .warning("Source is wider than the destination. Outputs will be lost."); + break; + } + dst = dstRanges.next(); + } else if (src.width < dst.width) { + // Split the right (dst) range in two. + connectPortInstances(src, dst.head(src.width), connection); + dst = dst.tail(src.width); + if (!srcRanges.hasNext()) { + if (connection.isIterated()) { + srcRanges = leftPorts.iterator(); + } else { + reporter + .at(connection) + .warning("Destination is wider than the source. Inputs will be missing."); + break; + } + } + src = srcRanges.next(); + } + } + } + } + + /** + * If path exists from the specified port to any reaction in the specified set of reactions, then + * add the specified port and all ports along the path to the specified set of ports. + * + * @return True if the specified port was added. + */ + private boolean findPaths( + PortInstance port, Set reactions, Set ports) { + if (ports.contains(port)) return false; + boolean result = false; + for (ReactionInstance d : port.getDependentReactions()) { + if (reactions.contains(d)) ports.add(port); + result = true; + } + // Perform a depth-first search. + for (SendRange r : port.dependentPorts) { + for (RuntimeRange p : r.destinations) { + boolean added = findPaths(p.instance, reactions, ports); + if (added) { + result = true; + ports.add(port); + } + } + } + return result; + } + + /** + * Given a list of port references, as found on either side of a connection, return a list of the + * port instance ranges referenced. These may be multiports, and may be ports of a contained bank + * (a port representing ports of the bank members) so the returned list includes ranges of banks + * and channels. + * + *

If a given port reference has the form {@code interleaved(b.m)}, where {@code b} is a bank + * and {@code m} is a multiport, then the corresponding range in the returned list is marked + * interleaved. + * + *

For example, if {@code b} and {@code m} have width 2, without the interleaved keyword, the + * returned range represents the sequence {@code [b0.m0, b0.m1, b1.m0, b1.m1]}. With the + * interleaved marking, the returned range represents the sequence {@code [b0.m0, b1.m0, b0.m1, + * b1.m1]}. Both ranges will have width 4. + * + * @param references The variable references on one side of the connection. + * @param connection The connection. + */ + private List> listPortInstances( + List references, Connection connection) { + List> result = new ArrayList<>(); + List> tails = new LinkedList<>(); + int count = 0; + for (VarRef portRef : references) { + // Simple error checking first. + if (!(portRef.getVariable() instanceof Port)) { + reporter.at(portRef).error("Not a port."); + return result; + } + // First, figure out which reactor we are dealing with. + // The reactor we want is the container of the port. + // If the port reference has no container, then the reactor is this one. + var reactor = this; + if (portRef.getContainer() != null) { + reactor = getChildReactorInstance(portRef.getContainer()); + } + // The reactor can be null only if there is an error in the code. + // Skip this portRef so that diagram synthesis can complete. + if (reactor != null) { + PortInstance portInstance = reactor.lookupPortInstance((Port) portRef.getVariable()); + + Set interleaved = new LinkedHashSet<>(); + if (portRef.isInterleaved()) { + // NOTE: Here, we are assuming that the interleaved() + // keyword is only allowed on the multiports contained by + // contained reactors. + interleaved.add(portInstance.parent); + } + RuntimeRange range = new RuntimeRange.Port(portInstance, interleaved); + // If this portRef is not the last one in the references list + // then we have to check whether the range can be incremented at + // the lowest two levels (port and container). If not, + // split the range and add the tail to list to iterate over again. + // The reason for this is that the connection has only local visibility, + // but the range width may be reflective of bank structure higher + // in the hierarchy. + if (count < references.size() - 1) { + int portWidth = portInstance.width; + int portParentWidth = portInstance.parent.width; + // If the port is being connected on the inside and there is + // more than one port in the list, then we can only connect one + // bank member at a time. + if (reactor == this && references.size() > 1) { + portParentWidth = 1; + } + int widthBound = portWidth * portParentWidth; + + // If either of these widths cannot be determined, assume infinite. + if (portWidth < 0) widthBound = Integer.MAX_VALUE; + if (portParentWidth < 0) widthBound = Integer.MAX_VALUE; + + if (widthBound < range.width) { + // Need to split the range. + tails.add(range.tail(widthBound)); + range = range.head(widthBound); + } + } + result.add(range); + } + } + // Iterate over the tails. + while (tails.size() > 0) { + List> moreTails = new LinkedList<>(); + count = 0; + for (RuntimeRange tail : tails) { + if (count < tails.size() - 1) { + int widthBound = tail.instance.width; + if (tail._interleaved.contains(tail.instance.parent)) { + widthBound = tail.instance.parent.width; + } + // If the width cannot be determined, assume infinite. + if (widthBound < 0) widthBound = Integer.MAX_VALUE; + + if (widthBound < tail.width) { + // Need to split the range again + moreTails.add(tail.tail(widthBound)); + tail = tail.head(widthBound); + } + } + result.add(tail); + } + tails = moreTails; + } + return result; + } + + /** + * If this is a bank of reactors, set the width. It will be set to -1 if it cannot be determined. + */ + private void setInitialWidth() { + WidthSpec widthSpec = definition.getWidthSpec(); + if (widthSpec != null) { + // We need the instantiations list of the containing reactor, + // not this one. + width = ASTUtils.width(widthSpec, parent.instantiations()); + } + } + + ////////////////////////////////////////////////////// + //// Private fields. + + /** Cached set of reactions and ports that form a causality loop. */ + private Set> cachedCycles; + + /** Cached reaction graph containing reactions that form a causality loop. */ + private ReactionInstanceGraph cachedReactionLoopGraph = null; + + /** + * Return true if this is a generated delay reactor that originates from an "after" delay on a + * connection. + * + * @return True if this is a generated delay, false otherwise. + */ + public boolean isGeneratedDelay() { + // FIXME: hacky string matching again... + return this.definition + .getReactorClass() + .getName() + .contains(DelayBodyGenerator.GEN_DELAY_CLASS_NAME); + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/RuntimeRange.java b/lfc/core/src/main/java/org/lflang/generator/RuntimeRange.java new file mode 100644 index 000000000..8f2997e7e --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/RuntimeRange.java @@ -0,0 +1,452 @@ +/* A representation of a range of runtime instances for a NamedInstance. */ + +/* +Copyright (c) 2019-2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Class representing a range of runtime instance objects (port channels, reactors, reactions, + * etc.). This class and its derived classes have the most detailed information about the structure + * of a Lingua Franca program. There are three levels of detail: + * + *

    + *
  • The abstract syntax tree (AST). + *
  • The compile-time instance graph (CIG). + *
  • The runtime instance graph (RIG). + *
+ * + * In the AST, each reactor class is represented once. In the CIG, each reactor class is represented + * as many times as it is instantiated, except that a bank has only one representation (as in the + * graphical rendition). Equivalently, each CIG node has a unique full name, even though it may + * represent many runtime instances. The CIG is represented by {@link NamedInstance} and its derived + * classes. In the RIG, each bank is expanded so each bank member and each port channel is + * represented. + * + *

In general, determining dependencies between reactions requires analysis at the level of the + * RIG. But a brute-force representation of the RIG can get very large, and for most programs it has + * a great deal of redundancy. In a fully detailed representation of the RIG, for example, a bank of + * width N that contains a bank of width M within which there is a reactor R with port P will have + * N*M runtime instances of the port. If the port is a multiport with width L, then there are N*M*L + * edges connected to instances of that port, each of which may go to a distinct set of other remote + * runtime port instances. + * + *

This class and its subclasses give a more compact representation of the RIG in most common + * cases where collections of runtime instances all have the same dependencies or dependencies form + * a range that can be represented compactly. + * + *

A RuntimeRange represents an adjacent set of RIG objects (port channels, reactions reactors). + * For example, it can represent port channels 2 through 5 in a multiport of width 10. The width in + * this case is 4. If such a port is contained by one or more banks of reactors, then channels 2 + * through 5 of one bank member form a contiguous range. If you want channels 2 through 5 of all + * bank members, then this needs to be represented with multiple ranges. + * + *

The maxWidth is the width of the instance multiplied by the widths of each of its containers. + * For example, if a port of width 4 is contained by a bank of width 2 that is then contained by a + * bank of width 3, then the maxWidth will be 2*3*4 = 24. + * + *

The function iterationOrder returns a list that includes the instance of this range and all + * its containers, except the top-level reactor (main or federated). The order of this list is the + * order in which an iteration over the RIG objects represented by this range should be iterated. If + * the instance is a PortInstance, then this order will depend on whether connections at any level + * of the hierarchy are interleaved. + * + *

The simplest Ranges are those where the corresponding CIG node represents only one runtime + * instance (its instance is not (deeply) within a bank and is not a multiport). In this case, the + * RuntimeRange and all the objects returned by iterationOrder will have width 1. + * + *

In a more complex instance, consider a bank A of width 2 that contains a bank B of width 2 + * that contains a port instance P with width 2. . There are a total of 8 instances of P, which we + * can name: + * + *

A0.B0.P0 A0.B0.P1 A0.B1.P0 A0.B1.P1 A1.B0.P0 A1.B0.P1 A1.B1.P0 A1.B1.P1 + * + *

If there is no interleaving, iterationOrder() returns [P, B, A], indicating that they should + * be iterated by incrementing the index of P first, then the index of B, then the index of A, as + * done above. + * + *

If the connection within B to port P is interleaved, then the order of iteration order will be + * [B, P, A], resulting in the list: + * + *

A0.B0.P0 A0.B1.P0 A0.B0.P1 A0.B1.P1 A1.B0.P0 A1.B1.P0 A1.B0.P1 A1.B1.P1 + * + *

If the connection within A to B is also interleaved, then the order will be [A, B, P], + * resulting in the list: + * + *

A0.B0.P0 A1.B0.P0 A0.B1.P0 A1.B1.P0 A0.B0.P1 A1.B0.P1 A0.B1.P1 A1.B1.P1 + * + *

Finally, if the connection within A to B is interleaved, but not the connection within B to P, + * then the order will be [A, P, B], resulting in the list: + * + *

A0.B0.P0 A1.B0.P0 A0.B0.P1 A1.B0.P1 A0.B1.P0 A1.B1.P0 A0.B1.P1 A1.B1.P1 + * + *

A RuntimeRange is a contiguous subset of one of the above lists, given by a start offset and a + * width that is less than or equal to maxWidth. + * + *

Each element of the above lists can be represented as a permuted mixed-radix (PMR) number, + * where the low-order digit has radix equal to the width of P, the second digit has radix equal to + * the width of B, and the final digit has radix equal to the width of A. Each PMR has a permutation + * vector that defines how to increment PMR number. This permutation vector is derived from the + * iteration order as follows. When there is no interleaving, the iteration order is [P, B, A], and + * the permutation vector is [0, 1, 2]. When there is interleaving, the permutation vector simply + * specifies the iteration order. For example, if the iteration order is [A, P, B], then the + * permutation vector is [2, 0, 1], indicating that digit 2 of the PMR (corresponding to A) should + * be incremented first, then digit 0 (for P), then digit 1 (for B). + * + *

For a RuntimeRange with width greater than 1, the head() and tail() functions split the range. + * + *

This class and subclasses are designed to be immutable. Modifications always return a new + * RuntimeRange. + * + * @author Edward A. Lee + */ +public class RuntimeRange> implements Comparable> { + + /** + * Create a new range representing the full width of the specified instance with no interleaving. + * The instances will be a list with the specified instance first, its parent next, and on up the + * hierarchy until the depth 1 parent (the top-level reactor is not included because it can never + * be a bank). + * + * @param instance The instance. + * @param interleaved A list of parents that are interleaved or null if none. + */ + public RuntimeRange(T instance, Set interleaved) { + this(instance, 0, 0, interleaved); + } + + /** + * Create a new range representing a range of the specified instance with no interleaving. The + * instances will be a list with the specified instance first, its parent next, and on up the + * hierarchy until the depth 1 parent (the top-level reactor is not included because it can never + * be a bank). + * + * @param instance The instance over which this is a range (port, reaction, etc.) + * @param start The starting index for the range. + * @param width The width of the range or 0 to specify the maximum possible width. + * @param interleaved A list of parents that are interleaved or null if none. + */ + public RuntimeRange(T instance, int start, int width, Set interleaved) { + this.instance = instance; + this.start = start; + if (interleaved != null) { + this._interleaved.addAll(interleaved); + } + + int maxWidth = instance.width; // Initial value. + NamedInstance parent = instance.parent; + while (parent.depth > 0) { + maxWidth *= parent.width; + parent = parent.parent; + } + this.maxWidth = maxWidth; + + if (width > 0 && width + start < maxWidth) { + this.width = width; + } else { + this.width = maxWidth - start; + } + } + + ////////////////////////////////////////////////////////// + //// Public variables + + /** The instance that this is a range of. */ + public final T instance; + + /** The start offset of this range. */ + public final int start; + + /** The maximum width of any range with this instance. */ + public final int maxWidth; + + /** The width of this range. */ + public final int width; + + ////////////////////////////////////////////////////////// + //// Public methods + + /** + * Compare ranges by first comparing their start offset, and then, if these are equal, comparing + * their widths. This comparison is meaningful only for ranges that have the same instances. Note + * that this can return 0 even if equals() does not return true. + */ + @Override + public int compareTo(RuntimeRange o) { + if (start < o.start) { + return -1; + } else if (start == o.start) { + return Integer.compare(width, o.width); + } else { + return 1; + } + } + + /** + * Return a new RuntimeRange that is identical to this range but with width reduced to the + * specified width. If the new width is greater than or equal to the width of this range, then + * return this range. If the newWidth is 0 or negative, return null. + * + * @param newWidth The new width. + */ + public RuntimeRange head(int newWidth) { + if (newWidth >= width) return this; + if (newWidth <= 0) return null; + return new RuntimeRange<>(instance, start, newWidth, _interleaved); + } + + /** + * Return the list of natural identifiers for the runtime instances in this + * range. Each returned identifier is an integer representation of the mixed-radix number [d0, ... + * , dn] with radices [w0, ... , wn], where d0 is the bank or channel index of this RuntimeRange's + * instance, which has width w0, and dn is the bank index of its topmost parent below the + * top-level (main) reactor, which has width wn. The depth of this RuntimeRange's instance, + * therefore, is n - 1. The order of the returned list is the order in which the runtime instances + * should be iterated. + */ + public List instances() { + List result = new ArrayList<>(width); + MixedRadixInt mr = startMR(); + int count = 0; + while (count++ < width) { + result.add(mr.get()); + mr.increment(); + } + return result; + } + + /** + * Return a list containing the instance for this range and all of its parents, not including the + * top level reactor, in the order in which their banks and multiport channels should be iterated. + * For each depth at which the connection is interleaved, that parent will appear before this + * instance in depth order (shallower to deeper). For each depth at which the connection is not + * interleaved, that parent will appear after this instance in reverse depth order (deeper to + * shallower). + */ + public List> iterationOrder() { + ArrayList> result = new ArrayList<>(); + result.add(instance); + ReactorInstance parent = instance.parent; + while (parent.depth > 0) { + if (_interleaved.contains(parent)) { + // Put the parent at the head of the list. + result.add(0, parent); + } else { + result.add(parent); + } + parent = parent.parent; + } + return result; + } + + /** + * Return a range that is the subset of this range that overlaps with the specified range or null + * if there is no overlap. + */ + public RuntimeRange overlap(RuntimeRange range) { + // If either width is unknown, return this range. + if (width < 0 || range.width < 0) return this; + if (!overlaps(range)) return null; + int newStart = Math.max(start, range.start); + int newEnd = Math.min(start + width, range.start + range.width); + return tail(newStart - start).head(newEnd - newStart); + } + + /** + * Return true if the specified range has the same instance as this range and the ranges overlap. + */ + public boolean overlaps(RuntimeRange range) { + if (!instance.equals(range.instance)) return false; + return start < range.start + range.width && start + width > range.start; + } + + /** + * Return a set of identifiers for runtime instances of a parent of this {@link RuntimeRange}'s + * instance {@code n} levels above this {@link RuntimeRange}'s instance. If {@code n == 1}, for + * example, then this return the identifiers for the parent ReactorInstance. + * + *

This returns a list of natural identifiers, as defined below, for the instances + * within the range. + * + *

The resulting list can be used to count the number of distinct runtime instances of this + * RuntimeRange's instance (using {@code n == 0}) or any of its parents that lie within the range + * and to provide an index into an array of runtime instances. + * + *

Each natural identifier is the integer value of a mixed-radix number defined as + * follows: + * + *

    + *
  • The low-order digit is the index of the runtime instance of {@code i} within its + * container. If the {@link NamedInstance} is a {@code PortInstance}, this will be the + * multiport channel or {@code 0} if it is not a multiport. If the NamedInstance is a + * ReactorInstance, then it will be the bank index or {@code 0} if the reactor is not a + * bank. The radix for this digit will be the multiport width or bank width or 1 if the + * NamedInstance is neither a multiport nor a bank. + *
  • The next digit will be the bank index of the container of the specified {@link + * NamedInstance} or {@code 0} if it is not a bank. + *
  • The remaining digits will be bank indices of containers up to but not including the + * top-level reactor (there is no point in including the top-level reactor because it is + * never a bank). + *
  • Each index that is returned can be used as an index into an array of runtime instances + * that is assumed to be in a natural order. + *
+ * + * @param n The number of levels up of the parent. This is required to be less than the depth of + * this RuntimeRange's instance or an exception will be thrown. + */ + public Set parentInstances(int n) { + Set result = new LinkedHashSet<>(width); + MixedRadixInt mr = startMR(); + int count = 0; + while (count++ < width) { + result.add(mr.get(n)); + mr.increment(); + } + return result; + } + + /** + * Return the nearest containing ReactorInstance for this instance. If this instance is a + * ReactorInstance, then return it. Otherwise, return its parent. + */ + public ReactorInstance parentReactor() { + if (instance instanceof ReactorInstance) { + return (ReactorInstance) instance; + } else { + return instance.getParent(); + } + } + + /** + * Return the permutation vector that indicates the order in which the digits of the permuted + * mixed-radix representations of indices in this range should be incremented. + */ + public List permutation() { + List result = new ArrayList<>(instance.depth); + // Populate the result with default zeros. + for (int i = 0; i < instance.depth; i++) { + result.add(0); + } + int count = 0; + for (NamedInstance i : iterationOrder()) { + result.set(count++, instance.depth - i.depth); + } + return result; + } + + /** + * Return the radixes vector containing the width of this instance followed by the widths of its + * containers, not including the top level, which always has radix 1 and value 0. + */ + public List radixes() { + List result = new ArrayList<>(instance.depth); + int width = instance.width; + // If the width cannot be determined, assume 1. + if (width < 0) width = 1; + result.add(width); + ReactorInstance p = instance.getParent(); + while (p != null && p.getDepth() > 0) { + width = p.getWidth(); + // If the width cannot be determined, assume 1. + if (width < 0) width = 1; + result.add(width); + p = p.getParent(); + } + return result; + } + + /** + * Return the start as a new permuted mixed-radix number. For any instance that is neither a + * multiport nor a bank, the corresponding digit will be 0. + */ + public MixedRadixInt startMR() { + MixedRadixInt result = new MixedRadixInt(null, radixes(), permutation()); + result.setMagnitude(start); + return result; + } + + /** + * Return a new range that represents the leftover elements starting at the specified offset + * relative to start. If start + offset is greater than or equal to the width, then this returns + * null. If this offset is 0 then this returns this range unmodified. + * + * @param offset The number of elements to consume. + */ + public RuntimeRange tail(int offset) { + if (offset == 0) return this; + if (offset >= width) return null; + return new RuntimeRange<>(instance, start + offset, width - offset, _interleaved); + } + + /** + * Toggle the interleaved status of the specified reactor, which is assumed to be a parent of the + * instance of this range. If it was previously interleaved, make it not interleaved and vice + * versa. This returns a new RuntimeRange. + * + * @param reactor The parent reactor at which to toggle interleaving. + */ + public RuntimeRange toggleInterleaved(ReactorInstance reactor) { + Set newInterleaved = new HashSet<>(_interleaved); + if (_interleaved.contains(reactor)) { + newInterleaved.remove(reactor); + } else { + newInterleaved.add(reactor); + } + return new RuntimeRange<>(instance, start, width, newInterleaved); + } + + @Override + public String toString() { + return instance.getFullName() + "(" + start + "," + width + ")"; + } + + ////////////////////////////////////////////////////////// + //// Protected variables + + /** Record of which levels are interleaved. */ + Set _interleaved = new HashSet<>(); + + ////////////////////////////////////////////////////////// + //// Public inner classes + + /** Special case of RuntimeRange for PortInstance. */ + public static class Port extends RuntimeRange { + public Port(PortInstance instance) { + super(instance, null); + } + + public Port(PortInstance instance, Set interleaved) { + super(instance, interleaved); + } + + public Port(PortInstance instance, int start, int width, Set interleaved) { + super(instance, start, width, interleaved); + } + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/SendRange.java b/lfc/core/src/main/java/org/lflang/generator/SendRange.java new file mode 100644 index 000000000..9562fb022 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/SendRange.java @@ -0,0 +1,311 @@ +/* Abstract class for ranges of NamedInstance. */ + +/* +Copyright (c) 2019-2021, The University of California at Berkeley. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package org.lflang.generator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.lflang.lf.Connection; + +/** + * Class representing a range of a port that sources data together with a list of destination ranges + * of other ports that should all receive the same data sent in this range. All ranges in the + * destinations list have widths that are an integer multiple N of this range but not necessarily + * the same start offsets. + * + *

This class and subclasses are designed to be immutable. Modifications always return a new + * RuntimeRange. + * + * @author Edward A. Lee + */ +public class SendRange extends RuntimeRange.Port { + + /** + * Create a new send range. + * + * @param instance The instance over which this is a range of. + * @param start The starting index. + * @param width The width. + * @param interleaved A list of parents that are interleaved or null if none. + * @param connection The connection that establishes this send or null if not unique or none. + */ + public SendRange( + PortInstance instance, + int start, + int width, + Set interleaved, + Connection connection) { + super(instance, start, width, interleaved); + this.connection = connection; + } + + /** + * Create a new send range representing sending from the specified src to the specified dst. This + * preserves the interleaved status of both the src and dst. + * + * @param src The source range. + * @param dst The destination range. + * @param interleaved A list of parents that are interleaved or null if none. + * @param connection The connection that establishes this send or null if not unique or none. + */ + public SendRange( + RuntimeRange src, + RuntimeRange dst, + Set interleaved, + Connection connection) { + super(src.instance, src.start, src.width, interleaved); + destinations.add(dst); + _interleaved.addAll(src._interleaved); + this.connection = connection; + } + + ////////////////////////////////////////////////////////// + //// Public variables + + /** The connection that establishes this relationship or null if not unique or none. */ + public final Connection connection; + + /** The list of destination ranges to which this broadcasts. */ + public final List> destinations = new ArrayList<>(); + + ////////////////////////////////////////////////////////// + //// Public methods + + /** + * Add a destination to the list of destinations of this range. If the width of the destination is + * not a multiple of the width of this range, throw an exception. + * + * @throws IllegalArgumentException If the width doesn't match. + */ + public void addDestination(RuntimeRange dst) { + if (dst.width % width != 0) { + // Throw an exception only if both widths are known. + if (dst.width >= 0 && width >= 0) { + throw new IllegalArgumentException( + "Destination range width is not a multiple of sender's width"); + } + } + destinations.add(dst); + // Void any precomputed number of destinations. + _numberOfDestinationReactors = -1; + } + + /** + * Override the base class to add additional comparisons so that ordering is never ambiguous. This + * means that sorting will be deterministic. Note that this can return 0 even if equals() does not + * return true. + */ + @Override + public int compareTo(RuntimeRange o) { + int result = super.compareTo(o); + if (result == 0) { + // Longer destination lists come first. + if (destinations.size() > ((SendRange) o).destinations.size()) { + return -1; + } else if (destinations.size() == ((SendRange) o).destinations.size()) { + return instance.getFullName().compareTo(o.instance.getFullName()); + } else { + return 1; + } + } + return result; + } + + /** + * Return the total number of destination reactors for this range. Specifically, this is the + * number of distinct runtime reactor instances that react to messages from this send range. + */ + public int getNumberOfDestinationReactors() { + if (_numberOfDestinationReactors < 0) { + // Has not been calculated before. Calculate now. + _numberOfDestinationReactors = 0; + Map> result = new HashMap<>(); + for (RuntimeRange destination : this.destinations) { + // The following set contains unique identifiers the parent reactors + // of destination ports. + Set parentIDs = destination.parentInstances(1); + Set previousParentIDs = result.get(destination.instance.parent); + if (previousParentIDs == null) { + result.put(destination.instance.parent, parentIDs); + } else { + previousParentIDs.addAll(parentIDs); + } + } + for (ReactorInstance parent : result.keySet()) { + _numberOfDestinationReactors += result.get(parent).size(); + } + } + return _numberOfDestinationReactors; + } + + /** + * Return a new SendRange that is identical to this range but with width reduced to the specified + * width. If the new width is greater than or equal to the width of this range, then return this + * range. If the newWidth is 0 or negative, return null. This overrides the base class to also + * apply head() to the destination list. + * + * @param newWidth The new width. + */ + @Override + public SendRange head(int newWidth) { + // NOTE: Cannot use the superclass because it returns a RuntimeRange, not a SendRange. + // Also, cannot return this without applying head() to the destinations. + if (newWidth <= 0) return null; + + SendRange result = new SendRange(instance, start, newWidth, _interleaved, connection); + + for (RuntimeRange destination : destinations) { + result.destinations.add(destination.head(newWidth)); + } + return result; + } + + /** + * Return a range that is the subset of this range that overlaps with the specified range or null + * if there is no overlap. + */ + @Override + public SendRange overlap(RuntimeRange range) { + if (!overlaps(range)) return null; + if (range.start == start && range.width == width) return this; + int newStart = Math.max(start, range.start); + int newEnd = Math.min(start + width, range.start + range.width); + int newWidth = newEnd - newStart; + SendRange result = new SendRange(instance, newStart, newWidth, _interleaved, connection); + result._interleaved.addAll(_interleaved); + for (RuntimeRange destination : destinations) { + // The destination width is a multiple of this range's width. + // If the multiple is greater than 1, then the destination needs to + // split into multiple destinations. + while (destination != null) { + int dstStart = destination.start + (newStart - start); + RuntimeRange.Port dst = + new RuntimeRange.Port( + destination.instance, dstStart, newWidth, destination._interleaved); + result.addDestination(dst); + destination = destination.tail(width); + } + } + return result; + } + + /** + * Return a new SendRange that represents the leftover elements starting at the specified offset. + * If the offset is greater than or equal to the width, then this returns null. If this offset is + * 0 then this returns this range unmodified. This overrides the base class to also apply tail() + * to the destination list. + * + * @param offset The number of elements to consume. + */ + @Override + public SendRange tail(int offset) { + // NOTE: Cannot use the superclass because it returns a RuntimeRange, not a SendRange. + // Also, cannot return this without applying tail() to the destinations. + if (offset >= width) return null; + SendRange result = + new SendRange(instance, start + offset, width - offset, _interleaved, connection); + + for (RuntimeRange destination : destinations) { + result.destinations.add(destination.tail(offset)); + } + return result; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(super.toString()); + result.append("->["); + List dsts = new LinkedList<>(); + for (RuntimeRange dst : destinations) { + dsts.add(dst.toString()); + } + result.append(String.join(", ", dsts)); + result.append("]"); + return result.toString(); + } + + ////////////////////////////////////////////////////////// + //// Protected methods + + /** + * Assuming that this SendRange is completely contained by one of the destinations of the + * specified srcRange, return a new SendRange where the send range is the subrange of the + * specified srcRange that overlaps with this SendRange and the destinations include all the + * destinations of this SendRange. If the assumption is not satisfied, throw an + * IllegalArgumentException. + * + *

If any parent of this range is marked interleaved and is also a parent of the specified + * srcRange, then that parent will be marked interleaved in the result. + * + * @param srcRange A new source range. + * @param srcRangeOffset An offset into the source range. + */ + protected SendRange newSendRange(SendRange srcRange, int srcRangeOffset) { + // Every destination of srcRange receives all channels of srcRange (multicast). + // Find which multicast destination overlaps with this srcRange. + for (RuntimeRange srcDestination : srcRange.destinations) { + RuntimeRange overlap = srcDestination.overlap(this); + if (overlap == null) continue; // Not this one. + + if (overlap.width == width) { + // Found an overlap that is completely contained. + // If this width is greater than the srcRange width, + // then assume srcRange is multicasting via this. + int newWidth = Math.min(width, srcRange.width); + // The interleaving of the result is the union of the two interleavings. + Set interleaving = new LinkedHashSet<>(); + interleaving.addAll(_interleaved); + interleaving.addAll(srcRange._interleaved); + SendRange result = + new SendRange( + srcRange.instance, + srcRange.start + srcRangeOffset, + newWidth, + interleaving, + connection); + for (RuntimeRange dst : destinations) { + result.addDestination(dst); + } + return result; + } + } + throw new IllegalArgumentException( + "Expected this SendRange " + + this + + " to be completely contained by a destination of " + + srcRange); + } + + ////////////////////////////////////////////////////////// + //// Private variables + + private int _numberOfDestinationReactors = -1; // Never access this directly. +} diff --git a/lfc/core/src/main/java/org/lflang/generator/StateVariableInstance.java b/lfc/core/src/main/java/org/lflang/generator/StateVariableInstance.java new file mode 100644 index 000000000..502825d19 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/StateVariableInstance.java @@ -0,0 +1,79 @@ +/** A data structure for a state variable. */ + +/************* + * Copyright (c) 2019-2022, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ +package org.lflang.generator; + +import org.lflang.MessageReporter; +import org.lflang.lf.StateVar; + +/** Representation of a compile-time instance of a state variable. */ +public class StateVariableInstance extends NamedInstance { + + /** + * Create a runtime instance from the specified definition and with the specified parent that + * instantiated it. + * + * @param definition The declaration in the AST. + * @param parent The parent. + */ + public StateVariableInstance(StateVar definition, ReactorInstance parent) { + this(definition, parent, null); + } + + /** + * Create a port instance from the specified definition and with the specified parent that + * instantiated it. + * + * @param definition The declaration in the AST. + * @param parent The parent. + * @param errorReporter An error reporter, or null to throw exceptions. + */ + public StateVariableInstance( + StateVar definition, ReactorInstance parent, MessageReporter errorReporter) { + super(definition, parent); + + if (parent == null) { + throw new NullPointerException("Cannot create a StateVariableInstance with no parent."); + } + } + + ////////////////////////////////////////////////////// + //// Public methods + + /** + * Return the name of this trigger. + * + * @return The name of this trigger. + */ + @Override + public String getName() { + return definition.getName(); + } + + @Override + public String toString() { + return "StateVariableInstance " + getFullName(); + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/TimerInstance.java b/lfc/core/src/main/java/org/lflang/generator/TimerInstance.java new file mode 100644 index 000000000..1f4626f5b --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/TimerInstance.java @@ -0,0 +1,88 @@ +/** Instance of a timer. */ + +/************* + * Copyright (c) 2019, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import org.lflang.TimeValue; +import org.lflang.lf.Timer; + +/** + * Instance of a timer. + * + * @author Marten Lohstroh + * @author Edward A. Lee + */ +public class TimerInstance extends TriggerInstance { + + /** The global default for offset. */ + public static final TimeValue DEFAULT_OFFSET = TimeValue.ZERO; + + /** The global default for period. */ + public static final TimeValue DEFAULT_PERIOD = TimeValue.ZERO; + + private TimeValue offset = DEFAULT_OFFSET; + + private TimeValue period = DEFAULT_PERIOD; + + /** + * Create a new timer instance. If the definition is null, then this is a startup timer. + * + * @param definition The AST definition, or null for startup. + * @param parent The parent reactor. + */ + public TimerInstance(Timer definition, ReactorInstance parent) { + super(definition, parent); + if (parent == null) { + throw new InvalidSourceException("Cannot create an TimerInstance with no parent."); + } + if (definition != null) { + if (definition.getOffset() != null) { + try { + this.offset = parent.getTimeValue(definition.getOffset()); + } catch (IllegalArgumentException ex) { + parent.reporter.at(definition.getOffset()).error("Invalid time."); + } + } + if (definition.getPeriod() != null) { + try { + this.period = parent.getTimeValue(definition.getPeriod()); + } catch (IllegalArgumentException ex) { + parent.reporter.at(definition.getPeriod()).error("Invalid time."); + } + } + } + } + + /** Get the value of the offset parameter. */ + public TimeValue getOffset() { + return offset; + } + + /** Get the value of the offset parameter. */ + public TimeValue getPeriod() { + return period; + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/TriggerInstance.java b/lfc/core/src/main/java/org/lflang/generator/TriggerInstance.java new file mode 100644 index 000000000..b4ddc35a1 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/TriggerInstance.java @@ -0,0 +1,170 @@ +/** Instance of a trigger (port, action, or timer). */ + +/************* + * Copyright (c) 2019, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator; + +import java.util.LinkedHashSet; +import java.util.Set; +import org.lflang.lf.BuiltinTrigger; +import org.lflang.lf.BuiltinTriggerRef; +import org.lflang.lf.TriggerRef; +import org.lflang.lf.Variable; +import org.lflang.lf.impl.VariableImpl; + +/** + * Instance of a trigger (port, action, or timer). + * + * @author Marten Lohstroh + * @author Edward A. Lee + * @author Alexander Schulz-Rosengarten + */ +public class TriggerInstance extends NamedInstance { + + /** + * Construct a new instance with the specified definition and parent. E.g., for a action instance, + * the definition is Action, and for a port instance, it is Port. These are nodes in the AST. This + * is protected because only subclasses should be constructed. + * + * @param definition The definition in the AST for this instance. + * @param parent The reactor instance that creates this instance. + */ + protected TriggerInstance(T definition, ReactorInstance parent) { + super(definition, parent); + } + + /** + * Construct a new instance for a special builtin trigger. + * + * @param trigger The actual trigger definition. + * @param parent The reactor instance that creates this instance. + */ + static TriggerInstance builtinTrigger( + BuiltinTriggerRef trigger, ReactorInstance parent) { + return new TriggerInstance<>(new BuiltinTriggerVariable(trigger), parent); + } + + ///////////////////////////////////////////// + //// Public Methods + + /** + * Return the reaction instances that are triggered or read by this trigger. If this port is an + * output, then the reaction instances belong to the parent of the port's parent. If the port is + * an input, then the reaction instances belong to the port's parent. + */ + public Set getDependentReactions() { + return dependentReactions; + } + + /** + * Return the reaction instances that may send data via this port. If this port is an input, then + * the reaction instance belongs to parent of the port's parent. If it is an output, the reaction + * instance belongs to the port's parent. + */ + public Set getDependsOnReactions() { + return dependsOnReactions; + } + + /** + * Return the name of this trigger. + * + * @return The name of this trigger. + */ + @Override + public String getName() { + return definition.getName(); + } + + /** Return true if this trigger is "shutdown". */ + public boolean isShutdown() { + return isBuiltInType(BuiltinTrigger.SHUTDOWN); + } + + /** Return true if this trigger is "startup"./ */ + public boolean isStartup() { + return isBuiltInType(BuiltinTrigger.STARTUP); + } + + /** Return true if this trigger is "startup"./ */ + public boolean isReset() { + return isBuiltInType(BuiltinTrigger.RESET); + } + + ///////////////////////////////////////////// + //// Private Methods + + private boolean isBuiltInType(BuiltinTrigger type) { + return this.definition instanceof BuiltinTriggerVariable + && ((BuiltinTriggerRef) ((BuiltinTriggerVariable) this.definition).definition) + .getType() + .equals(type); + } + + ///////////////////////////////////////////// + //// Protected Fields + + /** + * Reaction instances that are triggered or read by this trigger. If this port is an output, then + * the reaction instances belong to the parent of the port's parent. If the port is an input, then + * the reaction instances belong to the port's parent. + */ + Set dependentReactions = new LinkedHashSet<>(); + + /** + * Reaction instances that may send data via this port. If this port is an input, then the + * reaction instance belongs to parent of the port's parent. If it is an output, the reaction + * instance belongs to the port's parent. + */ + Set dependsOnReactions = new LinkedHashSet<>(); + + ///////////////////////////////////////////// + //// Special class for builtin triggers + + /** This class allows to have BuiltinTriggers represented by a Variable type. */ + public static class BuiltinTriggerVariable extends VariableImpl { + + /** The builtin trigger type represented by this variable. */ + public final BuiltinTrigger type; + + /** The actual TriggerRef definition in the AST. */ + public final TriggerRef definition; + + public BuiltinTriggerVariable(BuiltinTriggerRef trigger) { + this.type = trigger.getType(); + this.definition = trigger; + } + + @Override + public String getName() { + return this.type.name().toLowerCase(); + } + + @Override + public void setName(String newName) { + throw new UnsupportedOperationException( + this.getClass().getName() + " has an immutable name."); + } + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/WatchdogInstance.java b/lfc/core/src/main/java/org/lflang/generator/WatchdogInstance.java new file mode 100644 index 000000000..73abe02b7 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/WatchdogInstance.java @@ -0,0 +1,92 @@ +/** + * @file + * @author Benjamin Asch + * @author Edward A. Lee + * @copyright (c) 2023, The University of California at Berkeley License in BSD 2-clause + * @brief Instance of a watchdog + */ +package org.lflang.generator; + +import java.util.LinkedHashSet; +import java.util.Set; +import org.lflang.TimeValue; +import org.lflang.lf.Action; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; +import org.lflang.lf.Watchdog; + +/** + * Instance of a watchdog. Upon creation the actual delay is converted into a proper time value. If + * a parameter is referenced, it is looked up in the given (grand)parent reactor instance. + * + * @author Benjamin Asch + */ +public class WatchdogInstance extends TriggerInstance { + + /** Create a new watchdog instance associated with the given reactor instance. */ + public WatchdogInstance(Watchdog definition, ReactorInstance reactor) { + super(definition, reactor); + if (definition.getTimeout() != null) { + // Get the timeout value given in the watchdog declaration. + this.timeout = reactor.getTimeValue(definition.getTimeout()); + } else { + // NOTE: The grammar does not allow the timeout to be omitted, so this should not occur. + this.timeout = TimeValue.ZERO; + } + + this.name = definition.getName(); + this.definition = definition; + this.reactor = reactor; + for (VarRef effect : definition.getEffects()) { + Variable variable = effect.getVariable(); + if (variable instanceof Action) { + // Effect is an Action. + var actionInstance = reactor.lookupActionInstance((Action) variable); + if (actionInstance != null) this.effects.add(actionInstance); + } + // Otherwise, do nothing (effect is either a mode or an unresolved reference). + } + } + + ////////////////////////////////////////////////////// + //// Public methods. + + public String getName() { + return this.name; + } + + public Watchdog getDefinition() { + return this.definition; + } + + public TimeValue getTimeout() { + return this.timeout; + } + + public ReactorInstance getReactor() { + return this.reactor; + } + + @Override + public String toString() { + return "WatchdogInstance " + name + "(" + timeout.toString() + ")"; + } + + ////////////////////////////////////////////////////// + //// Public fields. + + /** The ports or actions that this reaction may write to. */ + public Set> effects = new LinkedHashSet<>(); + + ////////////////////////////////////////////////////// + //// Private fields. + + private final TimeValue timeout; + + private final String name; + + private final Watchdog definition; + + private final ReactorInstance reactor; +} diff --git a/lfc/core/src/main/java/org/lflang/generator/c/CFileConfig.java b/lfc/core/src/main/java/org/lflang/generator/c/CFileConfig.java new file mode 100644 index 000000000..db9447a83 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/c/CFileConfig.java @@ -0,0 +1,30 @@ +package org.lflang.generator.c; + +import java.io.IOException; +import java.nio.file.Path; +import org.eclipse.emf.ecore.resource.Resource; +import org.lflang.FileConfig; + +public class CFileConfig extends FileConfig { + private final Path includePath; + + public CFileConfig(Resource resource, Path srcGenBasePath, boolean useHierarchicalBin) + throws IOException { + super(resource, srcGenBasePath, useHierarchicalBin); + var includeDir = getOutPath().resolve("include"); + includePath = + !useHierarchicalBin + ? includeDir + : includeDir + .resolve(getOutPath().relativize(srcPath)) + .resolve(srcFile.getFileName().toString().split("\\.")[0]); + } + + public Path getIncludePath() { + return includePath; + } + + public String getRuntimeIncludePath() { + return "/lib/c/reactor-c/include"; + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/c/CMixedRadixGenerator.java b/lfc/core/src/main/java/org/lflang/generator/c/CMixedRadixGenerator.java new file mode 100644 index 000000000..86190cd02 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/c/CMixedRadixGenerator.java @@ -0,0 +1,21 @@ +package org.lflang.generator.c; + +public class CMixedRadixGenerator { + /** Standardized name for channel index variable for a source. */ + public static String sc = "src_channel"; + + /** Standardized name for bank index variable for a source. */ + public static String sb = "src_bank"; + + /** Standardized name for runtime index variable for a source. */ + public static String sr = "src_runtime"; + + /** Standardized name for channel index variable for a destination. */ + public static String dc = "dst_channel"; + + /** Standardized name for bank index variable for a destination. */ + public static String db = "dst_bank"; + + /** Standardized name for runtime index variable for a destination. */ + public static String dr = "dst_runtime"; +} diff --git a/lfc/core/src/main/java/org/lflang/generator/c/CUtil.java b/lfc/core/src/main/java/org/lflang/generator/c/CUtil.java new file mode 100644 index 000000000..c4a51910c --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/c/CUtil.java @@ -0,0 +1,913 @@ +/* Utilities for C code generation. */ + +/************* + * Copyright (c) 2019-2021, The University of California at Berkeley. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ***************/ + +package org.lflang.generator.c; + +import static org.lflang.AttributeUtils.isEnclave; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.stream.Collectors; +import org.lflang.FileConfig; +import org.lflang.InferredType; +import org.lflang.MessageReporter; +import org.lflang.ast.ASTUtils; +import org.lflang.generator.ActionInstance; +import org.lflang.generator.GeneratorCommandFactory; +import org.lflang.generator.LFGeneratorContext; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.TriggerInstance; +import org.lflang.lf.Parameter; +import org.lflang.lf.Port; +import org.lflang.lf.Reactor; +import org.lflang.lf.VarRef; +import org.lflang.lf.Variable; +import org.lflang.lf.WidthTerm; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.BuildCommandsProperty; +import org.lflang.util.FileUtil; +import org.lflang.util.LFCommand; + +/** + * A collection of utilities for C code generation. This class codifies the coding conventions for + * the C target code generator. I.e., it defines how variables are named and referenced. + * + * @author Edward A. Lee + */ +public class CUtil { + + /** + * Suffix that when appended to the name of a federated reactor yields the name of its + * corresponding RTI executable. + */ + public static final String RTI_BIN_SUFFIX = "_RTI"; + + /** + * Suffix that when appended to the name of a federated reactor yields the name of its + * corresponding distribution script. + */ + public static final String RTI_DISTRIBUTION_SCRIPT_SUFFIX = "_distribute.sh"; + + ////////////////////////////////////////////////////// + //// Public methods. + + /** + * Return a reference to the action struct of the specified action instance. This action_base_t + * struct is on the self struct. + * + * @param instance The action instance. + * @param runtimeIndex An optional index variable name to use to address runtime instances. + */ + public static String actionRef(ActionInstance instance, String runtimeIndex) { + return reactorRef(instance.getParent(), runtimeIndex) + "->_lf_" + instance.getName(); + } + + /** + * Return a default name of a variable to refer to the bank index of a reactor in a bank. This has + * the form uniqueID_i, where uniqueID is an identifier for the instance that is guaranteed to be + * different from the ID of any other instance in the program. If the instance is not a bank, + * return "0". + * + * @param instance A reactor instance. + */ + public static String bankIndex(ReactorInstance instance) { + if (!instance.isBank()) return "0"; + return bankIndexName(instance); + } + + /** + * Return a default name of a variable to refer to the bank index of a reactor in a bank. This has + * the form uniqueID_i, where uniqueID is an identifier for the instance that is guaranteed to be + * different from the ID of any other instance in the program. + * + * @param instance A reactor instance. + */ + public static String bankIndexName(ReactorInstance instance) { + return instance.uniqueID() + "_i"; + } + + /** + * Return a default name of a variable to refer to the channel index of a port in a bank. This has + * the form uniqueID_c where uniqueID is an identifier for the instance that is guaranteed to be + * different from the ID of any other instance in the program. If the port is not a multiport, + * then return the string "0". + */ + public static String channelIndex(PortInstance port) { + if (!port.isMultiport()) return "0"; + return channelIndexName(port); + } + + /** + * Return a default name of a variable to refer to the channel index of a port in a bank. This is + * has the form uniqueID_c where uniqueID is an identifier for the instance that is guaranteed to + * be different from the ID of any other instance in the program. + */ + public static String channelIndexName(PortInstance port) { + return port.uniqueID() + "_c"; + } + + /** + * Return the name of the reactor. A {@code _main} is appended to the name if the reactor is main + * (to allow for instantiations that have the same name as the main reactor or the .lf file). + */ + public static String getName(TypeParameterizedReactor reactor) { + String name = reactor.uniqueName(); + if (reactor.reactor().isMain()) { + return name + "_main"; + } + return name; + } + + /** Return the name used in the internal (non-user-facing) include guard for the given reactor. */ + public static String internalIncludeGuard(TypeParameterizedReactor tpr) { + final String headerName = CUtil.getName(tpr) + ".h"; + return headerName.toUpperCase().replace(".", "_"); + } + + /** + * Return a reference to the specified port. + * + *

The returned string will have one of the following forms: + * + *

    + *
  • {@code selfStructs[k]->_lf_portName} + *
  • {@code selfStructs[k]->_lf_portName} + *
  • {@code selfStructs[k]->_lf_portName[i]} + *
  • {@code selfStructs[k]->_lf_parent.portName} + *
  • {@code selfStructs[k]->_lf_parent.portName[i]} + *
  • {@code selfStructs[k]->_lf_parent[j].portName} + *
  • {@code selfStructs[k]->_lf_parent[j].portName[i]} + *
+ * + * where {@code k} is the runtime index of either the port's parent or the port's parent's parent, + * the latter when isNested is {@code true}. The index {@code j} is present if the parent is a + * bank, and the index {@code i} is present if the port is a multiport. + * + *

The first two forms are used if isNested is false, and the remaining four are used if + * isNested is true. Set {@code isNested} to {@code true} when referencing a port belonging to a + * contained reactor. + * + * @param port The port. + * @param isNested True to return a reference relative to the parent's parent. + * @param includeChannelIndex True to include the channel index at the end. + * @param runtimeIndex A variable name to use to index the runtime instance or null to use the + * default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the default, the + * string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to use the default, the + * string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + public static String portRef( + PortInstance port, + boolean isNested, + boolean includeChannelIndex, + String runtimeIndex, + String bankIndex, + String channelIndex) { + String channel = ""; + if (channelIndex == null) channelIndex = channelIndex(port); + if (port.isMultiport() && includeChannelIndex) { + channel = "[" + channelIndex + "]"; + } + if (isNested) { + return reactorRefNested(port.getParent(), runtimeIndex, bankIndex) + + "." + + port.getName() + + channel; + } else { + String sourceStruct = CUtil.reactorRef(port.getParent(), runtimeIndex); + return sourceStruct + "->_lf_" + port.getName() + channel; + } + } + + /** + * Return a reference to the port on the self struct of the port's parent. This is used when an + * input port triggers a reaction in the port's parent or when an output port is written to by a + * reaction in the port's parent. This is equivalent to calling {@code portRef(port, false, true, + * null, null)}. + * + * @param port An instance of the port to be referenced. + */ + public static String portRef(PortInstance port) { + return portRef(port, false, true, null, null, null); + } + + /** + * Return a reference to the port on the self struct of the port's parent using the specified + * index variables. This is used when an input port triggers a reaction in the port's parent or + * when an output port is written to by a reaction in the port's parent. This is equivalent to + * calling {@code portRef(port, false, true, bankIndex, channelIndex)}. + * + * @param port An instance of the port to be referenced. + * @param runtimeIndex A variable name to use to index the runtime instance or null to use the + * default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the default, the + * string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to use the default, the + * string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + public static String portRef( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex) { + return portRef(port, false, true, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return a reference to a port without any channel indexing. This is useful for deriving a + * reference to the _width variable. + * + * @param port An instance of the port to be referenced. + */ + public static String portRefName(PortInstance port) { + return portRef(port, false, false, null, null, null); + } + + /** + * Return the portRef without the channel indexing. This is useful for deriving a reference to the + * _width variable. + * + * @param port An instance of the port to be referenced. + * @param runtimeIndex A variable name to use to index the runtime instance or null to use the + * default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the default, the + * string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to use the default, the + * string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + public static String portRefName( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex) { + return portRef(port, false, false, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return a port reference to a port on the self struct of the parent of the port's parent. This + * is used when an input port is written to by a reaction in the parent of the port's parent, or + * when an output port triggers a reaction in the parent of the port's parent. This is equivalent + * to calling {@code portRef(port, true, true, null, null, null)}. + * + * @param port The port. + */ + public static String portRefNested(PortInstance port) { + return portRef(port, true, true, null, null, null); + } + + /** + * Return a reference to the port on the self struct of the parent of the port's parent. This is + * used when an input port is written to by a reaction in the parent of the port's parent, or when + * an output port triggers a reaction in the parent of the port's parent. This is equivalent to + * calling {@code portRef(port, true, true, runtimeIndex, bankIndex, channelIndex)}. + * + * @param port The port. + * @param runtimeIndex A variable name to use to index the runtime instance or null to use the + * default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the default, the + * string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to use the default, the + * string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + public static String portRefNested( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex) { + return portRef(port, true, true, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return a reference to the port on the self struct of the parent of the port's parent, but + * without the channel indexing, even if it is a multiport. This is used when an input port is + * written to by a reaction in the parent of the port's parent, or when an output port triggers a + * reaction in the parent of the port's parent. This is equivalent to calling {@code portRef(port, + * true, false, null, null, null)}. + * + * @param port The port. + */ + public static String portRefNestedName(PortInstance port) { + return portRef(port, true, false, null, null, null); + } + + /** + * Return a reference to the port on the self struct of the parent of the port's parent, but + * without the channel indexing, even if it is a multiport. This is used when an input port is + * written to by a reaction in the parent of the port's parent, or when an output port triggers a + * reaction in the parent of the port's parent. This is equivalent to calling {@code + * portRefNested(port, true, false, runtimeIndex, bankIndex, channelIndex)}. + * + * @param port The port. + * @param runtimeIndex A variable name to use to index the runtime instance or null to use the + * default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the default, the + * string returned by {@link CUtil#bankIndex(ReactorInstance)}. + * @param channelIndex A variable name to use to index the channel or null to use the default, the + * string returned by {@link CUtil#channelIndex(PortInstance)}. + */ + public static String portRefNestedName( + PortInstance port, String runtimeIndex, String bankIndex, String channelIndex) { + return portRef(port, true, false, runtimeIndex, bankIndex, channelIndex); + } + + /** + * Return code for referencing a port within a reaction body possibly indexed by a bank index + * and/or a multiport index. If the provided reference is not a port, then this returns the string + * "ERROR: not a port." + * + * @param reference The reference to the port. + * @param bankIndex A bank index or null or negative if not in a bank. + * @param multiportIndex A multiport index or null or negative if not in a multiport. + */ + public static String portRefInReaction( + VarRef reference, Integer bankIndex, Integer multiportIndex) { + if (!(reference.getVariable() instanceof Port)) { + return "ERROR: not a port."; // FIXME: This is not the fail-fast approach, and it seems + // arbitrary. + } + var prefix = ""; + if (reference.getContainer() != null) { + var bank = ""; + if (reference.getContainer().getWidthSpec() != null && bankIndex != null && bankIndex >= 0) { + bank = "[" + bankIndex + "]"; + } + prefix = reference.getContainer().getName() + bank + "."; + } + var multiport = ""; + if (((Port) reference.getVariable()).getWidthSpec() != null + && multiportIndex != null + && multiportIndex >= 0) { + multiport = "[" + multiportIndex + "]"; + } + return prefix + reference.getVariable().getName() + multiport; + } + + /** + * Return a reference to the reaction entry on the self struct of the parent of the specified + * reaction. + * + * @param reaction The reaction. + */ + public static String reactionRef(ReactionInstance reaction) { + return reactionRef(reaction, null); + } + + /** + * Return a reference to the reaction entry on the self struct of the parent of the specified + * reaction. + * + * @param reaction The reaction. + * @param runtimeIndex An index into the array of self structs for the parent. + */ + public static String reactionRef(ReactionInstance reaction, String runtimeIndex) { + return reactorRef(reaction.getParent(), runtimeIndex) + "->_lf__reaction_" + reaction.index; + } + + /** + * Return a reference to the "self" struct of the specified reactor instance. The returned string + * has the form self[j], where self is the name of the array of self structs for this reactor + * instance and j is the expression returned by {@link #runtimeIndex(ReactorInstance)} or 0 if + * there are no banks. + * + * @param instance The reactor instance. + */ + public static String reactorRef(ReactorInstance instance) { + return reactorRef(instance, null); + } + + /** + * Return the name of the array of "self" structs of the specified reactor instance. This is + * similar to {@link #reactorRef(ReactorInstance)} except that it does not index into the array. + * + * @param instance The reactor instance. + */ + public static String reactorRefName(ReactorInstance instance) { + return instance.uniqueID() + "_self"; + } + + /** + * Return a reference to the "self" struct of the specified reactor instance. The returned string + * has the form self[runtimeIndex], where self is the name of the array of self structs for this + * reactor instance. If runtimeIndex is null, then it is replaced by the expression returned by + * {@link #runtimeIndex(ReactorInstance)} or 0 if there are no banks. + * + * @param instance The reactor instance. + * @param runtimeIndex An optional expression to use to address bank members. If this is null, the + * expression used will be that returned by {@link #runtimeIndex(ReactorInstance)}. + */ + public static String reactorRef(ReactorInstance instance, String runtimeIndex) { + if (runtimeIndex == null) runtimeIndex = runtimeIndex(instance); + return reactorRefName(instance) + "[" + runtimeIndex + "]"; + } + + /** + * For situations where a reaction reacts to or reads from an output of a contained reactor or + * sends to an input of a contained reactor, then the container's self struct will have a field + * (or an array of fields if the contained reactor is a bank) that is a struct with fields + * corresponding to those inputs and outputs. This method returns a reference to that struct or + * array of structs. Note that the returned reference is not to the self struct of the contained + * reactor. Use {@link #reactorRef(ReactorInstance)} for that. + * + * @param reactor The contained reactor. + */ + public static String reactorRefNested(ReactorInstance reactor) { + return reactorRefNested(reactor, null, null); + } + + /** + * For situations where a reaction reacts to or reads from an output of a contained reactor or + * sends to an input of a contained reactor, then the container's self struct will have a field + * (or an array of fields if the contained reactor is a bank) that is a struct with fields + * corresponding to those inputs and outputs. This method returns a reference to that struct or + * array of structs. Note that the returned reference is not to the self struct of the contained + * reactor. Use {@link CUtil#reactorRef(ReactorInstance)} for that. + * + * @param reactor The contained reactor. + * @param runtimeIndex A variable name to use to index the runtime instance or null to use the + * default, the string returned by {@link CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex A variable name to use to index the bank or null to use the default, the + * string returned by {@link CUtil#bankIndex(ReactorInstance)}. + */ + public static String reactorRefNested( + ReactorInstance reactor, String runtimeIndex, String bankIndex) { + String result = reactorRef(reactor.getParent(), runtimeIndex) + "->_lf_" + reactor.getName(); + if (reactor.isBank()) { + // Need the bank index not the runtimeIndex. + if (bankIndex == null) bankIndex = bankIndex(reactor); + result += "[" + bankIndex + "]"; + } + return result; + } + + /** + * Return an expression that, when evaluated, gives the index of a runtime instance of the + * specified ReactorInstance. If the reactor is not within any banks, then this will return "0". + * Otherwise, it will return an expression that evaluates a mixed-radix number d0%w0, d1%w1, ... , + * dn%wn, where n is the depth minus one of the reactor. The radixes, w0 to wn, are the widths of + * this reactor, its parent reactor, on up to the top-level reactor. Since the top-level reactor + * is never a bank, dn = 0 and wn = 1. The digits, di, are either 0 (of the parent is not a bank) + * or the variable name returned by {@link #bankIndexName(ReactorInstance)} if the parent is a + * bank. The returned expression, when evaluated, will yield the following value: + * + *

+   *     d0 + w0 * (d1 + w1 * ( ... (dn-1 + wn-1 * dn) ... )
+   * 
+ * + * @param reactor The reactor. + */ + public static String runtimeIndex(ReactorInstance reactor) { + StringBuilder result = new StringBuilder(); + int width = 0; + int parens = 0; + while (reactor != null) { + if (reactor.isBank() && reactor.getWidth() > 1) { + if (width > 0) { + result.append(" + " + width + " * ("); + parens++; + } + result.append(bankIndexName(reactor)); + width = reactor.getWidth(); + } + reactor = reactor.getParent(); + } + while (parens-- > 0) { + result.append(")"); + } + if (result.length() == 0) return "0"; + return result.toString(); + } + + /** + * Return a unique type for the "self" struct of the specified reactor class from the reactor + * class. + * + * @param reactor The reactor class. + * @return The type of a self struct for the specified reactor class. + */ + public static String selfType(TypeParameterizedReactor reactor) { + if (reactor.reactor().isMain()) { + return CUtil.getName(reactor) + "_main_self_t"; + } + return CUtil.getName(reactor) + "_self_t"; + } + + /** Construct a unique type for the "self" struct of the class of the given reactor. */ + public static String selfType(ReactorInstance instance) { + return selfType(instance.tpr); + } + + /** + * Construct a unique type for the struct of the specified typed variable (port or action) of the + * specified reactor class. This is required to be the same as the type name returned by {@link + * #variableStructType(TriggerInstance)}. + */ + public static String variableStructType( + Variable variable, TypeParameterizedReactor tpr, boolean userFacing) { + return (userFacing ? tpr.getName().toLowerCase() : CUtil.getName(tpr)) + + "_" + + variable.getName() + + "_t"; + } + + /** + * Construct a unique type for the struct of the specified instance (port or action). This is + * required to be the same as the type name returned by {@link #variableStructType(Variable, + * TypeParameterizedReactor, boolean)}. + * + * @param portOrAction The port or action instance. + * @return The name of the self struct. + */ + public static String variableStructType(TriggerInstance portOrAction) { + return CUtil.getName(portOrAction.getParent().tpr) + "_" + portOrAction.getName() + "_t"; + } + + /** + * Return a reference to the trigger_t struct of the specified trigger instance (input port or + * action). This trigger_t struct is on the self struct. + * + * @param instance The port or action instance. + */ + public static String triggerRef(TriggerInstance instance) { + return triggerRef(instance, null); + } + + /** + * Return a reference to the trigger_t struct of the specified trigger instance (input port or + * action). This trigger_t struct is on the self struct. + * + * @param instance The port or action instance. + * @param runtimeIndex An optional index variable name to use to address runtime instances. + */ + public static String triggerRef( + TriggerInstance instance, String runtimeIndex) { + return reactorRef(instance.getParent(), runtimeIndex) + "->_lf__" + instance.getName(); + } + + /** + * Return a reference to the trigger_t struct for the specified port of a contained reactor. + * + * @param port The output port of a contained reactor. + */ + public static String triggerRefNested(PortInstance port) { + return triggerRefNested(port, null, null); + } + + /** + * Return a reference to the trigger_t struct for the specified port of a contained reactor. + * + * @param port The output port of a contained reactor. + * @param runtimeIndex An optional index variable name to use to index the runtime instance of the + * port's parent's parent, or null to get the default returned by {@link + * CUtil#runtimeIndex(ReactorInstance)}. + * @param bankIndex An optional index variable name to use to index the the bank of the port's + * parent, or null to get the default returned by {@link CUtil#bankIndex(ReactorInstance)}. + */ + public static String triggerRefNested(PortInstance port, String runtimeIndex, String bankIndex) { + return reactorRefNested(port.getParent(), runtimeIndex, bankIndex) + + "." + + port.getName() + + "_trigger"; + } + + /** + * Given a reactor Class, return a set of include names for interacting reactors which includes + * all instantiations of base class that it extends. + */ + public static HashSet allIncludes(TypeParameterizedReactor tpr) { + var set = new HashSet(); + for (var i : ASTUtils.allInstantiations(tpr.reactor())) { + set.add(getName(new TypeParameterizedReactor(i, tpr))); + } + return set; + } + + ////////////////////////////////////////////////////// + //// FIXME: Not clear what the strategy is with the following inner interface. + // The {@code ReportCommandErrors} interface allows the + // method runBuildCommand to call a protected + // method from the CGenerator if that method is passed + // using a method reference. The method that is passed + // is then interpreted as a ReportCommandErrors instance. + // This is a convenient way to factor out part of the + // internals of the CGenerator while maintaining + // encapsulation, even though the internals of the CGenerator + // might seem to be tightly coupled. FIXME: Delete this comment + + /** + * A {@code ReportCommandErrors} is a way to analyze command output and report any errors that it + * describes. FIXME: If the VSCode branch passes code review without significant revision, this + * mechanism will probably be replaced. + */ + public interface ReportCommandErrors { + void report(String errors); + } + + /** + * Run the custom build command specified with the "build" parameter. This command is executed in + * the same directory as the source file. + * + *

The following environment variables will be available to the command: + * + *

    + *
  • {@code: LF_CURRENT_WORKING_DIRECTORY}: The directory in which the command is invoked. + *
  • {@code:LF_SOURCE_DIRECTORY}: The directory containing the .lf file being compiled. + *
  • {@code:LF_PACKAGE_DIRECTORY}: The directory that is the root of the package. + *
  • {@code:LF_SOURCE_GEN_DIRECTORY}: The directory in which generated files are placed. + *
  • {@code:LF_BIN_DIRECTORY}: The directory into which to put binaries. + *
+ */ + public static void runBuildCommand( + FileConfig fileConfig, + TargetConfig targetConfig, + GeneratorCommandFactory commandFactory, + MessageReporter messageReporter, + ReportCommandErrors reportCommandErrors, + LFGeneratorContext.Mode mode) { + List commands = + getCommands( + targetConfig.get(BuildCommandsProperty.INSTANCE), commandFactory, fileConfig.srcPath); + // If the build command could not be found, abort. + // An error has already been reported in createCommand. + if (commands.stream().anyMatch(Objects::isNull)) return; + + for (LFCommand cmd : commands) { + int returnCode = cmd.run(); + if (returnCode != 0 && mode != LFGeneratorContext.Mode.EPOCH) { + // FIXME: Why is the content of stderr not provided to the user in this error + // message? + messageReporter + .nowhere() + .error( + String.format( + // FIXME: Why is the content of stderr not provided to the user in this error + // message? + "Build command \"%s\" failed with error code %d.", + targetConfig.get(BuildCommandsProperty.INSTANCE), returnCode)); + return; + } + // For warnings (vs. errors), the return code is 0. + // But we still want to mark the IDE. + if (!cmd.getErrors().isEmpty() && mode == LFGeneratorContext.Mode.EPOCH) { + reportCommandErrors.report(cmd.getErrors()); + return; // FIXME: Why do we return here? Even if there are warnings, the build process + // should proceed. + } + } + } + + /** + * Remove files in the bin directory that may have been created. Call this if a compilation occurs + * so that files from a previous version do not accidentally get executed. + * + * @param fileConfig + */ + public static void deleteBinFiles(FileConfig fileConfig) { + String name = FileUtil.nameWithoutExtension(fileConfig.srcFile); + String[] files = fileConfig.binPath.toFile().list(); + List federateNames = new LinkedList<>(); // FIXME: put this in ASTUtils? + fileConfig + .resource + .getAllContents() + .forEachRemaining( + node -> { + if (node instanceof Reactor r) { + if (r.isFederated()) { + r.getInstantiations().forEach(inst -> federateNames.add(inst.getName())); + } + } + }); + for (String f : files) { + // Delete executable file or launcher script, if any. + // Delete distribution file, if any. + // Delete RTI file, if any. + if (f.equals(name) + || f.equals(name + RTI_BIN_SUFFIX) + || f.equals(name + RTI_DISTRIBUTION_SCRIPT_SUFFIX)) { + //noinspection ResultOfMethodCallIgnored + fileConfig.binPath.resolve(f).toFile().delete(); + } + // Delete federate executable files, if any. + for (String federateName : federateNames) { + if (f.equals(name + "_" + federateName)) { + //noinspection ResultOfMethodCallIgnored + fileConfig.binPath.resolve(f).toFile().delete(); + } + } + } + } + + ////////////////////////////////////////////////////// + //// Private functions. + + /** + * If the argument is a multiport, then return a string that gives the width as an expression, and + * otherwise, return null. The string will be empty if the width is variable (specified as '[]'). + * Otherwise, if is a single term or a sum of terms (separated by '+'), where each term is either + * an integer or a parameter reference in the target language. + */ + public static String multiportWidthExpression(Variable variable) { + List spec = multiportWidthTerms(variable); + return spec == null ? null : String.join(" + ", spec); + } + + ////////////////////////////////////////////////////// + //// Private methods. + + /** + * Convert the given commands from strings to their LFCommand representation and return a list of + * LFCommand. + * + * @param commands A list of commands as strings. + * @param factory A command factory. + * @param dir The directory in which the commands should be executed. + * @return The LFCommand representations of the given commands, where {@code null} is a + * placeholder for commands that cannot be executed. + */ + private static List getCommands( + List commands, GeneratorCommandFactory factory, Path dir) { + return commands.stream() + .map(cmd -> List.of(cmd.split("\\s+"))) + .filter(tokens -> tokens.size() > 0) + .map(tokens -> factory.createCommand(tokens.get(0), tokens.subList(1, tokens.size()), dir)) + .collect(Collectors.toList()); + } + + /** + * Return target code for a parameter reference, which in this case is just the parameter name. + * + * @param param The parameter to generate code for. + * @return Parameter reference in target code. + */ + private static String getTargetReference(Parameter param) { + return param.getName(); + } + + /** + * If the argument is a multiport, return a list of strings describing the width of the port, and + * otherwise, return null. If the list is empty, then the width is variable (specified as '[]'). + * Otherwise, it is a list of integers and/or parameter references. + * + * @param variable The port. + * @return The width specification for a multiport or null if it is not a multiport. + */ + private static List multiportWidthTerms(Variable variable) { + List result = null; + if (variable instanceof Port) { + if (((Port) variable).getWidthSpec() != null) { + result = new ArrayList<>(); + if (!((Port) variable).getWidthSpec().isOfVariableLength()) { + for (WidthTerm term : ((Port) variable).getWidthSpec().getTerms()) { + if (term.getParameter() != null) { + result.add("self->" + getTargetReference(term.getParameter())); + } else { + result.add(String.valueOf(term.getWidth())); + } + } + } + } + } + return result; + } + + /** + * Given a type for an input or output, return true if it should be carried by a lf_token_t struct + * rather than the type itself. It should be carried by such a struct if the type ends with * (it + * is a pointer) or [] (it is a array with unspecified length). + * + * @param type The type specification. + */ + public static boolean isTokenType(InferredType type) { + if (type.isUndefined()) return false; + // FIXME: This is a hacky way to do this. It is now considered to be a bug (#657) + return type.astType != null + && (type.astType.getCStyleArraySpec() != null + && type.astType.getCStyleArraySpec().isOfVariableLength() + || !type.astType.getStars().isEmpty() + || type.astType.getCode() != null + && type.astType.getCode().getBody().stripTrailing().endsWith("*")); + } + + public static String generateWidthVariable(String var) { + return var + "_width"; + } + + /** + * If the type specification of the form {@code type[]}, {@code type*}, or {@code type}, return + * the type. + * + * @param type A string describing the type. + */ + public static String rootType(String type) { + if (type.endsWith("]")) { + return type.substring(0, type.indexOf("[")).trim(); + } else if (type.endsWith("*")) { + return type.substring(0, type.length() - 1).trim(); + } else { + return type.trim(); + } + } + + /** + * Return the full name of the specified instance without the leading name of the top-level + * reactor, unless this is the top-level reactor, in which case return its name. + * + * @param instance The instance. + * @return A shortened instance name. + */ + public static String getShortenedName(ReactorInstance instance) { + var description = instance.getFullName(); + // If not at the top level, strip off the name of the top level. + var period = description.indexOf("."); + if (period > 0) { + description = description.substring(period + 1); + } + return description; + } + + /** + * Returns the ReactorInstance of the closest enclave in the containment hierarchy. + * + * @param inst The instance + */ + public static ReactorInstance getClosestEnclave(ReactorInstance inst) { + if (inst.isMainOrFederated() || isEnclave(inst.getDefinition())) { + return inst; + } + return getClosestEnclave(inst.getParent()); + } + + /** + * Returns the unique ID of the environment. This ID is a global variable in the generated C file. + * + * @param inst The instance + */ + public static String getEnvironmentId(ReactorInstance inst) { + ReactorInstance enclave = getClosestEnclave(inst); + return enclave.uniqueID(); + } + + /** + * Returns a string which represents a C variable which points to the struct of the environment of + * the ReactorInstance inst. + * + * @param inst The instance + */ + public static String getEnvironmentStruct(ReactorInstance inst) { + return "envs[" + getEnvironmentId(inst) + "]"; + } + + /** + * Returns the name of the environment which `inst` is in + * + * @param inst The instance + */ + public static String getEnvironmentName(ReactorInstance inst) { + ReactorInstance enclave = getClosestEnclave(inst); + return enclave.getName(); + } + + /** + * Given an instance, e.g. the main reactor, return a list of all enclaves in the program + * + * @param inst The instance + */ + public static List getEnclaves(ReactorInstance root) { + List enclaves = new ArrayList<>(); + Queue queue = new LinkedList<>(); + queue.add(root); + + while (!queue.isEmpty()) { + ReactorInstance inst = queue.poll(); + if (inst.isMainOrFederated() || isEnclave(inst.getDefinition())) { + enclaves.add(inst); + } + + for (ReactorInstance child : inst.children) { + queue.add(child); + } + } + return enclaves; + } +} diff --git a/lfc/core/src/main/java/org/lflang/generator/c/TypeParameterizedReactor.java b/lfc/core/src/main/java/org/lflang/generator/c/TypeParameterizedReactor.java new file mode 100644 index 000000000..e5f3b1e81 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/generator/c/TypeParameterizedReactor.java @@ -0,0 +1,225 @@ +package org.lflang.generator.c; + +import com.google.common.collect.ImmutableMap; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.eclipse.emf.common.util.URI; +import org.lflang.InferredType; +import org.lflang.ast.ASTUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.lf.*; + +/** A reactor class combined with concrete type arguments bound to its type parameters. */ +public class TypeParameterizedReactor { + /** The syntactic reactor class definition. */ + private final Reactor reactor; + + /** The type arguments associated with this particular variant of the reactor class. */ + private final Map typeArgs; + + private final List typeParams; + private final ImmutableMap> nameMap; + + /** + * Construct the TPR corresponding to the given instantiation which syntactically appears within + * the definition corresponding to {@code parent}. + * + * @param i An instantiation of the TPR to be constructed. + * @param parent The reactor in which {@code i} appears, or {@code null} if type variables are + * permitted instead of types in this TPR. + */ + public TypeParameterizedReactor(Instantiation i, TypeParameterizedReactor parent) { + this(i, parent, parent.nameMap); + } + + public TypeParameterizedReactor(Instantiation i, List reactors) { + this(i, null, getNameMap(reactors)); + } + + /** + * Return a map from reactor names and URIs to integers such that no two reactor names with + * different URIs map to the same integer. + */ + private static Map> getNameMap(List reactors) { + Map> nameMap = new HashMap<>(); + Map countMap = new HashMap<>(); + var sortedReactors = + reactors.stream() + .sorted(Comparator.comparing(a -> a.eResource().getURI().toString())) + .toList(); + for (var reactor : sortedReactors) { + var def = ASTUtils.toDefinition(reactor); + var name = def.getName().toLowerCase(); + if (nameMap.containsKey(name)) { + nameMap.get(name).put(def.eResource().getURI(), countMap.get(name)); + countMap.put(name, countMap.get(name)); + } else { + nameMap.put(name, new HashMap<>()); + nameMap.get(name).put(def.eResource().getURI(), 0); + countMap.put(name, 1); + } + } + return nameMap; + } + + /** Return a name that is unique to the given {@code Reactor}. */ + private String uniqueName(Reactor def) { + var name = def.getName().toLowerCase(); + var number = Objects.requireNonNull(nameMap.get(name)).get(def.eResource().getURI()); + return name + (number == 0 ? "" : number); + } + + /** + * Construct a {@code TypeParameterizedReactor} corresponding to the reactor class of the + * instantiation {@code i} within the parent {@code parent} and with the given mapping of + * definition names and URIs to integers. + */ + private TypeParameterizedReactor( + Instantiation i, TypeParameterizedReactor parent, Map> nameMap) { + reactor = ASTUtils.toDefinition(i.getReactorClass()); + var definition = ASTUtils.toDefinition(i.getReactorClass()); + typeParams = definition.getTypeParms().stream().map(TypeParm::getLiteral).toList(); + typeArgs = addTypeArgs(i, parent, typeParams); + this.nameMap = ImmutableMap.copyOf(nameMap); + } + + /** Return a mapping from type parameters to type arguments. */ + private static Map addTypeArgs( + Instantiation instantiation, TypeParameterizedReactor parent, List typeParams) { + HashMap ret = new HashMap<>(); + if (instantiation.getTypeArgs() != null) { + for (int i = 0; i < typeParams.size(); i++) { + var arg = instantiation.getTypeArgs().get(i); + ret.put(typeParams.get(i), parent == null ? arg : parent.resolveType(arg)); + } + } + return ret; + } + + /** Return the name of the reactor given its type arguments. */ + public String getName() { + return reactor.getName() + argsString(); + } + + /** Return a string representation of the type args of this. */ + public String argsString() { + var stringRepresentation = new StringBuilder(); + int hash = 0; + var first = false; + for (var key : typeParams) { + if (!first) { + stringRepresentation.append('_'); + } + var value = typeArgs.get(key); + var valueString = ASTUtils.toOriginalText(value); + for (int idx = 0; idx < valueString.length(); idx++) { + var c = valueString.charAt(idx); + if (Character.isLetterOrDigit(c)) { + stringRepresentation.append(c); + } else { + hash = hash * 31 + idx; + hash = hash * 31 + c; + } + } + } + if (hash != 0) { + stringRepresentation.append('_'); + stringRepresentation.append(Integer.toHexString(hash)); + } + return stringRepresentation.toString(); + } + + /** #define type names as concrete types. */ + public void doDefines(CodeBuilder b) { + typeArgs.forEach( + (literal, concreteType) -> + b.pr( + "#if defined " + + literal + + "\n" + + "#undef " + + literal + + "\n" + + "#endif // " + + literal + + "\n" + + "#define " + + literal + + " " + + ASTUtils.toOriginalText(concreteType))); + } + + /** Resolve type arguments if the given type is defined in terms of generics. */ + public Type resolveType(Type t) { + if (t.getId() != null && typeArgs.get(t.getId()) != null) return typeArgs.get(t.getId()); + if (t.getCode() == null) return t; + var arg = typeArgs.get(t.getCode().getBody()); + if (arg != null) return arg; + return t; + } + + /** Resolve type arguments if the given type is defined in terms of generics. */ + public InferredType resolveType(InferredType t) { + if (t.astType == null) return t; + return InferredType.fromAST(resolveType(t.astType)); + } + + /** + * Return a name that is unique to this TypeParameterizedReactor (up to structural equality) and + * that is prefixed with exactly one underscore and that does not contain any upper-case letters. + */ + public String uniqueName() { + var resolved = ASTUtils.toDefinition(reactor); + return "_" + uniqueName(resolved) + argsString(); + } + + @Override + public int hashCode() { + return reactor.hashCode() * 31 + + typeArgs.entrySet().stream() + .mapToInt(it -> it.getKey().hashCode() ^ typeHash(it.getValue())) + .sum(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof TypeParameterizedReactor other + && reactor.equals(other.reactor) + && typeArgs.entrySet().stream() + .allMatch(it -> typeEquals(other.typeArgs.get(it.getKey()), it.getValue())); + } + + public Reactor reactor() { + return reactor; + } + + // We operate on the ECore model rather than an internal IR, so hashcode and equals are provided + // here instead of as methods. + private static int typeHash(Type t) { + var sum = t.getStars() == null ? 0 : t.getStars().stream().toList().hashCode(); + sum = 31 * sum + (t.getCode() == null ? 0 : Objects.hashCode(t.getCode().getBody())); + sum = 31 * sum + Objects.hashCode(t.getId()); + sum = 31 * sum + Objects.hashCode(t.getCStyleArraySpec()); + sum = 2 * sum + (t.isTime() ? 1 : 0); + sum = 31 * sum + (t.getTypeArgs() == null ? 0 : t.getTypeArgs().stream().toList().hashCode()); + return sum; + } + + private static boolean typeEquals(Type t, Type tt) { + return t.getStars() == null + ? tt.getStars() == null + : t.getStars().stream().toList().equals(tt.getStars().stream().toList()) + && t.getCode() == null + ? tt.getCode() == null + : Objects.equals(t.getCode().getBody(), tt.getCode().getBody()) + && Objects.equals(t.getId(), tt.getId()) + && Objects.equals(t.getCStyleArraySpec(), tt.getCStyleArraySpec()) + && t.isTime() == tt.isTime() + && t.getTypeArgs() == null + ? tt.getTypeArgs() == null + : t.getTypeArgs().stream().toList().equals(tt.getTypeArgs().stream().toList()); + } +} diff --git a/lfc/core/src/main/java/org/lflang/pretvm/ExecutionPhase.java b/lfc/core/src/main/java/org/lflang/pretvm/ExecutionPhase.java new file mode 100644 index 000000000..1d4fc07f8 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/pretvm/ExecutionPhase.java @@ -0,0 +1,12 @@ +package org.lflang.pretvm; + +public enum ExecutionPhase { + PREAMBLE, + INIT, + PERIODIC, + EPILOGUE, + SYNC_BLOCK, + INIT_AND_PERIODIC, + SHUTDOWN_TIMEOUT, + SHUTDOWN_STARVATION, +} diff --git a/lfc/core/src/main/java/org/lflang/pretvm/InstructionGenerator.java b/lfc/core/src/main/java/org/lflang/pretvm/InstructionGenerator.java new file mode 100644 index 000000000..b89a06c27 --- /dev/null +++ b/lfc/core/src/main/java/org/lflang/pretvm/InstructionGenerator.java @@ -0,0 +1,2250 @@ +package org.lflang.pretvm; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import org.lflang.FileConfig; +import org.lflang.TimeValue; +import org.lflang.ast.ASTUtils; +import org.lflang.generator.CodeBuilder; +import org.lflang.generator.PortInstance; +import org.lflang.generator.ReactionInstance; +import org.lflang.generator.ReactorInstance; +import org.lflang.generator.RuntimeRange; +import org.lflang.generator.SendRange; +import org.lflang.generator.TriggerInstance; +import org.lflang.generator.c.CUtil; +import org.lflang.generator.c.TypeParameterizedReactor; +import org.lflang.lf.Connection; +import org.lflang.lf.Expression; +import org.lflang.lf.Output; +import org.lflang.pretvm.dag.Dag; +import org.lflang.pretvm.dag.DagNode; +import org.lflang.pretvm.dag.JobNode; +import org.lflang.pretvm.dag.TimeNode; +import org.lflang.pretvm.instruction.*; +import org.lflang.pretvm.register.Register; +import org.lflang.pretvm.register.ReturnAddr; +import org.lflang.pretvm.register.RuntimeVar; +import org.lflang.pretvm.register.WorkerRegister; +import org.lflang.target.TargetConfig; +import org.lflang.target.property.FastProperty; +import org.lflang.target.property.TimeOutProperty; + +/** + * A generator that generates PRET VM programs from DAGs. It also acts as a linker that piece + * together multiple PRET VM object files. + */ +public class InstructionGenerator { + + /** File configuration */ + FileConfig fileConfig; + + /** Target configuration */ + TargetConfig targetConfig; + + /** Main reactor instance */ + protected ReactorInstance main; + + /** A list of reactor instances in the program */ + List reactors; + + /** A list of reaction instances in the program */ + List reactions; + + /** A list of port instances in the program */ + List ports; + + /** Number of workers */ + int workers; + + /** + * A nested map that maps a source port to a C function name, which updates a priority queue + * holding tokens in a delayed connection. Each input can identify a unique connection because no + * more than one connection can feed into an input port. + */ + private Map preConnectionHelperFunctionNameMap = new HashMap<>(); + + private Map postConnectionHelperFunctionNameMap = new HashMap<>(); + + /** + * A map that maps a trigger to a list of (BEQ) instructions where this trigger's presence is + * tested. + */ + private Map> triggerPresenceTestMap = new HashMap<>(); + + /** PretVM registers */ + private Registers registers; + + /** Utility object with helper functions for specific platforms (reactor-c, reactor-uc). */ + private PlatformUtil util; + + /** Constructor */ + public InstructionGenerator( + FileConfig fileConfig, + TargetConfig targetConfig, + int workers, + ReactorInstance main, + List reactors, + List reactions, + List ports, + Registers registers, + PlatformUtil util) { + this.fileConfig = fileConfig; + this.targetConfig = targetConfig; + this.workers = workers; + this.main = main; + this.reactors = reactors; + this.reactions = reactions; + this.ports = ports; + this.registers = registers; + this.util = util; + } + + /** Topologically sort the dag nodes and assign release values to DAG nodes for counting locks. */ + public void assignReleaseIndices(Dag dagParitioned) { + // Initialize a reaction index array to keep track of the latest counting + // lock value for each worker. + Long[] releaseValues = new Long[workers]; + Arrays.fill(releaseValues, 0L); // Initialize all elements to 0 + + // Iterate over a topologically sorted list of dag nodes. + for (DagNode current : dagParitioned.getTopologicalSort()) { + if (current instanceof JobNode currentJob) { + releaseValues[currentJob.getWorker()] += 1; + currentJob.setReleaseIndex(releaseValues[currentJob.getWorker()]); + } + } + } + + /** Traverse the DAG from head to tail using Khan's algorithm (topological sort). */ + public List> generateInstructions( + Dag dagParitioned, PartialSchedule partialSchedule) { + // Map from a reactor to its latest associated SYNC node. + // Use case 1: This is used to determine when ADVIs and DUs should be generated without + // duplicating them for each reaction node in the same reactor. + // Use case 2: Determine a relative time increment for ADVIs. + Map reactorToLastSeenSyncNodeMap = new HashMap<>(); + + // Map an output port to its last seen EXE instruction at the current + // tag. When we know for sure that no other reactions can modify a port, we then + // go back to the last seen reaction-invoking EXE that can modify this port and + // _insert_ a connection helper right after the last seen EXE in the schedule. + // All the key value pairs in this map are waiting to be handled, + // since all the output port values must be written to the buffers at the + // end of the tag. + Map portToUnhandledReactionExeMap = new HashMap<>(); + + // Map a reaction to its last seen invocation, which is a DagNode. + // If two invocations are mapped to different workers, a WU needs to + // be generated to prevent race condition. + // This map is used to check whether the WU needs to be generated. + Map reactionToLastSeenInvocationMap = new HashMap<>(); + + // Assign release values for the reaction nodes. + assignReleaseIndices(dagParitioned); + + // Instructions for all workers + List> instructions = new ArrayList<>(); + for (int i = 0; i < workers; i++) { + instructions.add(new ArrayList()); + } + + // Iterate over a topologically sorted list of dag nodes. + for (DagNode current : dagParitioned.getTopologicalSort()) { + // Get the upstream reaction nodes. + List upstreamReactionNodes = + dagParitioned.dagEdgesRev.getOrDefault(current, new HashMap<>()).keySet().stream() + .filter(n -> n instanceof JobNode) + .map(n -> (JobNode) n) + .toList(); + + if (current instanceof JobNode currentJob) { + // Find the worker assigned to the REACTION node, + // the reactor, and the reaction. + int worker = currentJob.getWorker(); + ReactionInstance reaction = currentJob.getReaction(); + ReactorInstance reactor = reaction.getParent(); + + // Current worker schedule + List workerInstructions = instructions.get(worker); + + // Get the nearest upstream sync node. + TimeNode associatedSyncNode = currentJob.getAssociatedSyncNode(); + + // WU Case 1: + // If the reaction depends on upstream reactions owned by other + // workers, generate WU instructions to resolve the dependencies. + // FIXME: The current implementation generates multiple unnecessary WUs + // for simplicity. How to only generate WU when necessary? + for (JobNode n : upstreamReactionNodes) { + int upstreamOwner = n.getWorker(); + if (upstreamOwner != worker) { + addInstructionForWorker( + instructions, + currentJob.getWorker(), + currentJob, + null, + new WU(registers.progressIndices.get(upstreamOwner), n.getReleaseIndex())); + } + } + + // WU Case 2: + // FIXME: Is there a way to implement this using waitUntilDependencies + // in the Dag class? + // If the reaction has an _earlier_ invocation and is mapped to a + // _different_ worker, then a WU needs to be generated to prevent from + // processing of these two invocations of the same reaction in parallel. + // If they are processed in parallel, the shared logical time field in + // the reactor could get concurrent updates, resulting in incorrect + // execution. + // Most often, there is not an edge between these two nodes, + // making this a trickier case to handle. + // The strategy here is to use a variable to remember the last seen + // invocation of the same reaction instance. + JobNode lastSeen = reactionToLastSeenInvocationMap.get(reaction); + if (lastSeen != null && lastSeen.getWorker() != currentJob.getWorker()) { + addInstructionForWorker( + instructions, + currentJob.getWorker(), + currentJob, + null, + new WU( + registers.progressIndices.get(lastSeen.getWorker()), lastSeen.getReleaseIndex())); + if (currentJob + .getAssociatedSyncNode() + .getTime() + .isEarlierThan(lastSeen.getAssociatedSyncNode().getTime())) { + System.out.println( + "FATAL ERROR: The current node is earlier than the lastSeen node. This case should" + + " not be possible and this strategy needs to be revised."); + System.exit(1); + } + } + reactionToLastSeenInvocationMap.put(reaction, currentJob); + + // WU Case 3: + // If the node has an upstream dependency based on connection, but the + // upstream is mapped to a different worker. Generate a WU. + List upstreamsFromConnection = dagParitioned.waitUntilDependencies.get(current); + if (upstreamsFromConnection != null && upstreamsFromConnection.size() > 0) { + for (JobNode us : upstreamsFromConnection) { + if (us.getWorker() != currentJob.getWorker()) { + addInstructionForWorker( + instructions, + currentJob.getWorker(), + currentJob, + null, + new WU(registers.progressIndices.get(us.getWorker()), us.getReleaseIndex())); + } + } + } + + // When the new associated sync node _differs_ from the last associated sync + // node of the reactor, this means that the current node's reactor needs + // to advance to a new tag (i.e., reaches a new timestamp). + // The code should update the associated sync node + // in the reactorToLastSeenSyncNodeMap map. And if + // associatedSyncNode is not the head, generate time-advancement + // instructions (abbreviated as ADVI) and DU. + if (associatedSyncNode != reactorToLastSeenSyncNodeMap.get(reactor)) { + // Before updating reactorToLastSeenSyncNodeMap, + // compute a relative time increment to be used when generating an ADVI. + long relativeTimeIncrement; + if (reactorToLastSeenSyncNodeMap.get(reactor) != null) { + relativeTimeIncrement = + associatedSyncNode.getTime().toNanoSeconds() + - reactorToLastSeenSyncNodeMap.get(reactor).getTime().toNanoSeconds(); + } else { + relativeTimeIncrement = associatedSyncNode.getTime().toNanoSeconds(); + } + + // Update the reactorToLastSeenSyncNodeMap mapping. + reactorToLastSeenSyncNodeMap.put(reactor, associatedSyncNode); + + // If the reaction depends on a single SYNC node, + // advance to the LOGICAL time of the SYNC node first, + // as well as delay until the PHYSICAL time indicated by the SYNC node. + // Skip if it is the start node since this is done in the sync block. + // FIXME: Here we have an implicit assumption "logical time is + // physical time." We need to find a way to relax this assumption. + // FIXME: One way to relax this is that "logical time is physical time + // only when executing real-time reactions, otherwise fast mode for + // non-real-time reactions." + // + // FIXME: The DU generation here could potentially be merged with the DU generation for the sync block, + // since both are about delaying physical time until a new tag. + if (associatedSyncNode != dagParitioned.start) { + + // A pre-connection helper for an output port cannot be inserted + // until we are sure that all reactions that can modify this port + // at this tag has been invoked. At this point, since we have + // detected time advancement, this condition is satisfied. + // Iterate over all the ports of this reactor. We know at + // this point that the EXE instruction stored in + // portToUnhandledReactionExeMap is that the very last reaction + // invocation that can modify these ports. So we can insert + // pre-connection helpers after that reaction invocation. + // + // FIXME: There should be a way to merge this for loop with + // the other loop involving generatePreConnectionHelper, since both + // effectively generate cleanup functions at the end of a tag. + for (PortInstance output : reactor.outputs) { + // Only generate for delayed connections. + if (outputToDelayedConnection(output)) { + EXE lastPortModifyingReactionExe = portToUnhandledReactionExeMap.get(output); + if (lastPortModifyingReactionExe != null) { + int exeWorker = lastPortModifyingReactionExe.getWorker(); + int indexToInsert = + indexOfByReference(instructions.get(exeWorker), lastPortModifyingReactionExe) + + 1; + generatePreConnectionHelper( + output, + instructions, + exeWorker, + indexToInsert, + lastPortModifyingReactionExe.getDagNode()); + // Remove the entry since this port is handled. + portToUnhandledReactionExeMap.remove(output); + } + } + } + + // Generate instructions to advance reactor-local logical time using a relative time increment. + // (instead of absolute), because using relative time increment promotes code reuse. + // FIXME: Factor out in a separate function. + String reactorTime = util.getReactorTimePointer(reactor); + Register reactorTimeReg = registers.getRuntimeVar(reactorTime); + var timeAdvInsts = + generateTimeAdvancementInstructions(reactor, reactorTimeReg, relativeTimeIncrement); + addInstructionSequenceForWorker(instructions, worker, current, null, timeAdvInsts); + + // Generate a DU using a relative time increment. + // There are two cases for NOT generating a DU within a + // hyperperiod: 1. if fast is on, 2. if dash is on and the parent + // reactor is not realtime. + // Generate a DU instruction if neither case holds. + if (!(targetConfig.get(FastProperty.INSTANCE))) { + // reactorTimeReg is already updated by time advancement instructions. + // Just delay until its recently updated value. + addInstructionForWorker( + instructions, worker, current, null, new DU(reactorTimeReg, 0L)); + } + } + } + + // Since we are starting a new tag, add a prepare function for the current job. + // + // Add the post-connection helper to the schedule, in case this reaction + // is triggered by an input port, which is connected to a connection buffer. + // Reaction invocations can be skipped, and we don't want the connection management to be skipped. + // + // FIXME (reactor-c): This will not work for reactor-c, because reactor-c's post-connection helper currently + // executes at the end of the last reaction. We need to make it like reactor-uc's prepare, which + // executes at the beginning of a tag. + // FIXME (reactor-c): This will not work when an input port triggers multiple reactions. + // We only want to add a post connection helper after the last reaction triggered by this port. + generatePostConnectionHelpers( + reaction, instructions, worker, null, currentJob); + + //////////////////////////////////////////////////////////////// + // Generate instruction sequence for invoking a single reaction or its deadline handler. + // The general structure of this instruction sequence is: + // + // (Outside the sequence) + // Line x-7: BEQ checks if trigger A is present, if so, jump to start of the sequence. + // Line x-6: BEQ checks if trigger B is present, if so, jump to start of the sequence. + // Line x-5: JAL skips reaction if triggers not present + // (Start of the sequence) + // Line x-4: EXE post_connection_helper_function (reactor-uc only) + // Line x-3: ADDI temp0_reg, tag.time, reaction_deadline + // Line x-2: EXE update_temp1_to_current_time() // temp1_reg := lf_time_physical() + // Line x-1: BLT temp0_reg, temp1_reg, x+1 + // Line x : EXE reaction_body_function + // Line x+1: JAL x+3 // Jump pass the deadline handler if reaction body is executed. + // Line x+2: EXE deadline_handler_function + // Line x+3: EXE post_connection_helper_function (reactor-c only) + // (End of the sequence) + // + // Here we need to create the ADDI, EXE, and BLT instructions involved. + //////////////////////////////////////////////////////////////// + // Declare a sequence of instructions related to invoking the + // reaction body and handling deadline violations. + List reactionInvokingSequence = new ArrayList<>(); + // Create an EXE instruction that invokes the reaction. + String reactorTimePointer = util.getReactorTimePointer(reactor); + String reactionPointer = util.getReactionFunctionPointer(reaction); + // reactor-uc passes in a _reaction_ pointer, while reactor-c passes in a _reactor_ pointer. + String reactionParameter1 = util.getReactionFunctionParameter1(reaction); + // reactor-uc passes in a reactor pointer to help get its reactor-local tag, + // while reactor-c passes in a reaction index. + String reactionParameter2 = util.getReactionFunctionParameter2(reaction); + EXE exeReaction = + new EXE( + registers.getRuntimeVar(reactionPointer), + registers.getRuntimeVar(reactionParameter1), + registers.getRuntimeVar(reactionParameter2)); + exeReaction.addLabel( + new Label( + "EXECUTE_" + reaction.getFullNameWithJoiner("_") + "_" + generateShortUUID())); + if (reaction.declaredDeadline != null) { + // Create ADDI for storing the physical time after which the + // deadline is considered violated, + // basically, current tag + deadline value. + Instruction addiDeadlineTime = + new ADDI( + registers.temp0.get(worker), + registers.getRuntimeVar(reactorTimePointer), + reaction.declaredDeadline.maxDelay.toNanoSeconds()); + addiDeadlineTime.addLabel( + new Label( + "CALCULATE_DEADLINE_VIOLATION_TIME_FOR_" + + reaction.getFullNameWithJoiner("_") + + "_" + + generateShortUUID())); + + // Create EXE for updating the time register. + var exeUpdateTimeRegister = + new EXE( + registers.getRuntimeVar("update_temp1_to_current_time"), + registers.temp1.get(worker), + null); + + // Create deadline handling EXE + String deadlineHandlerPointer = + util.getReactionDeadlineHandlerFunctionPointer(reaction); + Instruction exeDeadlineHandler = + new EXE( + registers.getRuntimeVar(deadlineHandlerPointer), + registers.getRuntimeVar(reactionParameter1), + registers.getRuntimeVar(reactionParameter2)); + exeDeadlineHandler.addLabel( + new Label( + "HANDLE_DEADLINE_VIOLATION_OF_" + + reaction.getFullNameWithJoiner("_") + + "_" + + generateShortUUID())); + + // Create BLT for checking deadline violation. + var bltDeadlineViolation = + new BLT( + registers.temp0.get(worker), + registers.temp1.get(worker), + exeDeadlineHandler.getLabel()); + + // Create JAL for jumping pass the deadline handler if the + // deadline is not violated. + var jalPassHandler = new JAL(registers.zero, exeDeadlineHandler.getLabel(), 1L); + + // Add the reaction-invoking EXE and deadline handling + // instructions to the schedule in the right order. + reactionInvokingSequence.add(addiDeadlineTime); + reactionInvokingSequence.add(exeUpdateTimeRegister); + reactionInvokingSequence.add(bltDeadlineViolation); + reactionInvokingSequence.add(exeReaction); + reactionInvokingSequence.add(jalPassHandler); + reactionInvokingSequence.add(exeDeadlineHandler); + } else { + // If the reaction does not have a deadline, just add the EXE + // running the reaction body. + reactionInvokingSequence.add(exeReaction); + } + + // It is important that the beginning and the end of the + // sequence has labels, so that the trigger checking BEQ + // instructions can jump to the right place. + if (reactionInvokingSequence.get(0).getLabel() == null + || reactionInvokingSequence.get(reactionInvokingSequence.size() - 1) == null) { + throw new RuntimeException( + "The reaction invoking instruction sequence either misses a label at the first" + + " instruction or at the last instruction, or both."); + } + + // Create BEQ instructions for checking ports. + // Check if the reaction reacts to input ports or not. If so, + // we need guards implemented using BEQ. + boolean hasGuards = false; + for (var trigger : reaction.triggers) { + if (trigger instanceof PortInstance port && port.isInput()) { + hasGuards = true; + Register reg1; + Register reg2; + // If connection has delay, check the connection buffer to see if + // the earliest event matches the reactor's current logical time. + if (inputFromDelayedConnection(port)) { + // Before checking trigger, update worker's temp0 to store the circular buffer head's timestamp. + addInstructionForWorker(instructions, currentJob.getWorker(), current, null, new EXE( + registers.getRuntimeVar(util.getHelperFunctionSetTemp0ToBufferHeadTime()), registers.getRuntimeVar(util.getPqueueHead(port)), null)); + + reg1 = registers.temp0.get(worker); + reg2 = registers.getRuntimeVar(reactorTimePointer); // Check if temp0 == reactor local timestamp + } + // Otherwise, if the connection has zero delay, check for the presence of the + // downstream port. + else { + String isPresentField = + "&" + util.getPortIsPresentFieldPointer(port); // The port's is_present field + reg1 = registers.getRuntimeVar(isPresentField); + reg2 = registers.one; // Checking if is_present == 1 + } + Instruction reactionSequenceStart = reactionInvokingSequence.get(0); + Instruction beq = new BEQ(reg1, reg2, reactionSequenceStart.getLabel()); + // FIXME: This label feels more like a comment. The label itself is never used. + // There should be a way to just specify comments in the schedule. + // The trigger-checking BEQs are not part of the reaction invoking sequence, + // so these labels are not referred to explicitly in the static schedule. + beq.addLabel( + new Label( + "TEST_TRIGGER_" + port.getFullNameWithJoiner("_") + "_" + generateShortUUID())); + addInstructionForWorker(instructions, currentJob.getWorker(), current, null, beq); + // Update triggerPresenceTestMap. + if (triggerPresenceTestMap.get(port) == null) + triggerPresenceTestMap.put(port, new LinkedList<>()); + triggerPresenceTestMap.get(port).add(beq); + } + } + + // If none of the guards are activated, jump to one line after the + // reaction-invoking instruction sequence. + if (hasGuards) { + Instruction reactionSkippingJAL = new JAL( + registers.zero, + reactionInvokingSequence.get(reactionInvokingSequence.size() - 1).getLabel(), + 1L); + addInstructionForWorker( + instructions, + worker, + current, + null, + reactionSkippingJAL); + } + + // Add the reaction-invoking sequence to the instructions. + addInstructionSequenceForWorker( + instructions, currentJob.getWorker(), current, null, reactionInvokingSequence); + + // Add this reaction invoking EXE to the output-port-to-EXE map, + // so that we know when to insert pre-connection helpers. + for (TriggerInstance effect : reaction.effects) { + if (effect instanceof PortInstance output) { + portToUnhandledReactionExeMap.put(output, exeReaction); + } + } + + // Increment the progress index of the worker. + // IMPORTANT: This ADDI has to be last because executing it releases + // downstream workers. If this ADDI is executed before + // connection management, then there is a race condition between + // upstream pushing events into connection buffers and downstream + // reading connection buffers. + // Instantiate an ADDI to be executed after EXE, releasing the counting locks. + var addi = + new ADDI( + registers.progressIndices.get(currentJob.getWorker()), + registers.progressIndices.get(currentJob.getWorker()), + 1L); + addInstructionForWorker(instructions, worker, current, null, addi); + + } else if (current instanceof TimeNode currentTime) { + if (current == dagParitioned.end) { + // At this point, we know for sure that all reactors are done with + // its current tag and are ready to advance time. We now insert a + // pre-connection helper (cleanup function) after each output port + // has been modified by the last reaction that could write to it. + // In short, we cleanup the port at the end of the tag + // (i.e. push the output event into buffer). + for (var entry : portToUnhandledReactionExeMap.entrySet()) { + PortInstance output = entry.getKey(); + // Only generate for delayed connections. + if (outputToDelayedConnection(output)) { + Instruction lastReactionExe = entry.getValue(); + int exeWorker = lastReactionExe.getWorker(); + int indexToInsert = + indexOfByReference(instructions.get(exeWorker), lastReactionExe) + 1; + generatePreConnectionHelper( + output, instructions, exeWorker, indexToInsert, lastReactionExe.getDagNode()); + } + } + portToUnhandledReactionExeMap.clear(); + + // When the timeStep = TimeValue.MAX_VALUE in a SYNC node, + // this means that the DAG is acyclic and can end without + // real-time constraints, hence we do not genereate DU and ADDI. + if (currentTime.getTime() != TimeValue.MAX_VALUE) { + for (int worker = 0; worker < workers; worker++) { + // [Only Worker 0] Update the time offset increment register. + if (worker == 0) { + addInstructionForWorker( + instructions, + worker, + current, + null, + new ADDI( + registers.offsetInc, + registers.zero, + currentTime.getTime().toNanoSeconds())); + } + // Let all workers go to SYNC_BLOCK after finishing PREAMBLE. + addInstructionForWorker( + instructions, + worker, + current, + null, + new JAL( + registers.returnAddrs.get(worker), + Label.getExecutionPhaseLabel(ExecutionPhase.SYNC_BLOCK))); + // Add a DU instruction if the fast mode is off. + // Turning on the dash mode does not affect this DU. The + // hyperperiod is still real-time. + // ALTERNATIVE DESIGN: remove the DU here and let the start node, + // instead of the end node, handle DU. This potentially allows + // breaking the hyperperiod boundary. + // + // At this point, the global offset register has been + // updated in SYNC_BLOCK. + // + // We want to place this DU after the SYNC_BLOCK so that + // workers enters a new hyperperiod with almost zero lag. + // If this DU is placed before, then the SYNC_BLOCK will + // contribute the lag at the beginning of the hyperperiod. + if (!targetConfig.get(FastProperty.INSTANCE)) + addInstructionForWorker( + instructions, worker, current, null, new DU(registers.offset, 0L)); + } + } + } + } + } + // Add a label to the first instruction using the exploration phase + // (INIT, PERIODIC, SHUTDOWN_TIMEOUT, etc.). + for (int w = 0; w < workers; w++) { + // First, check if there is any instruction generated. + // A worker without any work assignment has an empty schedule. + // In this case, generate a dummy instruction: adding zero to a + // temp register. + // Without this dummy instruction, currently there will be + // compilation errors due to not having a place to put phase labels. + if (instructions.get(w).size() == 0) { + addInstructionForWorker( + instructions, + w, + dagParitioned.end, + null, + new ADD(registers.temp0.get(w), registers.temp0.get(w), registers.zero)); + } + // Then assign a label to the first instruction. + instructions.get(w).get(0).addLabel(Label.getExecutionPhaseLabel(partialSchedule.getPhase())); + } + + return instructions; + } + + /** + * Generate a sequence of instructions for advancing a reactor's logical time. First, the + * reactor's local time register needs to be incremented. Then, the `is_present` fields of the + * reactor's output ports need to be set to false. This function replaces two previously + * specialized instructions: ADV & ADVI. + * + *

This function is designed to have the same signature as ADV and ADVI. + * + * @param reactor The reactor instance to advance time and clear output ports for + * @param baseTimeReg The base time this reactor should advance to (either the reactor's current + * time register, or the time offset for the next hyperperiod) + * @param relativeTimeIncrement The time increment added on top of baseTimeReg + * @return A list of instructions for advancing reactor's local time + */ + private List generateTimeAdvancementInstructions( + ReactorInstance reactor, Register baseTimeReg, long relativeTimeIncrement) { + List timeAdvInsts = new ArrayList<>(); + + // Increment the reactor local time. + String reactorTimePointer = util.getReactorTimePointer(reactor); + Register reactorTimeReg = registers.getRuntimeVar(reactorTimePointer); + var addiIncrementTime = new ADDI(reactorTimeReg, baseTimeReg, relativeTimeIncrement); + var uuid = generateShortUUID(); + addiIncrementTime.addLabel( + new Label("ADVANCE_TAG_FOR_" + reactor.getFullNameWithJoiner("_") + "_" + uuid)); + timeAdvInsts.add(addiIncrementTime); + + // Reset the is_present fields of all output ports of this reactor. + var outputs = reactor.outputs; + for (int i = 0; i < outputs.size(); i++) { + var output = outputs.get(i); + String isPresentPointer = util.getPortIsPresentFieldPointer(output); + Register portIsPresentReg = registers.getRuntimeVar(isPresentPointer); + var addiResetIsPresent = new ADD(portIsPresentReg, registers.zero, registers.zero); + timeAdvInsts.add(addiResetIsPresent); + } + return timeAdvInsts; + } + + /** + * Helper function for adding an instruction to a worker schedule. This function is not meant to + * be called in the code generation logic above, because a node needs to be associated with each + * instruction added. + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param inst The instruction to be added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + */ + private void _addInstructionForWorker( + List> instructions, int worker, Integer index, Instruction inst) { + if (index == null) { + // Add instruction to the instruction list. + instructions.get(worker).add(inst); + } else { + // Insert instruction to the instruction list at the specified index. + instructions.get(worker).add(index, inst); + } + // Remember the worker at the instruction level. + inst.setWorker(worker); + } + + /** + * Helper function for adding an instruction to a worker schedule + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param node The DAG node for which this instruction is added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + * @param inst The instruction to be added + */ + private void addInstructionForWorker( + List> instructions, + int worker, + DagNode node, + Integer index, + Instruction inst) { + // Add an instruction to the instruction list. + _addInstructionForWorker(instructions, worker, index, inst); + // Store the reference to the DAG node in the instruction. + inst.addDagNode(node); + } + + /** + * Helper function for adding an instruction to a worker schedule + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param nodes A list of DAG nodes for which this instruction is added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + * @param inst The instruction to be added + */ + private void addInstructionForWorker( + List> instructions, + int worker, + List nodes, + Integer index, + Instruction inst) { + // Add an instruction to the instruction list. + _addInstructionForWorker(instructions, worker, index, inst); + for (DagNode node : nodes) { + // Store the reference to the DAG node in the instruction. + inst.addDagNode(node); + } + } + + /** + * Helper function for adding a sequence of instructions to a worker schedule + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param node The DAG node for which this instruction is added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + * @param instList The list of instructions to be added + */ + private void addInstructionSequenceForWorker( + List> instructions, + int worker, + DagNode node, + Integer index, + List instList) { + // Add instructions to the instruction list. + for (int i = 0; i < instList.size(); i++) { + Instruction inst = instList.get(i); + _addInstructionForWorker(instructions, worker, index, inst); + // Store the reference to the DAG node in the instruction. + inst.addDagNode(node); + } + } + + /** + * Helper function for adding a sequence of instructions to a worker schedule + * + * @param instructions The instructions under generation for a particular phase + * @param worker The worker who owns the instruction + * @param nodes A list of DAG nodes for which this instruction is added + * @param index The index at which to insert the instruction. If the index is null, append the + * instruction at the end. Otherwise, append it at the specific index. + * @param instList The list of instructions to be added + */ + private void addInstructionSequenceForWorker( + List> instructions, + int worker, + List nodes, + Integer index, + List instList) { + // Add instructions to the instruction list. + for (int i = 0; i < instList.size(); i++) { + Instruction inst = instList.get(i); + _addInstructionForWorker(instructions, worker, index, inst); + // Store the reference to the DAG node in the instruction. + for (DagNode node : nodes) { + // Store the reference to the DAG node in the instruction. + inst.addDagNode(node); + } + } + } + + /** Generate C code from the instructions list. */ + public void generateCode(List> instructions) { + + // Instantiate a code builder. + Path srcgen = fileConfig.getSrcGenPath(); + Path file = srcgen.resolve("static_schedule.c"); + CodeBuilder code = new CodeBuilder(); + + // Generate a block comment. + code.pr( + String.join( + "\n", + "/**", + " * An auto-generated schedule file for the STATIC scheduler.", + " * ", + " * reactor array:", + " * " + this.reactors, + " * ", + " * reaction array:", + " * " + this.reactions, + " */")); + + // Header files + code.pr( + String.join( + "\n", + "#include ", + "#include // size_t", + "#include // ULLONG_MAX", + "#include \"core/environment.h\"", + "#include \"core/threaded/scheduler_instance.h\"", + "#include \"core/threaded/scheduler_static_functions.h\"", + "#include " + "\"" + fileConfig.name + ".h" + "\"")); + + // Include reactor header files. + List tprs = this.reactors.stream().map(it -> it.tpr).toList(); + Set headerNames = new HashSet<>(); + for (var tpr : tprs) headerNames.add(CUtil.getName(tpr)); + for (var name : headerNames) { + code.pr("#include " + "\"" + name + ".h" + "\""); + } + + // Generate label macros. + for (int workerId = 0; workerId < instructions.size(); workerId++) { + List schedule = instructions.get(workerId); + for (int lineNumber = 0; lineNumber < schedule.size(); lineNumber++) { + Instruction inst = schedule.get(lineNumber); + // If the instruction already has a label, print it. + if (inst.hasLabel()) { + List