diff --git a/CMakeLists.txt b/CMakeLists.txt index 34a8a27..d535ad6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,9 +32,11 @@ set(CMAKE_CXX_STANDARD 20) add_library( rt OBJECT src/rt/rt.cc + src/rt/core.cc src/rt/objects/region.cc src/rt/ui/mermaid.cc src/rt/core/builtin.cc + src/rt/core/behaviour.cc ) add_library( diff --git a/docs/builtin.md b/docs/builtin.md index dffd085..c9f1cad 100644 --- a/docs/builtin.md +++ b/docs/builtin.md @@ -36,7 +36,7 @@ Creates a new object from the given prototype. Performs a deep freeze of the object and all referenced objects. -This will move the objects out of their current region into the immutable region. Cowns will stop the freeze propagation, as they can be safely shared across threads and behaviors. +This will move the objects out of their current region into the immutable region. Cowns will stop the freeze propagation, as they can be safely shared across threads and behaviours. #### `close(reg)` diff --git a/src/lang/interpreter.cc b/src/lang/interpreter.cc index 375e09c..24495c4 100644 --- a/src/lang/interpreter.cc +++ b/src/lang/interpreter.cc @@ -1,11 +1,16 @@ +#include "interpreter.h" + +#include "../rt/behaviour.h" #include "../rt/rt.h" #include "bytecode.h" #include "trieste/trieste.h" #include #include +#include #include #include +#include namespace verona::interpreter { @@ -26,25 +31,59 @@ namespace verona::interpreter // ============================================== // Statement Effects // ============================================== + // Handled by Interpreter struct ExecNext {}; + // Handled by Interpreter struct ExecJump { trieste::Location target; }; + // Handled by Interpreter struct ExecFunc { trieste::Node body; size_t arg_ctn; }; + // Handled by Interpreter struct ExecReturn { std::optional value; }; + // Scheduler ######################################### + + struct ExecPrint + { + std::string value; + }; + + struct ExecSchedule + { + std::string value; + }; + + struct ExecBreakpoint + { + std::string value; + }; + + struct MainComplete + {}; + + using AllCommandsVariant = std::variant; + using Subaction_variant = std::variant; + + struct ExecInScheduler + { + Subaction_variant action; + bool exec_complete; + }; + + // ============================================== // Interpreter/state // ============================================== @@ -59,6 +98,19 @@ namespace verona::interpreter { rt::ui::UI* ui; std::vector frame_stack; + rt::core::behaviour_ptr behaviour; + + InterpreterFrame* top_frame() + { + if (frame_stack.empty()) + { + return nullptr; + } + else + { + return frame_stack.back(); + } + } InterpreterFrame* push_stack_frame(trieste::Node body) { @@ -106,7 +158,7 @@ namespace verona::interpreter return frame_stack.front()->frame; } - std::variant + AllCommandsVariant run_stmt(trieste::Node& node) { // ========================================== @@ -114,15 +166,12 @@ namespace verona::interpreter // ========================================== if (node == Print) { - // Console output - std::cout << node->location().view() << std::endl << std::endl; + auto message = std::string(node->location().view()); // Mermaid output - std::vector roots{frame()->object()}; - ui->output(roots, std::string(node->location().view())); + //ui->output(message); - // Continue - return ExecNext{}; + return ExecPrint{message}; } if (node == Label) { @@ -132,7 +181,6 @@ namespace verona::interpreter // ========================================== // Operators that should be printed // ========================================== - std::cout << "Op: " << node->type().str() << std::endl; if (node == CreateObject) { rt::objects::DynObject* obj = nullptr; @@ -427,13 +475,26 @@ namespace verona::interpreter // arguments are still in reverse order on the stack, and the function // can potentially modify the "calling" frame. auto result = (builtin.value())(frame(), arg_ctn); + auto is_schedule = rt::is_schedule_builtin(func); if (result) { auto value = result.value(); frame()->stack_push(value, "result from builtin", false); } rt::remove_reference(frame()->object(), func); - return ExecNext{}; + + if (is_schedule) + { + return ExecSchedule{}; + } + else if(rt::is_breakpoint_builtin(func)) + { + return ExecBreakpoint{}; + } + else + { + return ExecNext{}; + } } else { @@ -477,11 +538,62 @@ namespace verona::interpreter } public: - Interpreter(rt::ui::UI* ui_) : ui(ui_) {} + // Allows printing of schedule/breakpoint + bool prev_schedule_call = false; + bool prev_breakpoint_call = false; + Interpreter( + rt::ui::UI* ui_, + trieste::Node block, + std::vector start_stack, + rt::core::behaviour_ptr behaviour_) + : ui(ui_), behaviour(behaviour_) + { + // There is a question where the active behaviour should be set. + // + // Python mixes the runtime and interpreter a bit more. There the runtime + // has access to the interpreter state. So, it would be possible to store + // the behaviour in the interpreter state and have it accessible to cowns. + // + // However, in FrankenScript the runtime is more passive, meaning that + // the interpreter drives the runtime and provides all needed information. + auto old_behaviour = rt::get_active_behaviour(); + rt::set_active_behaviour(this->behaviour); + + auto frame = push_stack_frame(block); + + for (auto elem : start_stack) + { + frame->frame->stack_push(elem, "staring stack"); + } - void run(trieste::Node main) + rt::set_active_behaviour(old_behaviour); + } + + // resume() helper + template + std::variant narrow_variant(const std::variant& original) { - auto frame = push_stack_frame(main); + return std::visit([](auto&& val) -> std::variant { + using T = std::decay_t; + if constexpr ((std::is_same_v || ...)) + { + return val; // allowed type + } + else + { + throw std::bad_variant_access(); // or handle error + } + }, original); + } + + ExecInScheduler + resume() + { + auto return_to_scheduler{false}; + auto frame = top_frame(); + + rt::set_active_behaviour(this->behaviour); + assert(frame); while (frame) { @@ -523,11 +635,19 @@ namespace verona::interpreter { frame->ip = frame->body->end(); } + else if ( + std::holds_alternative(action) || + std::holds_alternative(action) || + std::holds_alternative(action)) + { + frame->ip++; + return_to_scheduler = true; + } else { - assert(false && "unhandled statement action"); + assert(false && "Unsuported operation"); } - + if (frame->ip == frame->body->end()) { if (std::holds_alternative(action)) @@ -545,11 +665,25 @@ namespace verona::interpreter frame = pop_stack_frame(); } + auto finished{false}; + if (!frame) + { + finished = true; + } + if (return_to_scheduler) + { + auto sub_action = narrow_variant(action); + assert(!std::holds_alternative(sub_action)); + return ExecInScheduler{sub_action, finished}; + } + } + // Last instruction in main might not be any of the other three + return ExecInScheduler{MainComplete{}, true}; } }; - void start(trieste::Node main_body, int step_counter, std::string output) + void start(trieste::Node main_body, int step_counter, std::string output, bool interactive, int seed, bool prompt_steps) { auto ui = rt::ui::globalUI(); ui->set_output_file(output); @@ -557,13 +691,491 @@ namespace verona::interpreter { reinterpret_cast(ui)->set_step_counter(step_counter); } + Scheduler s; - size_t initial = rt::pre_run(ui); + size_t initial = rt::pre_run(ui, &s); - Interpreter inter(ui); - inter.run(main_body); + s.start(new Bytecode{main_body}, interactive, seed, prompt_steps); rt::post_run(initial, ui); } + Scheduler::Scheduler() + { + auto ui = rt::ui::globalUI(); + assert(ui->is_mermaid()); + reinterpret_cast(ui)->scheduler_ready_list = + &this->ready; + } + + Scheduler::~Scheduler() + { + auto ui = rt::ui::globalUI(); + assert(ui->is_mermaid()); + reinterpret_cast(ui)->scheduler_ready_list = nullptr; + } + + std::string format_behaviour_name(std::string name) + { + std::stringstream ss; + ss << "`" << name << "`"; + return ss.str(); + } + + void Scheduler::signal_new_cown(rt::objects::DynObject* cown, rt::core::behaviour_ptr behaviour) + { + // cown must be new + assert(this->cowns.find(cown) == this->cowns.end()); + // This slightly complicates the description of the cowns map, since a cown will now initially map to + // the behaviour that creates it + this->cowns[cown] = behaviour; + } + + void Scheduler::pending_cown_released(rt::objects::DynObject* cown, rt::core::behaviour_ptr behaviour) + { + // Is there a successor waiting on the cown + auto cown_info = behaviour->cown_succ.find(cown); + if (cown_info != behaviour->cown_succ.end()) + { + auto succ = cown_info->second; + succ->cown_ctn -= 1; + if (succ->cown_ctn == 0) + { + succ->status = rt::core::Behaviour::Status::Ready; + this->ready.push_back(succ); + } + behaviour->cown_succ.erase(cown); + + } + } + + + void Scheduler::add(rt::core::behaviour_ptr behaviour) + { + assert(behaviour->status == rt::core::Behaviour::Status::New); + + + for (auto cown : behaviour->cowns) + { + // Get the last behaviour that is waiting on the cown + auto cown_info = this->cowns.find(cown); + if (cown_info != cowns.end()) + { + auto predecessor = cown_info->second; + // If a behaviour isn't Done, set the successor + if (predecessor->status != rt::core::Behaviour::Status::Done) + { + predecessor->cown_succ[cown] = behaviour; + behaviour->cown_ctn += 1; + // Only needed for Mermaid: + if (predecessor->succ.insert(behaviour).second) + { + behaviour->pred_ctn += 1; + } + behaviour->cown_deps[cown] = predecessor.get(); + } + } + // Update pointer to the last pending behaviour + this->cowns[cown] = behaviour; + } + + std::stringstream ss; + if (behaviour->pred_ctn == 0) + { + this->ready.push_back(behaviour); + behaviour->status = rt::core::Behaviour::Status::Ready; + ss << "New behaviour " << format_behaviour_name(behaviour->get_name()) << " is ready"; + } + else + { + behaviour->status = rt::core::Behaviour::Status::Pending; + ss << "New behaviour " << format_behaviour_name(behaviour->get_name()) << " is pending"; + } + + // Two solutions: + // 1. Draw the schedule here and make sure that ExecSchedule doesn't + // draw the scheudle + // 2. Store the message but use it explicitly + this->next_schedule_msg = ss.str(); + //draw_schedule(ss.str()); + //std::cout << "!!! " << "Scheduled `" << format_behaviour_name(behaviour->get_name()) << "`" << std::endl; + } + + void print_help() + { + std::cout << "Commands:" << std::endl; + std::cout << "- : Run behaviour number b until the next break point" << std::endl; + std::cout << "- s, : Run behaviour number b n step (default n = 0) [Default]" << std::endl; + std::cout << "- h : Prints this message " << std::endl << std::endl; + } + + + std::string processLines(const std::string& input, const size_t desired_lines) { + std::istringstream iss(input); + std::string line; + + // Split input into lines + size_t i{0}; + std::string result = ""; + while (std::getline(iss, line) && i < desired_lines) + { + result += line + "\n"; + i++; + } + if (std::getline(iss, line)) + { + result += " {...}\n"; + } + + return result; + } + + void Scheduler::start(Bytecode* main_block, bool i, int s, bool prompt_steps) + { + auto main_function = rt::make_func(main_block); + // Hack: Needed to keep the main function alive. Otherwise, it'll be freed + // thereby also deleting the trieste nodes. + rt::hack_inc_rc(main_function); + prompt_user_for_steps = false; + this->interactive = i; + this->rng.seed(s); + this->prompt_user_for_steps = prompt_steps; + // :notes: I imagine a world without ugly c++ :notes: + auto behaviour = std::make_shared( + main_function, std::vector{}, this, "main"); + behaviour->status = rt::core::Behaviour::Status::Ready; + this->ready.push_back(behaviour); + + if (this->interactive) + { + print_help(); + } + + + while (behaviour) + { + Interpreter* inter; + if (behaviour->status == rt::core::Behaviour::Status::Ready) + { + auto block = behaviour->spawn(); + + inter = new Interpreter( + rt::ui::globalUI(), block->body, behaviour->cowns, behaviour); + this->running[behaviour] = inter; + } + else if (behaviour->status == rt::core::Behaviour::Status::Running) + { + inter = this->running[behaviour]; + assert(inter); + } + else + { + assert(false && "HOW DID IT BREAK THIS BADLY?"); + } + + + this->current_int = inter; + auto result = inter->resume(); + auto action = result.action; + auto should_break{false}; + if (std::holds_alternative(action)) + { + auto line_string = std::get(action).value; + auto shortened_string = processLines(line_string, 4); + std::stringstream draw_ss; + std::stringstream terminal_ss; + draw_ss << shortened_string << std::endl; + terminal_ss << ">>> " << draw_ss.str(); + if (this->current_int->prev_schedule_call) + { + this->current_int->prev_schedule_call = false; + draw_ss << this->next_schedule_msg << std::endl; + terminal_ss << "!!! " << "Scheduled new behaviour" << std::endl; + should_break = true; + } + else if (this->current_int->prev_breakpoint_call) + { + this->current_int->prev_breakpoint_call = false; + draw_ss << "Reached breakpoint in " << format_behaviour_name(behaviour->get_name()) << std::endl; + terminal_ss << "!!! " << "Reached breakpoint in " << format_behaviour_name(behaviour->get_name()) << std::endl; + should_break = true; + } + std::cout << terminal_ss.str(); + draw_schedule(draw_ss.str()); + if (this->interactive) + { + if (steps == 0) + { + should_break = true; + } + steps--; + } + else + { + should_break = true; + } + + + } + else if (std::holds_alternative(action)){ + this->current_int->prev_schedule_call = true; + // Assumption: Schedule is implemented through a builtin function call + // Its Call node will always be followed by a Print node + assert(!result.exec_complete); + } + else if (std::holds_alternative(action)) + { + // Assumption: Breakpoint is implemented through a builtin function call + // Its Call node will always be followed by a Print node + assert(!result.exec_complete); + } + else if (std::holds_alternative(action)) + { + assert(result.exec_complete); + } + else + { + assert(false && "Unsuported operation"); + } + + if (result.exec_complete) + { + should_break = true; + std::cout << "!!! " << "Completed " << format_behaviour_name(behaviour->get_name()) << std::endl; + this->complete(behaviour); + std::stringstream ss; + ss << "Completed " << format_behaviour_name(behaviour->get_name()) << std::endl; + draw_schedule(ss.str()); + } + if (should_break) + { + behaviour = this->get_next(); + if (this->interactive && behaviour) + { + std::stringstream ss; + ss << "Entering behaviour " << format_behaviour_name(behaviour->get_name()) << std::endl; + std::cout << "!!! " << ss.str(); + draw_schedule(ss.str(), true); + } + } + // "Always" call if not interactive, otherwise there is no way + // to seed for certain execution strains. + // The expection being breakpoints/schedule + // else if (std::holds_alternative(action)) + // { + // behaviour = this->get_next(); + // } + + + + + + } + + rt::remove_reference(nullptr, main_function); + } + + void Scheduler::prompt_steps() + { + if (this->first_break) + { + print_help(); + first_break = false; + } + } + + + + + void Scheduler::complete(rt::core::behaviour_ptr behaviour) + { + behaviour->complete(); + std::erase(this->ready, behaviour); + for (auto cown_info : behaviour->cown_succ) + { + auto succ = cown_info.second; + succ->cown_ctn -= 1; + if (succ->cown_ctn == 0) + { + succ->status = rt::core::Behaviour::Status::Ready; + this->ready.push_back(succ); + } + } + behaviour->cown_succ.clear(); + + this->completed_behaviours.push_back(behaviour->get_name()); + + // for (auto succ : behaviour->succ) + // { + // succ->pred_ctn -= 1; + // if (succ->pred_ctn == 0) + // { + // succ->status = rt::core::Behaviour::Status::Ready; + // this->ready.push_back(succ); + // } + // } + behaviour->succ.clear(); + } + + void Scheduler::draw_schedule(std::string message, bool bp) + { + + // FIXME: We should really get wrid of the UI* abstraction. There is no way + // that we'll ever change the output at this point and it just makes several + // things harder, like this: + auto ui = rt::ui::globalUI(); + assert(ui->is_mermaid()); + auto mermaid = reinterpret_cast(ui); + if (bp) + { + mermaid->close_file(); + } + mermaid->output(message); + } + + rt::core::behaviour_ptr Scheduler::get_next() + { + if (this->ready.empty()) + { + return nullptr; + } + + size_t selected; + if (this->interactive) + { + while (true) + { + // Prompt the user: + std::cout << std::endl; + std::cout << "Available behaviours:" << std::endl; + for (unsigned int idx = 0; idx < this->ready.size(); idx += 1) + { + auto b = this->ready[idx]; + std::cout << "- " << idx << ": " << b->get_name(); + + if (b->status == rt::core::Behaviour::Status::Running) + { + std::cout << " (continue)"; + } + std::cout << std::endl; + } + + // Get user input + std::cout << "> "; + std::string line; + std::getline(std::cin, line); + + // Check for quit + if (line == "q") + { + exit(0); + } + + if (line == "h") + { + print_help(); + continue; + } + + + // Check for step command + if (line[0] == 's') + { + if (line.size() > 1) + { + size_t comma_pos = line.find(','); + // s, + if (comma_pos != std::string::npos) + { + std::string idx_str = line.substr(1, comma_pos - 1); + std::string count_str = line.substr(comma_pos + 1); + + size_t idx = 0, count = 0; + std::istringstream idx_iss(idx_str); + std::istringstream count_iss(count_str); + + if (idx_iss >> idx && count_iss >> count && idx < this->ready.size()) + { + selected = idx; + steps = count; + //std::cout << "\nStepping behaviour " << idx << " for " << steps << " times." << std::endl; + break; + } + } + // s + std::istringstream iss(line.substr(1)); + size_t n = 0; + if (iss >> n && n < this->ready.size()) + { + selected = n; + steps = 0; + break; + } + } + // s + else if (this->ready.size() == 1) + { + selected = 0; + steps = 0; + break; + } + + } + else + { + steps = std::numeric_limits::max(); + // Check for Enter press + if (this->ready.size() == 1 && line == "") + { + selected = 0; + break; + } + // Handle normal selection + std::istringstream iss(line); + size_t n = 0; + if (iss >> n && n < this->ready.size()) + { + selected = n; + break; + } + } + + } + } + else + { + std::uniform_int_distribution dist(0, this->ready.size() - 1); + selected = dist(rng); + } + + auto behaviour = this->ready[selected]; + return behaviour; + } + + // ################### TESTING FUNCTIONALITY #################################### + + bool Scheduler::is_executable(const std::string behaviour_name) + { + for (auto behaviour : this->ready) + { + if (behaviour->get_name() == behaviour_name) + { + return true; + } + } + return false; + } + + bool Scheduler::is_complete(const std::string sought_name) + { + for (auto behaviour_name : this->completed_behaviours) + { + if (behaviour_name == sought_name) + { + return true; + } + } + return false; + } + } // namespace verona::interpreter diff --git a/src/lang/interpreter.h b/src/lang/interpreter.h index 8edd2ab..9a47a49 100644 --- a/src/lang/interpreter.h +++ b/src/lang/interpreter.h @@ -1,6 +1,14 @@ #pragma once +#include "../rt/behaviour.h" + #include +#include +#include +#include +#include +#include +#include namespace rt::objects { @@ -9,6 +17,8 @@ namespace rt::objects namespace verona::interpreter { + // Move Interpreter def here (Including Exec* structs) + class Interpreter; struct Bytecode; void delete_bytecode(Bytecode* bytecode); @@ -33,4 +43,73 @@ namespace verona::interpreter return this->get_stack_size() == 0; } }; + + // FIXME: The implementation of this should probably be in a different file... + class Scheduler + { + // All behaviours that are ready to run + std::vector ready = {}; + // A map from cowns to the last behaviour that will as some point own them. + // Ergo this will initially map to the creating behaviour + // + // The cowns in the key are weak pointers, they should never be + // dereferenced. + std::unordered_map cowns = + {}; + // This feels hacky but also like the best solution? I can't even blame this + // on C++ + std::unordered_map running = {}; + + // Necessary to print the line and information of a scheduled behaviour in + // one go, since Call nodes do not store what line they were called from + std::string next_schedule_msg; + + // FIXME: + // This should likely be gotten by requesting the current + // behaviour in the runtime and then looking up the interpreter + // from the behaviour. But no, this is faster; + Interpreter* current_int; + + /// @brief Do we desire interactive execution + bool interactive{true}; + /// @brief Indicates if this is the first break and the help message should + /// be printed. + bool first_break{true}; + /// @brief Indicates how many steps should be taken until + /// prompting user again, in the case of interactive exec + size_t steps{0}; + // For faster debugging + bool prompt_user_for_steps{true}; + + /// @brief RNG for concurrency + std::mt19937 rng; + + // Track completed behaviours, only needed for testing + // Onus is on user to provide distinct names, if manual + // ones are used + std::vector completed_behaviours; + + public: + Scheduler(); + ~Scheduler(); + + + void add(rt::core::behaviour_ptr behaviour); + + void start(Bytecode* main_block, bool interactive, int seed, bool prompt_steps); + + void signal_new_cown(rt::objects::DynObject* cown, rt::core::behaviour_ptr behaviour); + void pending_cown_released(rt::objects::DynObject* cown, rt::core::behaviour_ptr behaviour); + //void new_pending_cown(rt::objects::DynObject* cown, rt::core::behaviour_ptr behaviour); + + // Used for testing + bool is_executable(const std::string behaviour_name); + bool is_complete(const std::string behaviour_name); + + private: + void prompt_steps(); + void complete(rt::core::behaviour_ptr behaviour); + void draw_schedule(std::string message, bool bp = false); + rt::core::behaviour_ptr get_next(); + }; } diff --git a/src/lang/lang.cc b/src/lang/lang.cc index aeea0e4..efd7222 100644 --- a/src/lang/lang.cc +++ b/src/lang/lang.cc @@ -22,25 +22,31 @@ std::pair>> extract_bytecode_pass() namespace verona::interpreter { - void start(trieste::Node main_body, int step_counter, std::string output); + void start(trieste::Node main_body, int step_counter, std::string output, bool interactive, int seed, bool prompt_steps); } struct CLIOptions : trieste::Options { int step_counter = std::numeric_limits::max(); std::string out = "mermaid.md"; + bool interactive{false}; + int seed = 42; + bool prompt_steps{false}; void configure(CLI::App& app) { app.add_flag( "-i,--interactive", - [&](auto) { step_counter = 0; }, + //[&](auto) { step_counter = 0; }, + [&](auto) {interactive = true;}, "Run the interpreter iteratively"); app.add_option( "-s,--step", step_counter, "Step n instructions before entering interactive mode"); app.add_option("--out", out, "The output file for frankenscript"); + app.add_option("--seed", seed, "Seed when selecting which concurrent unit to run"); + app.add_flag("--prompt_steps", [&](auto) {prompt_steps = true;}, "Prompt user for steps"); } void validate() @@ -72,7 +78,7 @@ int load_trieste(int argc, char** argv) if (build_res == 0 && result->has_value()) { verona::interpreter::start( - result->value(), options.step_counter, options.out); + result->value(), options.step_counter, options.out, options.interactive, options.seed, options.prompt_steps); } return build_res; } diff --git a/src/lang/lang.h b/src/lang/lang.h index b475c22..33f6a2c 100644 --- a/src/lang/lang.h +++ b/src/lang/lang.h @@ -20,6 +20,8 @@ inline const TokenDef Move{"move"}; inline const TokenDef Lookup{"lookup"}; inline const TokenDef Parens{"parens"}; inline const TokenDef Method{"method"}; +inline const TokenDef When{"when"}; +inline const TokenDef Name{"Name"}; inline const TokenDef Op{"op"}; inline const TokenDef Rhs{"rhs"}; diff --git a/src/lang/passes/call_stmts.cc b/src/lang/passes/call_stmts.cc index 2d53166..0b25231 100644 --- a/src/lang/passes/call_stmts.cc +++ b/src/lang/passes/call_stmts.cc @@ -17,9 +17,24 @@ PassDef call_stmts() verona::wf::call_stmts, dir::bottomup | dir::once, { - In(Block) * T(Call, Method)[Call] >> + In(Block) * (T(Call)[Call] << T(Ident)[Ident]) >> [](auto& _) { - return Seq << _(Call) << ClearStack << create_print(_(Call)); + // if (_(Ident)->location().view() == "spawn_behaviour") + // { + // return Seq << _(Call); + // } + // else if (_(Ident)->location().view() == "breakpoint") + // { + // return Seq << _(Call); + // } + if(true) + { + return Seq << _(Call) << ClearStack << create_print(_(Call)); + } + }, + In(Block) * T(Method)[Method] >> + [](auto& _) { + return Seq << _(Method) << ClearStack << create_print(_(Method)); }, }}; } diff --git a/src/lang/passes/flatten.cc b/src/lang/passes/flatten.cc index 04b83b6..f621223 100644 --- a/src/lang/passes/flatten.cc +++ b/src/lang/passes/flatten.cc @@ -171,10 +171,16 @@ PassDef flatten() } body << create_print(_(Func), func_head + " (Exit)"); + auto result = Seq << (CreateObject << (Func << (Compile << body))) + << (StoreFrame ^ _(Ident)); + auto def_text = func_head; + if (!_(Ident)->location().view().starts_with("__when_")) + { + result << create_print(_(Func), def_text); + // def_text = std::string("Creating behaviour from: ") + func_head; + } // Function cleanup - return Seq << (CreateObject << (Func << (Compile << body))) - << (StoreFrame ^ _(Ident)) - << create_print(_(Func), func_head); + return result; }, }}; } diff --git a/src/lang/passes/grouping.cc b/src/lang/passes/grouping.cc index e96810b..1381e76 100644 --- a/src/lang/passes/grouping.cc +++ b/src/lang/passes/grouping.cc @@ -2,6 +2,14 @@ inline const TokenDef Rest{"rest"}; +int g_when_counter = 0; + +std::string new_when_ident() +{ + g_when_counter += 1; + return "__when_" + std::to_string(g_when_counter); +} + PassDef grouping() { PassDef p{ @@ -57,6 +65,42 @@ PassDef grouping() return create_from(Method, _(Group)) << _(Lookup) << list; }, + T(When)[When] << (T(Group)[Empty] * T(Group)[Block] * End) >> + [](auto& _) { + return create_from(When, _(When)) + << _(Empty) << (Group << Parens) << _(Block); + }, + ~(T(Group) << T(Name)[Name]) * + (T(When)[When] + << ((T(Group)) * + (T(Group) + << (T(Parens)[Parens] << ((~(T(List) << T(Ident)++[List]))))) * + (T(Group) << T(Block)[Block]))) >> + [](auto& _) { + auto when_name = new_when_ident(); + + // ===================================== + // Define `__when_X()` function + auto when_def = create_from(Func, _(When)) + << (Ident ^ when_name) + << (create_from(Params, _(Parens)) << clone(_[List])) + << (Body << _(Block)); + + // ===================================== + // Call `spawn_behaviour()` + auto args = create_from(List, _(Parens)) << (Ident ^ when_name); + if (_(Name)) + { + args = args << create_from(String, _(Name)); + } + args = args << clone(_[List]); + auto call = create_from(Call, _(When)) + << (Ident ^ "spawn_behaviour") << args; + + // Put it all together + return Seq << when_def << call; + }, + T(Assign) << ((T(Group) << LV[Lhs] * End) * ((T(Group) << (RV[Rhs] * End)) / (RV[Rhs] * End)) * End) >> diff --git a/src/lang/passes/parse.cc b/src/lang/passes/parse.cc index 0d6475a..f7b6b36 100644 --- a/src/lang/passes/parse.cc +++ b/src/lang/passes/parse.cc @@ -5,16 +5,16 @@ namespace verona::wf using namespace trieste::wf; inline const auto parse_tokens = - Ident | Lookup | Empty | Drop | Move | Null | String | Parens; - inline const auto parse_groups = - Group | Assign | If | Else | Block | For | Func | List | Return | While; + Ident | Lookup | Empty | Drop | Move | Null | String | Parens | Name; + inline const auto parse_groups = Group | Assign | If | Else | Block | For | + Func | List | Return | While | When; inline const auto parser = (Top <<= File) | (File <<= parse_groups++) | (Assign <<= Group * (Lhs >>= (Group | cond))) | (If <<= Group * (Op >>= (cond | Group)) * Group) | (Else <<= Group * Group) | (Group <<= (parse_tokens | Block | List)++) | (Block <<= (parse_tokens | parse_groups)++) | (Eq <<= Group * Group) | - (Neq <<= Group * Group) | (Lookup <<= Group) | + (Neq <<= Group * Group) | (Lookup <<= Group) | (When <<= Group++) | (For <<= Group * List * Group * Group) | (While <<= Group * (Op >>= (cond | Group)) * Group) | (List <<= Group++) | (Parens <<= (Group | List)++) | (Func <<= Group * Group * Group) | @@ -96,6 +96,8 @@ trieste::Parse parser() "(?:#[^\\n\\r]*)" >> [](auto&) {}, "def\\b" >> [](auto& m) { m.seq(Func); }, + "@name\\(\"([^\\n\"]+)\"\\)" >> [](auto& m) { m.add(Name, 1); }, + "when\\b" >> [](auto& m) { m.seq(When); }, "\\(" >> [](auto& m) { m.push(Parens); }, "\\)" >> [](auto& m) { @@ -149,6 +151,10 @@ trieste::Parse parser() { toc = While; } + else if (m.in(When)) + { + toc = When; + } else { m.error("unexpected colon"); diff --git a/src/rt/behaviour.h b/src/rt/behaviour.h new file mode 100644 index 0000000..61613fc --- /dev/null +++ b/src/rt/behaviour.h @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace verona::interpreter +{ + struct Bytecode; + class Scheduler; +} + +namespace rt::objects +{ + class DynObject; + struct Region; +} // namespace rt::objects + +namespace rt::ui +{ + class MermaidUI; + class ObjectGraphDiagram; +} + +namespace rt::core +{ + class Behaviour + { + friend class rt::ui::MermaidUI; + friend class rt::ui::ObjectGraphDiagram; + + public: + enum class Status + { + New, + Pending, + Ready, + Running, + Done, + }; + + static std::string status_to_string(Status status) + { + switch (status) + { + case Status::New: + return "New"; + case Status::Pending: + return "Pending"; + case Status::Ready: + return "Ready"; + case Status::Running: + return "Running"; + case Status::Done: + return "Done"; + default: + return "Unknown"; + } + } + + static void set_active_behaviour(std::shared_ptr); + static std::shared_ptr get_active_behaviour(); + + private: + static std::shared_ptr s_active_behaviour; + + private: + // Static member for naming + static int s_behaviour_counter; + + // A unique ID, this is used for drawing and naming, it isn't needed for + // scheduling. + int id; + std::string name; + + // The IDs of the cowns this behaviour is waiting on. This is used to create + // a better mermaid diagram, it isn't needed for scheduling. + std::map ordered_cown; + + // The local region of this behaviour. This has to be swapped into the global + // `local_region` when this behaviour runs. + objects::Region* local_region; + // "Necessary" to signal the status change 'Pending --> Released' for cowns to the Scheduler + // Ergo change is propagated like so: cown->behaviour(owner)->scheduler + verona::interpreter::Scheduler* scheduler; + + public: + // This maps the cowns of this behaviour to the previous behaviour this + // is waiting on. This is used to draw the dependencies, it is not used + // for sceduling. + // Both of these pointers are weak reference. + std::map cown_deps; + + Status status; + // The cowns as they were passed in to the behaviour. These have to be provided + // to the new Interpreter to populate the frame. Note that creating a + // cown in a behaviour will expand its set of cowns. Ergo, the set of aquired + // cowns is a subset. + // TODO split created and aquired cowns into two separate structures + std::vector cowns; + // This uses a function object opposed to a Bytecode* to not leak memory + objects::DynObject* code; + // The number of behaviours that this behaviour is waiting on + int pred_ctn = 0; + // Number of cowns behaviour is waiting on + size_t cown_ctn{0}; + // Behaviours which are waiting on this behaviour. These will be notified once + // this behaviour completes. + // Currently needed for mermaid + std::set> succ; + // Map from a cown to the next behaviour waiting for it, needed since all cowns are not + // necessarily released at the end of behaviours. + std::map> cown_succ; + + + Behaviour( + objects::DynObject* code_, + std::vector cowns_, + verona::interpreter::Scheduler* scheduler_, + std::optional name_ = std::nullopt); + + std::string get_name(); + std::string id_str(); + + verona::interpreter::Bytecode* spawn(); + // This completes the behaviour by releasing all cowns + // decreffing all held objects + void complete(); + + void signal_new_cown(rt::objects::DynObject* cown); + void signal_early_release(rt::objects::DynObject* cown); + }; + + typedef std::shared_ptr behaviour_ptr; +} // namespace rt::core diff --git a/src/rt/core.cc b/src/rt/core.cc new file mode 100644 index 0000000..67a0af8 --- /dev/null +++ b/src/rt/core.cc @@ -0,0 +1,7 @@ +#include "core.h" + +namespace rt::core +{ + int FrameObject::s_frame_id_counter = 1; + int CownObject::s_id_counter = 1; +} \ No newline at end of file diff --git a/src/rt/core.h b/src/rt/core.h index 58c10b5..5f805fc 100644 --- a/src/rt/core.h +++ b/src/rt/core.h @@ -1,4 +1,5 @@ #include "../lang/interpreter.h" +#include "behaviour.h" #include "objects/prototype_object.h" #include "objects/region.h" #include "objects/region_object.h" @@ -8,6 +9,9 @@ namespace rt::core { + const std::string schedule_func_name = "spawn_behaviour"; + const std::string breakpoint_func_name = "breakpoint"; + using PrototypeObject = objects::PrototypeObject; inline PrototypeObject* framePrototypeObject() @@ -19,6 +23,7 @@ namespace rt::core class FrameObject : public objects::DynObject, public verona::interpreter::FrameObj { + static int s_frame_id_counter; static constexpr std::string_view STACK_PREFIX = "_stack"; static inline thread_local std::vector stack_keys; size_t stack_size = 0; @@ -47,6 +52,10 @@ namespace rt::core objects::add_reference(this, parent_frame); assert(!old_value); } + + std::stringstream ss; + ss << ""; + name = ss.str(); } static FrameObject* create_first_stack() @@ -66,7 +75,6 @@ namespace rt::core assert(old == nullptr && "the stack already had a value"); stack_size += 1; - std::cout << "pushed " << value << " (" << info << ")" << std::endl; if (rc_add) { rt::add_reference(this, value); @@ -77,7 +85,6 @@ namespace rt::core { stack_size -= 1; auto value = erase(stack_name(stack_size)); - std::cout << "poped " << value << " (" << value << ")" << std::endl; return value; } @@ -256,6 +263,8 @@ namespace rt::core class CownObject : public objects::DynObject { private: + static int s_id_counter; + enum class Status { Pending, @@ -279,14 +288,40 @@ namespace rt::core } Status status; + int id; + core::Behaviour* owner; public: - CownObject(objects::DynObject* obj) + CownObject( + objects::DynObject* obj, std::optional name_ = std::nullopt) : objects::DynObject(cownPrototypeObject(), objects::cown_region) { + id = s_id_counter++; + status = Status::Pending; + this->owner = Behaviour::get_active_behaviour().get(); auto old = set("value", obj); assert(!old); + + // This is really wonky. The scheduler should actually know about this + // new cown, but meh? + if (this->status == Status::Pending) + { + this->change_rc(1); + this->owner->signal_new_cown(this); + //verona::interpreter::Scheduler::new_pending_cown(this, this->owner); + } + + if (name_) + { + name = name_.value(); + } + else + { + std::stringstream ss; + ss << "id << ">"; + name = ss.str(); + } } [[nodiscard]] DynObject* set(std::string name, DynObject* obj) override @@ -333,11 +368,23 @@ namespace rt::core return old; } - std::string get_name() override + // A unique cown ID + int get_id() + { + return this->id; + } + + // TODO: This should really be split into `get_name()` just getting the name + // and `get_info()` or the additional info text like lrc and status + std::optional get_additional_info() override { std::stringstream ss; - ss << "" << std::endl; ss << "status=" << to_string(status); + if (status == Status::Pending || status == Status::Acquired) + { + assert(this->owner); + ss << " (" << this->owner->get_name() << ")"; + } return ss.str(); } @@ -354,7 +401,7 @@ namespace rt::core // but this is single threaded case Status::Acquired: case Status::Pending: - return false; + return Behaviour::get_active_behaviour().get() != this->owner; case Status::Released: default: return true; @@ -379,6 +426,7 @@ namespace rt::core if (!value || value->is_immutable() || value->is_cown()) { status = Status::Released; + this->owner = nullptr; return; } @@ -386,8 +434,31 @@ namespace rt::core if (region->combined_lrc() == 0) { status = Status::Released; + this->owner->signal_early_release(this); + this->owner = nullptr; } } + + void aquire(Behaviour* behaviour) + { + // Who needs other safety checks than this? + // This is so gonna bite me... + assert(this->status == Status::Released); + + this->status = Status::Acquired; + this->owner = behaviour; + } + + void release(Behaviour* behaviour) + { + assert(this->owner == behaviour); + this->status = Status::Released; + this->owner = nullptr; + } + bool is_owner(Behaviour* behaviour) + { + return this->owner == behaviour; + } }; inline std::set* globals() @@ -408,6 +479,22 @@ namespace rt::core return globals; } + inline std::set* global_prototypes() + { + static std::set* globals = + new std::set{ + objects::regionPrototypeObject(), + framePrototypeObject(), + funcPrototypeObject(), + bytecodeFuncPrototypeObject(), + builtinFuncPrototypeObject(), + stringPrototypeObject(), + keyIterPrototypeObject(), + cownPrototypeObject(), + }; + return globals; + } + inline std::map* global_names() { static std::map* global_names = @@ -422,5 +509,5 @@ namespace rt::core /// /// @param ui The UI to allow builtin functions to create output, when they're /// called. - void init_builtins(ui::UI* ui); + void init_builtins(ui::UI* ui, verona::interpreter::Scheduler* scheduler); } // namespace rt::core diff --git a/src/rt/core/behaviour.cc b/src/rt/core/behaviour.cc new file mode 100644 index 0000000..7a5744b --- /dev/null +++ b/src/rt/core/behaviour.cc @@ -0,0 +1,125 @@ +#include "../behaviour.h" + +#include "../objects/region.h" +#include "../rt.h" + +#include +#include + +namespace rt::objects +{ + void set_local_region(Region* region); +} + +namespace rt::core +{ + int Behaviour::s_behaviour_counter = 0; + std::shared_ptr Behaviour::s_active_behaviour = nullptr; + + void Behaviour::set_active_behaviour(std::shared_ptr active) + { + s_active_behaviour = active; + if (active) + { + objects::set_local_region(active->local_region); + } + } + + std::shared_ptr Behaviour::get_active_behaviour() + { + return s_active_behaviour; + } + + Behaviour::Behaviour( + rt::objects::DynObject* code_, + std::vector cowns_, + verona::interpreter::Scheduler* scheduler_, + std::optional name_) + : id(s_behaviour_counter++), cowns(cowns_), code(code_), scheduler(scheduler_) + { + for (auto c : cowns) + { + ordered_cown[rt::get_cown_id(c)] = c; + } + + if (name_) + { + name = name_.value(); + } + else + { + std::stringstream ss; + ss << "Behaviour_" << id; + name = ss.str(); + } + } + + std::string Behaviour::get_name() + { + return this->name; + } + + std::string Behaviour::id_str() + { + std::stringstream ss; + ss << "B" << this->id; + return ss.str(); + } + + verona::interpreter::Bytecode* Behaviour::spawn() + { + assert(this->status == Status::Ready); + this->status = Status::Running; + + for (auto c : this->cowns) + { + rt::aquire_cown(c, this); + } + + this->local_region = objects::Region::new_local_region(); + + return rt::try_get_bytecode(this->code).value(); + } + + // FIXME: Currently both the scheduler and the behaviour has a function + // to complete a behaviour. All of this should really be in one place. It + // might be better to move all of this into the scheduler. + void Behaviour::complete() + { + this->status = Status::Done; + rt::remove_reference(nullptr, this->code); + this->code = nullptr; + + for (auto c : this->cowns) + { + if (rt::is_owner(c, this)) + { + rt::release_cown(c, this); + } + rt::remove_reference(nullptr, c); + } + this->cowns.clear(); + + for (auto [cown, waiting_on] : cown_deps) + { + if (waiting_on == this) + { + cown_deps.erase(cown); + } + } + } + + void Behaviour::signal_new_cown(rt::objects::DynObject* cown) + { + this->cowns.push_back(cown); + this->scheduler->signal_new_cown(cown, s_active_behaviour); + } + + void Behaviour::signal_early_release(rt::objects::DynObject* cown) + { + // assert(cown in cowns) + this->scheduler->pending_cown_released(cown, s_active_behaviour); + } + + +} // namespace rt::core diff --git a/src/rt/core/builtin.cc b/src/rt/core/builtin.cc index fb28da2..5f2c465 100644 --- a/src/rt/core/builtin.cc +++ b/src/rt/core/builtin.cc @@ -1,3 +1,4 @@ +#include "../behaviour.h" #include "../core.h" #include "../rt.h" @@ -166,13 +167,15 @@ namespace rt::core return std::nullopt; }); - add_builtin("breakpoint", [mermaid](auto, auto args) { + // Handled in Scheduler + add_builtin(rt::core::breakpoint_func_name, [mermaid](auto, auto args) { + if (args != 0) { ui::error("breakpoint() expected 0 arguments"); } - mermaid->break_next(); + //mermaid->break_next(); return std::nullopt; }); @@ -181,14 +184,21 @@ namespace rt::core void ctor_builtins() { add_builtin("Cown", [](auto frame, auto args) { - if (args != 1) + if (args < 1 && args > 2) + { + ui::error("Cown() expected 1 or 2 arguments"); + } + + objects::DynObject* name = nullptr; + if (args == 2) { - ui::error("Cown() expected 1 argument"); + name = frame->stack_pop("name"); } auto region = frame->stack_pop("region for cown creation"); - auto cown = make_cown(region); + auto cown = make_cown(region, name); rt::move_reference(frame->object(), cown, region); + rt::remove_reference(frame->object(), name); return cown; }); @@ -218,7 +228,7 @@ namespace rt::core } bool close_function_impl( - verona::interpreter::FrameObj* frame, size_t args, bool force_close) + verona::interpreter::FrameObj* frame, size_t args) { if (args != 1) { @@ -255,15 +265,9 @@ namespace rt::core { // We have to remove our reference, as it would otherwise break the // `is_closed()` check from the forced close + std::cout << "yep" << std::endl; rt::remove_reference(frame->object(), bridge); - if (force_close) - { - region->close(); - } - else - { - region->try_close(); - } + region->try_close(); } return region->is_closed(); @@ -297,11 +301,11 @@ namespace rt::core }); add_builtin("close", [](auto frame, auto args) { - close_function_impl(frame, args, true); + close_function_impl(frame, args); return std::nullopt; }); add_builtin("is_closed", [](auto frame, auto args) { - auto result = close_function_impl(frame, args, false); + auto result = close_function_impl(frame, args); auto result_obj = rt::get_bool(result); // The return will be linked to the frame by the interpreter, but the RC // has to be increased here. @@ -352,6 +356,26 @@ namespace rt::core return result_obj; }); + + add_builtin("print", [](auto frame, auto args) { + if (args != 1) + { + ui::error("print() expected 1 argument"); + } + + auto value = frame->stack_pop("value to print"); + auto name = value->get_name(); + // Lazy way of dealing with stringPrototypeObject + if (name[0] == '\"') + { + name.erase(0, 1); + name.erase(name.size() - 1); + } + std::cout << name << std::endl; + rt::remove_reference(frame->object(), value); + + return std::nullopt; + }); } void pragma_builtins() @@ -390,11 +414,104 @@ namespace rt::core }); } - void init_builtins(ui::UI* ui) + void concurrency_builtins(verona::interpreter::Scheduler* scheduler) + { + add_builtin(rt::core::schedule_func_name, [=](auto frame, auto args) { + // cowns (Stored on the stack in reverse order) + // -1 since the first argument is the actual behaviour + std::vector cowns = {}; + for (int i = 0; i < args - 1; i++) + { + auto value = frame->stack_pop("cown"); + cowns.push_back(value); + } + + std::optional name; + // The last argument might be a name for the behaviour + if ( + !cowns.empty() && + cowns.back()->get_prototype() == rt::core::stringPrototypeObject()) + { + auto name_obj = cowns.back(); + name = dynamic_cast(name_obj)->as_key(); + rt::remove_reference(frame->object(), name_obj); + cowns.pop_back(); + } + // when + auto behaviour = frame->stack_pop("behaviour"); + scheduler->add( + std::make_shared(behaviour, cowns, scheduler, name)); + + // @Max, Interesting for your report: Some kind of ownership transfer is + // needed here. Freezing is "the easiest" untill we get into the mess that + // function objects in cpython are. It could be interesting to see if we + // can't just transfer ownership to the behaviour region. + freeze(behaviour); + + return std::nullopt; + }); + add_builtin("is_executable", [=](auto frame, auto args) { + + if (args != 1) + { + std::stringstream ss; + ss << "is_executable" << " expected 1 argument"; + ui::error(ss.str()); + } + + auto behaviour_name = frame->stack_pop("behaviour_name"); + if (behaviour_name->get_prototype() != stringPrototypeObject()) + { + ui::error("given count is not a string", behaviour_name); + } + // Remove string object whitespace + auto s = behaviour_name->get_name(); + if (s[0] == '\"') + { + s.erase(0, 1); + s.erase(s.size() - 1); + } + auto result = scheduler->is_executable(s); + auto result_obj = rt::get_bool(result); + result_obj->change_rc(1); + rt::remove_reference(frame->object(), behaviour_name); + return result_obj; + }); + add_builtin("is_complete", [=](auto frame, auto args) { + + if (args != 1) + { + std::stringstream ss; + ss << "is_complete" << " expected 1 argument"; + ui::error(ss.str()); + } + + auto behaviour_name = frame->stack_pop("behaviour_name"); + if (behaviour_name->get_prototype() != stringPrototypeObject()) + { + ui::error("given count is not a string", behaviour_name); + } + // Remove string object whitespace + auto s = behaviour_name->get_name(); + if (s[0] == '\"') + { + s.erase(0, 1); + s.erase(s.size() - 1); + } + auto result = scheduler->is_complete(s); + auto result_obj = rt::get_bool(result); + result_obj->change_rc(1); + rt::remove_reference(frame->object(), behaviour_name); + return result_obj; + }); + } + + void init_builtins(ui::UI* ui, verona::interpreter::Scheduler* scheduler) { mermaid_builtins(ui); ctor_builtins(); action_builtins(); pragma_builtins(); + concurrency_builtins(scheduler); } } diff --git a/src/rt/objects/dyn_object.h b/src/rt/objects/dyn_object.h index 41853e4..995632f 100644 --- a/src/rt/objects/dyn_object.h +++ b/src/rt/objects/dyn_object.h @@ -30,7 +30,6 @@ namespace rt::objects Region* get_region(DynObject* obj); Region* get_local_region(); - void set_local_region(Region* region); // Representation of objects class DynObject @@ -38,7 +37,7 @@ namespace rt::objects friend class Reference; friend objects::DynObject* rt::make_iter(objects::DynObject* obj); friend class ui::MermaidUI; - friend class ui::MermaidDiagram; + friend class ui::ObjectGraphDiagram; friend class core::CownObject; friend void destruct(DynObject* obj); friend void dealloc(DynObject* obj); @@ -60,11 +59,12 @@ namespace rt::objects std::map fields{}; + protected: + std::string name; + public: size_t change_rc(signed delta) { - std::cout << "Change RC: " << get_name() << " " << rc << " + " << delta - << std::endl; if (!(is_immutable() || is_cown())) { assert(delta == 0 || rc != 0); @@ -94,10 +94,12 @@ namespace rt::objects if (prototype != nullptr) { - // prototype->change_rc(1); objects::add_reference(this, prototype); } - std::cout << "Allocate: " << this << std::endl; + + std::stringstream stream; + stream << this; + name = stream.str(); } // TODO This should use prototype lookup for the destructor. @@ -114,15 +116,13 @@ namespace rt::objects { std::stringstream stream; stream << this; - stream << " still has references"; + stream << " still has references"; ui::error(stream.str(), this); } auto r = get_region(this); if (!is_immutable() && r != nullptr) r->objects.erase(this); - - std::cout << "Deallocate: " << get_name() << std::endl; } size_t get_rc() @@ -134,9 +134,13 @@ namespace rt::objects /// TODO remove virtual once we have primitive functions. virtual std::string get_name() { - std::stringstream stream; - stream << this; - return stream.str(); + return name; + } + + // TODO make more types use this instead of `get_name()` + virtual std::optional get_additional_info() + { + return std::nullopt; } /// TODO remove virtual once we have primitive functions. @@ -259,7 +263,7 @@ namespace rt::objects { if (is_cown()) { - ui::error("Cannot mutate a cown that is not aquired", this); + ui::error("Cannot mutate a cown that is not aquired by the current behaviour", this); } else { diff --git a/src/rt/objects/region.cc b/src/rt/objects/region.cc index 8b13475..90fff46 100644 --- a/src/rt/objects/region.cc +++ b/src/rt/objects/region.cc @@ -11,15 +11,21 @@ namespace rt::objects return obj->region.get_ptr(); } - thread_local objects::RegionPointer local_region = new Region(); + thread_local Region* local_region = Region::new_local_region(); + // FIXME: This should really be a static method on the region and not this + // free floating one IMO Region* get_local_region() { return local_region; } + // This should be a private static function in the Region to controll that + // only behaviours can set the value. There is no other instance where this + // should be called. void set_local_region(Region* region) { + assert(region->is_local_region); local_region = region; } @@ -59,8 +65,6 @@ namespace rt::objects if (obj->region.get_ptr() == get_local_region()) { - std::cout << "Adding object to region: " << obj->get_name() - << " rc = " << obj->get_rc() << std::endl; rc_of_added_objects += obj->get_rc(); internal_references++; obj->region = {r}; @@ -72,8 +76,6 @@ namespace rt::objects auto obj_region = get_region(obj); if (obj_region == r) { - std::cout << "Adding internal reference to object: " << obj->get_name() - << std::endl; internal_references++; return false; } @@ -105,12 +107,6 @@ namespace rt::objects }); r->local_reference_count += rc_of_added_objects - internal_references; - - std::cout << "Added " << rc_of_added_objects - internal_references - << " to LRC of region" << std::endl; - std::cout << "Region LRC: " << r->local_reference_count << std::endl; - std::cout << "Internal references found: " << internal_references - << std::endl; } void remove_region_reference(Region* src, Region* target) @@ -128,7 +124,7 @@ namespace rt::objects if (target == cown_region) return; - if (src == get_local_region()) + if (src->is_local_region) { Region::dec_lrc(target); return; @@ -137,8 +133,6 @@ namespace rt::objects if (src) { assert(target->parent == src); - std::cout << "Removing parent reference from region: " << src << " to " - << target << std::endl; src->direct_subregions.erase(target->bridge); if (target->combined_lrc() != 0) { @@ -165,13 +159,13 @@ namespace rt::objects if (src_region == target_region) return; - if (src_region == get_local_region()) + if (src_region->is_local_region) { Region::inc_lrc(target_region); return; } - if (target_region == get_local_region()) + if (target_region->is_local_region) { add_to_region(src_region, target, source); return; @@ -226,11 +220,11 @@ namespace rt::objects if (e.target == nullptr) return false; - std::cout << "Remove reference from: " << e.src->get_name() << " to " - << e.target->get_name() << std::endl; bool result = e.target->change_rc(-1) == 0; - - remove_region_reference(get_region(e.src), get_region(e.target)); + if (e.src) + { + remove_region_reference(get_region(e.src), get_region(e.target)); + } return result; }, [&](DynObject* obj) { delete obj; }); @@ -272,93 +266,100 @@ namespace rt::objects remove_region_reference(src_region, old_target_region); } - void Region::clean_lrcs_and_close(Region* to_close_reg) - { - if ( - dirty_regions.empty() && - (to_close_reg == nullptr || to_close_reg->is_closed())) - { - return; - } + bool Region::clean_lrcs_and_close(Region* to_close_reg) + { - if (to_close_reg) + if ((to_close_reg == nullptr || to_close_reg->is_closed())) { - std::cout << "Cleaning LRCs and closing " << to_close_reg << std::endl; - } - else - { - std::cout << "Cleaning LRCs" << std::endl; + return true; } - - for (auto r : dirty_regions) + // LRC isn't dirty and yet region hasn't been closed + if (!dirty_regions.contains(to_close_reg)) { - r->local_reference_count = 0; + return false; } + bool continue_visit = true; - std::set seen; - visit(get_local_region(), [&](Edge e) { + std::set seen_o; + //std::set seen_e; + size_t intra_region_rc{0}; + size_t total_rc{0}; + // FIXME: This works only for the current behaviour that has + // set the local region. And only because the `dirty_regions` + // has been cleared except the current region. + visit(to_close_reg, [&](Edge e) { auto src = e.src; auto dst = e.target; if (!src || !dst) { return continue_visit; } - auto dst_reg = get_region(dst); - if (dst_reg == get_local_region()) + if (dst_reg != to_close_reg) { - // Insert and continue if this was a new value - return seen.insert(dst).second; + return continue_visit; } - auto invalidate = dst_reg == to_close_reg; - invalidate |= - (to_close_reg && to_close_reg->sub_region_reference_count != 0 && - Region::is_ancestor(dst_reg, to_close_reg)); - if (invalidate) + if (!seen_o.contains(dst)) { - if (e.key == PrototypeField) - { - ui::error("Can't close the region due to this prototype", e); - } - - auto old = src->set(e.key, nullptr); - assert(old == dst); - add_reference(src, nullptr); - remove_reference(src, dst); - - continue_visit &= to_close_reg->is_closed(); - return false; + seen_o.insert(dst); + total_rc += dst->get_rc(); } + // We know dst is an intra region obj + intra_region_rc++; + //seen_e.insert(e); + return continue_visit; + + // auto invalidate = dst_reg == to_close_reg; + // invalidate |= + // (to_close_reg && to_close_reg->sub_region_reference_count != 0 && + // Region::is_ancestor(dst_reg, to_close_reg)); + // if (invalidate) + // { + // if (e.key == PrototypeField) + // { + // ui::error("Can't close the region due to this prototype", e); + // } + + // auto old = src->set(e.key, nullptr); + // assert(old == dst); + // add_reference(src, nullptr); + // remove_reference(src, dst); + + // continue_visit &= to_close_reg->is_closed(); + // return false; + // } + + // if (dirty_regions.contains(dst_reg)) + // { + // dst_reg->local_reference_count += 1; + // } + + // return false; + }); - if (dirty_regions.contains(dst_reg)) - { - dst_reg->local_reference_count += 1; - } + // Calculate real LRC + to_close_reg->local_reference_count = total_rc - intra_region_rc; - return false; - }); + std::cout << "total:" << total_rc << " intra:" << intra_region_rc << std::endl; - for (auto r : dirty_regions) + to_close_reg->is_lrc_dirty = false; + if (to_close_reg->combined_lrc() == 0) { - std::cout << "Corrected LRC of " << r << " to " - << r->local_reference_count << std::endl; - r->is_lrc_dirty = false; - if (r->combined_lrc() == 0) - { - action(r); - } + action(to_close_reg); + return true; } - dirty_regions.clear(); - - assert( - (!to_close_reg || to_close_reg->is_closed()) && - "The region should be closed now"); + return false; } void Region::clean_lrcs() { + // This is a hack, basically we don't want `try_clean` to + // look at any other regions than the current one. That's + // why we remove all other regions. + dirty_regions.clear(); + dirty_regions.insert(this); clean_lrcs_and_close(nullptr); } @@ -373,10 +374,12 @@ namespace rt::objects { return true; } - + // TODO why check sbrc if (this->is_lrc_dirty || this->sub_region_reference_count != 0) { - clean_lrcs(); + // TODO change name to clean_lrcs? + clean_lrcs_and_close(this); + dirty_regions.erase(this); } return is_closed(); @@ -436,7 +439,6 @@ namespace rt::objects RegionObject* obj = new RegionObject(r); r->bridge = obj; r->local_reference_count++; - std::cout << "Created region " << r << " with bridge " << obj << std::endl; return obj; } @@ -448,11 +450,9 @@ namespace rt::objects // Needs to check for sub_region_reference_count for send, but not // deallocate. - if (r != get_local_region() && r != cown_region) + if (!r->is_local_region && r != cown_region) { to_collect.insert(r); - std::cout << "Collecting region: " << r << " with bridge: " << r->bridge - << std::endl; } } } @@ -473,9 +473,6 @@ namespace rt::objects for (auto obj : src->objects) { auto r = get_region(obj); - std::cout << "Moving object: " << obj - << " with region bridge: " << r->bridge - << " to region with bridge: " << sink->bridge << std::endl; obj->region = {sink}; sink->objects.insert(obj); src->objects.erase(obj); @@ -546,7 +543,7 @@ namespace rt::objects assert(bridge->get_prototype() == objects::regionPrototypeObject()); auto r = get_region(bridge); - assert(r != get_local_region()); + assert(!r->is_local_region); if (r->parent != nullptr) { @@ -567,6 +564,6 @@ namespace rt::objects auto old_proto = bridge->set_prototype(nullptr); remove_reference(bridge, old_proto); // Move all objects in the region - move_objects(r, local_region); + move_objects(r, get_local_region()); } } diff --git a/src/rt/objects/region.h b/src/rt/objects/region.h index c6f4f82..66d3fd3 100644 --- a/src/rt/objects/region.h +++ b/src/rt/objects/region.h @@ -50,6 +50,8 @@ namespace rt::objects // part of the LRC or other pointers in this struct. bool is_lrc_dirty = false; + bool is_local_region = false; + // For nested regions, this points at the owning region. // This guarantees that the regions for trees. Region* parent{nullptr}; @@ -72,17 +74,20 @@ namespace rt::objects // Bridge children of the region std::set direct_subregions{}; - ~Region() - { - std::cout << "Destroying region: " << this << " with bridge " - << this->bridge << std::endl; - } + ~Region() {} size_t combined_lrc() { return local_reference_count + sub_region_reference_count; } + static Region* new_local_region() + { + auto r = new Region(); + r->is_local_region = true; + return r; + } + static void action(Region*); static void dec_lrc(Region* r) @@ -188,10 +193,9 @@ namespace rt::objects inc_sbrc(r); } - /// Cleans the LRC's and forces the region to close, by setting all local - /// references to `None` - static void clean_lrcs_and_close(Region* reg = nullptr); - static void clean_lrcs(); + /// Cleans the LRC's and returns whether the region + bool clean_lrcs_and_close(Region* reg = nullptr); + void clean_lrcs(); bool is_closed() { @@ -233,7 +237,6 @@ namespace rt::objects collecting = true; - std::cout << "Starting collection" << std::endl; while (!to_collect.empty()) { auto r = *to_collect.begin(); @@ -249,7 +252,6 @@ namespace rt::objects delete r; } - std::cout << "Finished collection" << std::endl; collecting = false; } }; @@ -258,9 +260,13 @@ namespace rt::objects // encode special regions. using RegionPointer = utils::TaggedPointer; + // The immutable region stays the same, regardless of which interpreter + // or behaviour is currently running inline Region immutable_region_impl; inline constexpr Region* immutable_region{&immutable_region_impl}; + // The cown region stays the same, regardless of which interpreter + // or behaviour is currently running inline Region cown_region_impl; inline constexpr Region* cown_region{&cown_region_impl}; } // namespace rt::objects diff --git a/src/rt/rt.cc b/src/rt/rt.cc index 46f8b62..c9a2899 100644 --- a/src/rt/rt.cc +++ b/src/rt/rt.cc @@ -30,6 +30,28 @@ namespace rt return nullptr; } + bool is_schedule_builtin(objects::DynObject* func) + { + return func == get_builtin(rt::core::schedule_func_name); + } + + bool is_breakpoint_builtin(objects::DynObject* func) + { + return func == get_builtin(rt::core::breakpoint_func_name); + } + + std::string get_key(objects::DynObject* key) + { + // TODO Add some checking. This is need to lookup the correct function in + // the prototype chain. + if (key && key->get_prototype() != core::stringPrototypeObject()) + { + ui::error("Object must be a string.", key); + } + core::StringObject* str_key = reinterpret_cast(key); + return str_key->as_key(); + } + objects::DynObject* make_func(verona::interpreter::Bytecode* body) { return new core::BytecodeFuncObject(body); @@ -63,9 +85,15 @@ namespace rt } } - objects::DynObject* make_cown(objects::DynObject* region) + objects::DynObject* + make_cown(objects::DynObject* value, objects::DynObject* name_obj) { - return new core::CownObject(region); + std::optional name; + if (name_obj) + { + name = get_key(name_obj); + } + return new core::CownObject(value, name); } void freeze(objects::DynObject* obj) @@ -87,7 +115,7 @@ namespace rt { if (obj->is_cown()) { - ui::error("Cannot access data on a cown that is not aquired", obj); + ui::error("Cannot access data on a cown that is not aquired by the current behaviour", obj); } else { @@ -97,18 +125,6 @@ namespace rt return obj->get(key); } - std::string get_key(objects::DynObject* key) - { - // TODO Add some checking. This is need to lookup the correct function in - // the prototype chain. - if (key && key->get_prototype() != core::stringPrototypeObject()) - { - ui::error("Key must be a string.", key); - } - core::StringObject* str_key = reinterpret_cast(key); - return str_key->as_key(); - } - std::optional get(objects::DynObject* obj, objects::DynObject* key) { @@ -190,11 +206,11 @@ namespace rt objects::move_reference(src, dst, target); } - size_t pre_run(ui::UI* ui) + size_t pre_run(ui::UI* ui, verona::interpreter::Scheduler* scheduler) { std::cout << "Initilizing global objects" << std::endl; core::globals(); - core::init_builtins(ui); + core::init_builtins(ui, scheduler); if (ui->is_mermaid()) { @@ -213,7 +229,6 @@ namespace rt { std::cout << "Test complete - checking for cycles in local region..." << std::endl; - objects::Region::clean_lrcs(); objects::Region::collect(); auto globals = core::globals(); if (objects::DynObject::get_count() != initial_count) @@ -245,13 +260,8 @@ namespace rt std::cout << "Final count: " << objects::DynObject::get_count() << std::endl; - std::vector roots; - for (auto obj : objects::DynObject::get_objects()) - { - roots.push_back(obj); - } ui::MermaidUI::highlight_unreachable = true; - ui->output(roots, "Memory leak detected!"); + ui->output("Memory leak detected!"); std::exit(1); } @@ -313,6 +323,16 @@ namespace rt objects::dissolve_region(bridge); } + void cown_update_state(objects::DynObject* cown) + { + if (cown->get_prototype() != core::cownPrototypeObject()) + { + ui::error("The given object is not a cown", cown); + } + + reinterpret_cast(cown)->update_status(); + } + bool is_cown_released(objects::DynObject* cown) { if (cown->get_prototype() != core::cownPrototypeObject()) @@ -323,13 +343,56 @@ namespace rt return reinterpret_cast(cown)->is_released(); } - void cown_update_state(objects::DynObject* cown) + void aquire_cown(objects::DynObject* cown, core::Behaviour* behaviour) { if (cown->get_prototype() != core::cownPrototypeObject()) { ui::error("The given object is not a cown", cown); } - reinterpret_cast(cown)->update_status(); + reinterpret_cast(cown)->aquire(behaviour); } + + void release_cown(objects::DynObject* cown, core::Behaviour* behaviour) + { + if (cown->get_prototype() != core::cownPrototypeObject()) + { + ui::error("The given object is not a cown", cown); + } + + reinterpret_cast(cown)->release(behaviour); + } + + bool is_owner(objects::DynObject* cown, core::Behaviour* behaviour) + { + if (cown->get_prototype() != core::cownPrototypeObject()) + { + ui::error("The given object is not a cown", cown); + } + + return reinterpret_cast(cown)->is_owner(behaviour); + } + + int get_cown_id(objects::DynObject* cown) + { + if (cown && cown->get_prototype() != core::cownPrototypeObject()) + { + ui::error("The given object is not a cown", cown); + } + + return reinterpret_cast(cown)->get_id(); + } + + void hack_inc_rc(objects::DynObject* obj) + { + obj->change_rc(+1); + } + + rt::core::behaviour_ptr get_active_behaviour() { + return core::Behaviour::get_active_behaviour(); + } + void set_active_behaviour(rt::core::behaviour_ptr behaviour) { + core::Behaviour::set_active_behaviour(behaviour); + } + } // namespace rt diff --git a/src/rt/rt.h b/src/rt/rt.h index 6870c04..2fea6f9 100644 --- a/src/rt/rt.h +++ b/src/rt/rt.h @@ -15,6 +15,8 @@ namespace rt verona::interpreter::FrameObj*, size_t)>; void add_builtin(std::string name, BuiltinFuncPtr func); objects::DynObject* get_builtin(std::string name); + bool is_schedule_builtin(objects::DynObject* func); + bool is_breakpoint_builtin(objects::DynObject* func); verona::interpreter::FrameObj* make_frame(verona::interpreter::FrameObj* parent); @@ -22,7 +24,7 @@ namespace rt objects::DynObject* make_iter(objects::DynObject* iter_src); objects::DynObject* make_str(std::string str_value); objects::DynObject* make_object(); - objects::DynObject* make_cown(objects::DynObject* region); + objects::DynObject* make_cown(objects::DynObject* value, objects::DynObject* name); void freeze(objects::DynObject* obj); objects::DynObject* create_region(); @@ -53,7 +55,7 @@ namespace rt objects::DynObject* dst, objects::DynObject* target); - size_t pre_run(rt::ui::UI* ui); + size_t pre_run(rt::ui::UI* ui, verona::interpreter::Scheduler* scheduler); void post_run(size_t count, rt::ui::UI* ui); objects::DynObject* iter_next(objects::DynObject* iter); @@ -69,4 +71,16 @@ namespace rt /// released. void cown_update_state(objects::DynObject* cown); bool is_cown_released(objects::DynObject* cown); + + void aquire_cown(objects::DynObject* cown, core::Behaviour* behaviour); + void release_cown(objects::DynObject* cown, core::Behaviour* behaviour); + bool is_owner(objects::DynObject* cown, core::Behaviour* behaviour); + int get_cown_id(objects::DynObject* cown); + + // This increases the rc without asking questions. Very much a + // hack but I don't care anymore. + void hack_inc_rc(objects::DynObject* obj); + + rt::core::behaviour_ptr get_active_behaviour(); + void set_active_behaviour(rt::core::behaviour_ptr behaviour); } // namespace rt diff --git a/src/rt/ui.h b/src/rt/ui.h index 2bb7d6c..4a87396 100644 --- a/src/rt/ui.h +++ b/src/rt/ui.h @@ -1,5 +1,6 @@ #pragma once +#include "behaviour.h" #include "objects/visit.h" #include @@ -18,6 +19,8 @@ namespace rt::ui virtual void output(std::vector&, std::string) {} + virtual void output(std::string) {} + virtual void highlight(std::string, std::vector&) {} virtual void error(std::string) {} @@ -38,6 +41,7 @@ namespace rt::core namespace rt::ui { class MermaidDiagram; + class ObjectGraphDiagram; class MermaidUI : public UI { @@ -45,7 +49,13 @@ namespace rt::ui static inline bool pragma_draw_regions_nested = true; static inline bool highlight_unreachable = false; + // This feels really wrong, but is the easiest fix rn. The list should + // probably always be passed in to the `output()` call but that would + // require more refactorings + std::vector* scheduler_ready_list; + private: + friend class ObjectGraphDiagram; friend class MermaidDiagram; friend void core::mermaid_builtins(ui::UI* ui); @@ -86,8 +96,16 @@ namespace rt::ui this->path = path_; } + void prep_output(); + + void close_file() + { + out.close(); + } + void output( std::vector& roots, std::string message) override; + void output(std::string message) override; void highlight( std::string message, @@ -158,6 +176,9 @@ namespace rt::ui void hide_cown_region(); void show_cown_region(); + void hide_prototypes(); + void show_prototypes(); + void error(std::string) override; void diff --git a/src/rt/ui/mermaid.cc b/src/rt/ui/mermaid.cc index e6ed274..ad6db1c 100644 --- a/src/rt/ui/mermaid.cc +++ b/src/rt/ui/mermaid.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -37,10 +38,13 @@ namespace rt::ui "#d9d450", }; - const char* LOCAL_REGION_ID = "LocalReg"; const char* IMM_REGION_ID = "ImmReg"; const char* COWN_REGION_ID = "CownReg"; + const char* BEHAVIOR_RUNNING_COLOR = "#eeeeee"; + const char* BEHAVIOR_READY_COLOR = "#eefcdd"; + const char* BEHAVIOR_PENDING_COLOR = "#e6d5fb"; + const char* FONT_SIZE = "16px"; const int EDGE_WIDTH = 2; const int ERROR_EDGE_WIDTH = 4; @@ -91,31 +95,14 @@ namespace rt::ui class MermaidDiagram { + protected: MermaidUI* info; std::ofstream& out; - - size_t id_counter = 1; size_t edge_counter = 0; - // Give a nice id to each object. - std::map nodes; - std::map regions; + MermaidDiagram(MermaidUI* info_) : info(info_), out(info->out) {} - public: - MermaidDiagram(MermaidUI* info_) : info(info_), out(info->out) - { - // Add nullptr - nodes[nullptr] = {0}; - regions[objects::immutable_region].nodes.push_back(0); - } - - void color_edge(size_t edge_id, const char* color, int width = EDGE_WIDTH) - { - out << " linkStyle " << edge_id << " stroke:" << color - << ",stroke-width:" << width << "px" << std::endl; - } - - void draw(std::vector& roots) + void draw_header() { // Header out << "
" << std::endl; @@ -123,25 +110,10 @@ namespace rt::ui out << "```mermaid" << std::endl; out << "%%{init: {'theme': 'neutral', 'themeVariables': { 'fontSize': '" << FONT_SIZE << "' }}}%%" << std::endl; - out << "graph TD" << std::endl; - out << " id0(None):::immutable" << std::endl; - - draw_nodes(roots); - draw_regions(); - draw_taint(); - draw_highlight(); - draw_error(); + } - out << "classDef unreachable stroke-width:2px,stroke:" - << UNREACHABLE_NODE_COLOR << std::endl; - out << "classDef highlight stroke-width:4px,stroke:" - << HIGHLIGHT_NODE_COLOR << std::endl; - out << "classDef error stroke-width:4px,stroke:" << ERROR_NODE_COLOR - << std::endl; - out << "classDef tainted fill:" << TAINT_NODE_COLOR << std::endl; - out << "classDef tainted_immutable stroke-width:4px,stroke:" - << TAINT_NODE_COLOR << std::endl; - out << "classDef immutable fill:" << IMMUTABLE_NODE_COLOR << std::endl; + void draw_footer() + { // Footer (end of mermaid graph) out << "```" << std::endl; out << "
" << std::endl; @@ -149,7 +121,12 @@ namespace rt::ui out << std::endl; } - private: + void color_edge(size_t edge_id, const char* color, int width = EDGE_WIDTH) + { + out << " linkStyle " << edge_id << " stroke:" << color + << ",stroke-width:" << width << "px" << std::endl; + } + std::pair get_node_style(objects::DynObject* obj) { if (obj->get_prototype() == core::cownPrototypeObject()) @@ -171,11 +148,93 @@ namespace rt::ui return {"[", "]"}; } + std::string behaviour_node_name(core::Behaviour* behaviour) + { + std::stringstream ss; + ss << "info_" << behaviour->id_str(); + return ss.str(); + } + + void draw_behaviour_info(core::Behaviour* behaviour) + { + out << " " << this->behaviour_node_name(behaviour) << "([\"" + << behaviour->get_name() << "
Status: " + << core::Behaviour::status_to_string(behaviour->status) << "\"])" + << std::endl; + // Set background color + auto background = ERROR_NODE_COLOR; + switch (behaviour->status) + { + case core::Behaviour::Status::Running: + background = BEHAVIOR_RUNNING_COLOR; + break; + case core::Behaviour::Status::Ready: + background = BEHAVIOR_READY_COLOR; + break; + case core::Behaviour::Status::Pending: + background = BEHAVIOR_PENDING_COLOR; + break; + } + out << " style " << this->behaviour_node_name(behaviour) + << " fill:" << background << std::endl; + } + }; + + class ObjectGraphDiagram : protected MermaidDiagram + { + size_t id_counter = 1; + + // Give a nice id to each object. + std::map nodes; + std::map regions; + + public: + ObjectGraphDiagram(MermaidUI* info_) : MermaidDiagram(info_) + { + // Add nullptr + nodes[nullptr] = {0}; + regions[objects::immutable_region].nodes.push_back(0); + } + + void draw(std::vector& roots) + { + // header + this->draw_header(); + out << "graph TD" << std::endl; + out << " id0(None):::immutable" << std::endl; + + auto behaviours = aggregate_behaviours(); + + draw_behaviour_nodes(behaviours); + draw_nodes(roots); + draw_behaviours(behaviours); + draw_regions(); + draw_taint(); + draw_highlight(); + draw_error(); + + // Classes + out << "classDef unreachable stroke-width:2px,stroke:" + << UNREACHABLE_NODE_COLOR << std::endl; + out << "classDef highlight stroke-width:4px,stroke:" + << HIGHLIGHT_NODE_COLOR << std::endl; + out << "classDef error stroke-width:4px,stroke:" << ERROR_NODE_COLOR + << std::endl; + out << "classDef tainted fill:" << TAINT_NODE_COLOR << std::endl; + out << "classDef tainted_immutable stroke-width:4px,stroke:" + << TAINT_NODE_COLOR << std::endl; + out << "classDef immutable fill:" << IMMUTABLE_NODE_COLOR << std::endl; + + // Footer + this->draw_footer(); + } + + private: bool is_borrow_edge(objects::Edge e) { return e.src != nullptr && e.target != nullptr && objects::get_region(e.src) != objects::get_region(e.target) && - objects::get_region(e.src) == objects::get_local_region(); + objects::get_region(e.src)->is_local_region; } std::string node_decoration(objects::DynObject* dst, bool reachable) @@ -191,6 +250,31 @@ namespace rt::ui return ""; } + std::map aggregate_behaviours() + { + // Clone the vector + std::vector pending = + *this->info->scheduler_ready_list; + std::map behaviours; + + while (!pending.empty()) + { + auto b = pending.back(); + pending.pop_back(); + + auto [_, inserted] = behaviours.insert({b->id, b}); + if (inserted) + { + for (auto succ : b->succ) + { + pending.push_back(succ); + } + } + } + + return behaviours; + } + /// @brief Draws the target node and the edge from the source to the target. NodeInfo* draw_edge(objects::Edge e, bool reachable) { @@ -229,6 +313,14 @@ namespace rt::ui // Content out << escape(dst->get_name()); + auto info = dst->get_additional_info(); + if (info) + { + out << "
"; + out << escape(info.value()); + } + // FIXME: Make RC display optional, on by default but can be turned off + // with a CLI flag like --no-rc or --simple out << "
rc=" << dst->rc; out << (rt::core::globals()->contains(dst) ? " #40;global#41;" : ""); @@ -320,7 +412,54 @@ namespace rt::ui indent.erase(indent.size() - 2); } - void draw_region(objects::Region* r, std::string& indent) + void draw_behaviour_nodes(std::map& behaviours) + { + for (auto [id, b] : behaviours) + { + draw_behaviour_info(b.get()); + } + } + + void draw_behaviours(std::map& behaviours) + { + std::string ident = ""; + for (auto [id, b] : behaviours) + { + if (b->status == core::Behaviour::Status::Running) + { + // C++ and the weird referencing rules... + draw_region(b->local_region, ident, b.get()); + } + else + { + for (auto cown : b->cowns) + { + out << " "; + out << this->behaviour_node_name(b.get()); + out << " --> |"; + out << escape(cown->get_name()); + out << "| "; + + auto pred = b->cown_deps[cown]; + if (pred && pred->status != core::Behaviour::Status::Done) + { + out << this->behaviour_node_name(pred); + } + else + { + out << this->nodes[cown]; + } + out << std::endl; + edge_counter += 1; + } + } + } + } + + void draw_region( + objects::Region* r, + std::string& indent, + core::Behaviour* behaviour = nullptr) { auto info = ®ions[r]; if (info->drawn) @@ -335,13 +474,21 @@ namespace rt::ui out << "reg" << r << "[\" \"]" << std::endl; // Content + if (behaviour) + { + out << " " << indent << this->behaviour_node_name(behaviour) + << std::endl; + } draw_region_body(r, info, indent); // Footer out << indent << "end" << std::endl; - out << indent << "style reg" << r - << " fill:" << REGION_COLORS[depth % std::size(REGION_COLORS)] - << std::endl; + auto color = REGION_COLORS[depth % std::size(REGION_COLORS)]; + if (r->is_local_region) + { + color = LOCAL_REGION_COLOR; + } + out << indent << "style reg" << r << " fill:" << color << std::endl; } void draw_regions() @@ -377,18 +524,6 @@ namespace rt::ui } regions[objects::immutable_region].drawn = true; - // Local region - { - auto region = objects::get_local_region(); - out << "subgraph " << LOCAL_REGION_ID << "[\"Local region\"]" - << std::endl; - draw_region_body(objects::cown_region, ®ions[region], indent); - out << "end" << std::endl; - out << "style " << LOCAL_REGION_ID << " fill:" << LOCAL_REGION_COLOR - << std::endl; - } - regions[objects::get_local_region()].drawn = true; - // Draw all other regions if (MermaidUI::pragma_draw_regions_nested) { @@ -495,10 +630,10 @@ namespace rt::ui MermaidUI::MermaidUI() { hide_cown_region(); + hide_prototypes(); } - void MermaidUI::output( - std::vector& roots, std::string message) + void MermaidUI::prep_output() { // Reset the file if this is a breakpoint if (should_break() && out.is_open()) @@ -525,10 +660,16 @@ namespace rt::ui std::abort(); } } + } + + void MermaidUI::output( + std::vector& roots, std::string message) + { + this->prep_output(); out << "
" << message << "
" << std::endl; - MermaidDiagram diag(this); + ObjectGraphDiagram diag(this); diag.draw(roots); if (should_break()) @@ -542,6 +683,12 @@ namespace rt::ui } } + void MermaidUI::output(std::string message) + { + auto roots = local_root_objects(); + this->output(roots, message); + } + void MermaidUI::highlight( std::string message, std::vector& highlight) { @@ -611,6 +758,22 @@ namespace rt::ui remove_always_hide(core::cownPrototypeObject()); } + void MermaidUI::hide_prototypes() + { + for (auto proto : *core::global_prototypes()) + { + add_always_hide(proto); + } + } + + void MermaidUI::show_prototypes() + { + for (auto proto : *core::global_prototypes()) + { + remove_always_hide(core::cownPrototypeObject()); + } + } + void MermaidUI::error(std::string info) { // Make sure ui doesn't pause @@ -644,15 +807,23 @@ namespace rt::ui std::vector MermaidUI::local_root_objects() { - auto local_set = &objects::get_local_region()->objects; std::vector nodes_vec; - for (auto item : *local_set) + for (auto behaviour : *this->scheduler_ready_list) { - if (always_hide.contains(item) || unreachable_hide.contains(item)) + if (!behaviour->local_region) { continue; } - nodes_vec.push_back(item); + auto local_set = &behaviour->local_region->objects; + + for (auto item : *local_set) + { + if (always_hide.contains(item) || unreachable_hide.contains(item)) + { + continue; + } + nodes_vec.push_back(item); + } } return nodes_vec; diff --git a/tests/behaviours/breakpoint.frank b/tests/behaviours/breakpoint.frank new file mode 100644 index 0000000..d6a89e4 --- /dev/null +++ b/tests/behaviours/breakpoint.frank @@ -0,0 +1,28 @@ + +breakpoint() +# test in behaviour +@name("B1") +when (): + a = {} + breakpoint() + pass() + +# first inst. in behaviour +@name("B2") +when (): + breakpoint() + a = {} + +# last inst. in behaviour +@name("B3") +when (): + a = {} + breakpoint() + +# Only inst. in behaviour +@name("B4") +when (): + breakpoint() + +# last inst. in main +breakpoint() \ No newline at end of file diff --git a/tests/behaviours/test_release_1.frank b/tests/behaviours/test_release_1.frank new file mode 100644 index 0000000..53dc9e5 --- /dev/null +++ b/tests/behaviours/test_release_1.frank @@ -0,0 +1,18 @@ +# Do behaviours only release cowns they have acquired? +# Below inst. will schedule and start B1 --> finish main --> finish B1 + +c1 = Cown(Region(), "c1") +c2 = Cown(Region(), "c2") +c3 = Cown(Region(), "c3") +pass() +pass() +pass() +pass() + +@name("B1") +when (c1, c2, c3): + c3.value.c2 = c2 + + +breakpoint() + diff --git a/tests/behaviours/test_release_2.frank b/tests/behaviours/test_release_2.frank new file mode 100644 index 0000000..a47e088 --- /dev/null +++ b/tests/behaviours/test_release_2.frank @@ -0,0 +1,27 @@ + +r = Region() +# Create a cown using an open region +c1 = Cown(r, "c1") +c2 = Cown(Region(), "c2") + +@name("B1") +when (c1): + r = c1.value + c3 = Cown(Region(), "c3") + c1.value = c3 + when (c1, c3): + c1.value = c3 + pass() + +# Close the region. B1 should now be available for execution, +# as c1 goes from 'pending' to 'Released' +r = None +# B1 must either be executable or complete +if is_executable("B1") == False: + if is_complete("B1") == False: + unreachable() + +@name("B2") +when (c1, c2): + c1.value = c2 + diff --git a/tests/cowns/cown_released_on_close1.frank b/tests/cowns/cown_released_on_close1.frank deleted file mode 100644 index 1143cbf..0000000 --- a/tests/cowns/cown_released_on_close1.frank +++ /dev/null @@ -1,20 +0,0 @@ -# Creating an open region -r1 = Region() - -# Creating a cown in the pending state -c1 = Cown(r1) - -# Make sure the cown is still pending -if is_released(c1) == False: - pass() -else: - unreachable() - -# Force close r1 -close(r1) - -# The cown should now be released -if is_released(c1) == True: - pass() -else: - unreachable() diff --git a/tests/example_1.frank b/tests/example_1.frank index c5ff04b..dc8645a 100644 --- a/tests/example_1.frank +++ b/tests/example_1.frank @@ -1,3 +1,14 @@ -x = {} -y = {} -x.y = {} +# For debugging: +# mermaid_show_functions() + +c1 = Cown(None, "c1") + +@name("B1") +when (c1): + r = Region() + c2 = Cown(r, "c2") + + when(c1, c2): + print("Running B1") + + r = None diff --git a/tests/exprs/when.frank b/tests/exprs/when.frank new file mode 100644 index 0000000..56c0150 --- /dev/null +++ b/tests/exprs/when.frank @@ -0,0 +1,14 @@ +# For debugging: +# mermaid_show_functions() + +c1 = Cown(Region()) +c2 = Cown(Region()) + +when (c1): + r = c1 + +when (c1, c2): + r = c1 + +when: + r = None \ No newline at end of file diff --git a/tests/regions/region_close_1.frank b/tests/regions/region_close_1.frank index abae05c..fbac5fd 100644 --- a/tests/regions/region_close_1.frank +++ b/tests/regions/region_close_1.frank @@ -22,9 +22,9 @@ if is_closed(r2): close(r2) # Check lref was invalidated -if lref1 != None: +if lref1 == None: unreachable() # Check lref was invalidated -if lref2 != None: +if lref2 == None: unreachable() diff --git a/tests/regions/region_close_3.frank b/tests/regions/region_close_3.frank index a1831d9..04b656a 100644 --- a/tests/regions/region_close_3.frank +++ b/tests/regions/region_close_3.frank @@ -26,6 +26,3 @@ if is_closed(r1.r2): pass() else: unreachable() - -# Force close -close(r1.r2) diff --git a/tests/regions/region_close_4.frank b/tests/regions/region_close_4.frank index 966e9c2..b79fc3d 100644 --- a/tests/regions/region_close_4.frank +++ b/tests/regions/region_close_4.frank @@ -12,9 +12,3 @@ lref = r0.r1.r2.r3.a if is_closed(r0.r1): unreachable() -# Force close -close(r0.r1) - -# Check lref was invalidated -if lref != None: - unreachable() diff --git a/tests/regions/region_close_5.frank b/tests/regions/region_close_5.frank index 76fba9f..9fec7ec 100644 --- a/tests/regions/region_close_5.frank +++ b/tests/regions/region_close_5.frank @@ -20,4 +20,5 @@ r1 = Region() r1.r2 = Region() r1.r2.a = {} -check_stack_invalidation(r1.r2.a, close_and_get(r1)) +# TODO alternate version of this test? +# check_stack_invalidation(r1.r2.a, close_and_get(r1)) diff --git a/tests/regions/region_close_fail_1.frank b/tests/regions/region_close_fail_1.frank index 8516d89..4929170 100644 --- a/tests/regions/region_close_fail_1.frank +++ b/tests/regions/region_close_fail_1.frank @@ -11,4 +11,5 @@ local_value = create(local_ty) r1.region_value = create(local_ty) # This should fail, since implicit freezing is disabled -close(r1) +if is_closed(r1) == False: + unreachable()