diff --git a/CMakeLists.txt b/CMakeLists.txt index 34a8a27..6224b52 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/behavior.cc ) add_library( diff --git a/src/lang/interpreter.cc b/src/lang/interpreter.cc index 375e09c..c48fdc7 100644 --- a/src/lang/interpreter.cc +++ b/src/lang/interpreter.cc @@ -1,9 +1,13 @@ +#include "interpreter.h" + +#include "../rt/behavior.h" #include "../rt/rt.h" #include "bytecode.h" #include "trieste/trieste.h" #include #include +#include #include #include @@ -57,8 +61,22 @@ namespace verona::interpreter class Interpreter { + bool paused = false; rt::ui::UI* ui; std::vector frame_stack; + rt::core::behavior_ptr behavior; + + InterpreterFrame* top_frame() + { + if (frame_stack.empty()) + { + return nullptr; + } + else + { + return frame_stack.back(); + } + } InterpreterFrame* push_stack_frame(trieste::Node body) { @@ -114,12 +132,12 @@ namespace verona::interpreter // ========================================== if (node == Print) { + auto message = std::string(node->location().view()); // Console output - std::cout << node->location().view() << std::endl << std::endl; + std::cout << ">>> " << message << std::endl; // Mermaid output - std::vector roots{frame()->object()}; - ui->output(roots, std::string(node->location().view())); + ui->output(message); // Continue return ExecNext{}; @@ -132,7 +150,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; @@ -477,13 +494,43 @@ namespace verona::interpreter } public: - Interpreter(rt::ui::UI* ui_) : ui(ui_) {} + Interpreter( + rt::ui::UI* ui_, + trieste::Node block, + std::vector start_stack, + rt::core::behavior_ptr behavior_) + : ui(ui_), behavior(behavior_) + { + // There is a question where the active behavior 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 behavior 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_behavior = rt::get_active_behavior(); + rt::set_active_behavior(this->behavior); + + auto frame = push_stack_frame(block); + + for (auto elem : start_stack) + { + frame->frame->stack_push(elem, "staring stack"); + } + + rt::set_active_behavior(old_behavior); + } - void run(trieste::Node main) + // Returns true if this interpreter is done, otherwise false. + bool resume() { - auto frame = push_stack_frame(main); + this->paused = false; + auto frame = top_frame(); + + rt::set_active_behavior(this->behavior); - while (frame) + while (!this->paused && frame) { const auto action = run_stmt(*frame->ip); @@ -546,6 +593,15 @@ namespace verona::interpreter frame = pop_stack_frame(); } } + + return !this->paused; + } + + // This will pause the interpreter once it's done processing the current + // statement. + void pause() + { + this->paused = true; } }; @@ -558,12 +614,218 @@ namespace verona::interpreter reinterpret_cast(ui)->set_step_counter(step_counter); } - size_t initial = rt::pre_run(ui); + Scheduler s; + + size_t initial = rt::pre_run(ui, &s); - Interpreter inter(ui); - inter.run(main_body); + s.start(new Bytecode{main_body}); 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; + } + + void Scheduler::add(rt::core::behavior_ptr behavior) + { + assert(behavior->status == rt::core::Behavior::Status::New); + + // TODO add a testing mode that selects based on a seed + for (auto cown : behavior->cowns) + { + // Get the last behavior that is waiting on the cown + auto cown_info = cowns.find(cown); + if (cown_info != cowns.end()) + { + auto predecessor = cown_info->second; + // If a behavior is pending, set the successor + if (predecessor->status != rt::core::Behavior::Status::Done) + { + if (predecessor->succ.insert(behavior).second) + { + behavior->pred_ctn += 1; + } + // Only needed for Mermaid: + behavior->cown_deps[cown] = predecessor.get(); + } + } + // Update pointer to the last pending behavior + this->cowns[cown] = behavior; + } + + std::stringstream ss; + if (behavior->pred_ctn == 0) + { + this->ready.push_back(behavior); + behavior->status = rt::core::Behavior::Status::Ready; + ss << "New behavior `" << behavior->get_name() << "` is ready"; + } + else + { + behavior->status = rt::core::Behavior::Status::Pending; + ss << "New behavior `" << behavior->get_name() << "` is pending"; + } + + this->next_schedule_msg = ss.str(); + if (this->current_int) + { + this->current_int->pause(); + } + } + + void Scheduler::start(Bytecode* main_block) + { + 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); + // :notes: I imagine a world without ugly c++ :notes: + auto behavior = std::make_shared( + main_function, std::vector{}, "main"); + behavior->status = rt::core::Behavior::Status::Ready; + this->ready.push_back(behavior); + // Seriously, why do we use this language? The memory problems I currently + // have could easly be avoided. + while (behavior) + { + Interpreter* inter; + if (behavior->status == rt::core::Behavior::Status::Ready) + { + auto block = behavior->spawn(); + + inter = new Interpreter( + rt::ui::globalUI(), block->body, behavior->cowns, behavior); + this->running[behavior] = inter; + } + else if (behavior->status == rt::core::Behavior::Status::Running) + { + inter = this->running[behavior]; + assert(inter); + } + else + { + assert(false && "HOW DID IT BREAK THIS BADLY?"); + } + + this->current_int = inter; + // TODO: + // I believe, this would be the right place to only run one step at a time + if (inter->resume()) + { + this->complete(behavior); + } + + behavior = this->get_next(); + } + + rt::remove_reference(nullptr, main_function); + } + + void Scheduler::complete(rt::core::behavior_ptr behavior) + { + behavior->complete(); + std::erase(this->ready, behavior); + + for (auto succ : behavior->succ) + { + succ->pred_ctn -= 1; + if (succ->pred_ctn == 0) + { + succ->status = rt::core::Behavior::Status::Ready; + this->ready.push_back(succ); + } + } + behavior->succ.clear(); + } + + void Scheduler::draw_scedule(std::string message) + { + if (this->next_schedule_msg) + { + message = this->next_schedule_msg.value(); + this->next_schedule_msg.reset(); + } + + // 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); + mermaid->output(message); + mermaid->close_file(); + } + + rt::core::behavior_ptr Scheduler::get_next() + { + if (this->ready.empty()) + { + return nullptr; + } + + this->draw_scedule("Current Schedule:"); + + // I hate c and c++ `unsigned` soo much... This is such an s... *suboptimal* + // language + unsigned int selected = 0; + while (true) + { + // Promt the user: + std::cout << std::endl; + std::cout << "Available behaviors:" << 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::Behavior::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); + } + + // Check selection + std::istringstream iss(line); + int n = 0; + if (iss >> n) + { + selected = n; + + // Sanity checks and preventing undefined behavior. + if (selected < this->ready.size()) + { + std::cout << std::endl; + break; + } + } + } + + auto behavior = this->ready[selected]; + return behavior; + } + } // namespace verona::interpreter diff --git a/src/lang/interpreter.h b/src/lang/interpreter.h index 8edd2ab..430c0ce 100644 --- a/src/lang/interpreter.h +++ b/src/lang/interpreter.h @@ -1,6 +1,13 @@ #pragma once +#include "../rt/behavior.h" + #include +#include +#include +#include +#include +#include namespace rt::objects { @@ -9,6 +16,7 @@ namespace rt::objects namespace verona::interpreter { + class Interpreter; struct Bytecode; void delete_bytecode(Bytecode* bytecode); @@ -33,4 +41,49 @@ namespace verona::interpreter return this->get_stack_size() == 0; } }; + + // FIXME: The implementation of this should probably be in a different file... + class Scheduler + { + // All behaviors that are ready to run + std::vector ready = {}; + // A map from cowns to the last behavior that is waiting on them. + // + // 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 = {}; + + // FIXME: To not pause twice for a new behavior (schedule::Add) and + // inter->pause() we'll store a message here for the next + // draw scedule. + // TO be clear, this is super duper hacky and shouldn't be done + // like this. + std::optional next_schedule_msg; + + // FIXME: + // This should likely be gotten by requesting the current + // behavior in the runtime and then looking up the interpreter + // from the behavior. But no, this is faster; + Interpreter* current_int; + + public: + Scheduler(); + ~Scheduler(); + + void add(rt::core::behavior_ptr behavior); + + void start(Bytecode* main); + + // void new_pending_cown(rt::objects::DynObject* cown, rt::core::behavior_ptr behavior); + // void pending_cown_released(rt::objects::DynObject* cown, rt::core::behavior_ptr behavior); + + private: + void complete(rt::core::behavior_ptr behavior); + void draw_scedule(std::string message); + rt::core::behavior_ptr get_next(); + }; } 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..950f7a9 100644 --- a/src/lang/passes/call_stmts.cc +++ b/src/lang/passes/call_stmts.cc @@ -17,9 +17,20 @@ 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_behavior") + { + return Seq << _(Call); + } + else + { + 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..1114683 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 behavior 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..f498799 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_behavior()` + 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_behavior") << 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/behavior.h b/src/rt/behavior.h new file mode 100644 index 0000000..e4a0f38 --- /dev/null +++ b/src/rt/behavior.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace verona::interpreter +{ + struct Bytecode; +} + +namespace rt::objects +{ + class DynObject; + struct Region; +} // namespace rt::objects + +namespace rt::ui +{ + class MermaidUI; + class ObjectGraphDiagram; +} + +namespace rt::core +{ + // TODO rename this to `Behaviour` + class Behavior + { + 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_behavior(std::shared_ptr); + static std::shared_ptr get_active_behavior(); + + private: + static std::shared_ptr s_active_behavior; + + private: + // Static member for naming + static int s_behavior_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 behavior 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 behavior. This has to be swapped into the global + // `local_region` when this behavior runs. + objects::Region* local_region; + + public: + // This maps the cowns of this behavior to the previous behavior 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 cown. These have to be provided + // to the new Interpreter to populate the frame + std::vector cowns; + // This uses a function object opposed to a Bytecode* to not leak memory + objects::DynObject* code; + // The number of behaviors that this behavior is waiting on + int pred_ctn = 0; + // Behaviors which are waiting on this behavior. These will be notified once + // this behavior completes + std::set> succ; + + Behavior( + objects::DynObject* code_, + std::vector cowns_, + std::optional name_ = std::nullopt); + + std::string get_name(); + std::string id_str(); + + verona::interpreter::Bytecode* spawn(); + // This completes the behavior by releasing all cowns + // decreffing all held objects + void complete(); + }; + + typedef std::shared_ptr behavior_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..5af1f96 100644 --- a/src/rt/core.h +++ b/src/rt/core.h @@ -1,4 +1,5 @@ #include "../lang/interpreter.h" +#include "behavior.h" #include "objects/prototype_object.h" #include "objects/region.h" #include "objects/region_object.h" @@ -19,6 +20,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 +49,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 +72,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 +82,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 +260,8 @@ namespace rt::core class CownObject : public objects::DynObject { private: + static int s_id_counter; + enum class Status { Pending, @@ -279,14 +285,39 @@ namespace rt::core } Status status; + int id; + core::Behavior* 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 = Behavior::get_active_behavior().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->cowns.push_back(this); + } + + 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 +364,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 +397,7 @@ namespace rt::core // but this is single threaded case Status::Acquired: case Status::Pending: - return false; + return Behavior::get_active_behavior().get() != this->owner; case Status::Released: default: return true; @@ -379,6 +422,7 @@ namespace rt::core if (!value || value->is_immutable() || value->is_cown()) { status = Status::Released; + this->owner = nullptr; return; } @@ -386,8 +430,25 @@ namespace rt::core if (region->combined_lrc() == 0) { status = Status::Released; + this->owner = nullptr; } } + + void aquire(Behavior* behavior) + { + // Who needs other safety checks than this? + // This is so gonna bite me... + assert(this->status == Status::Released); + + this->status = Status::Acquired; + this->owner = behavior; + } + + void release() + { + this->status = Status::Released; + this->owner = nullptr; + } }; inline std::set* globals() @@ -408,6 +469,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 +499,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/behavior.cc b/src/rt/core/behavior.cc new file mode 100644 index 0000000..3d1e55b --- /dev/null +++ b/src/rt/core/behavior.cc @@ -0,0 +1,107 @@ +#include "../behavior.h" + +#include "../objects/region.h" +#include "../rt.h" + +#include +#include + +namespace rt::objects +{ + void set_local_region(Region* region); +} + +namespace rt::core +{ + int Behavior::s_behavior_counter = 0; + std::shared_ptr Behavior::s_active_behavior = nullptr; + + void Behavior::set_active_behavior(std::shared_ptr active) + { + s_active_behavior = active; + if (active) + { + objects::set_local_region(active->local_region); + } + } + + std::shared_ptr Behavior::get_active_behavior() + { + return s_active_behavior; + } + + Behavior::Behavior( + rt::objects::DynObject* code_, + std::vector cowns_, + std::optional name_) + : id(s_behavior_counter++), cowns(cowns_), code(code_) + { + for (auto c : cowns) + { + ordered_cown[rt::get_cown_id(c)] = c; + } + + if (name_) + { + name = name_.value(); + } + else + { + std::stringstream ss; + ss << "Behavior_" << id; + name = ss.str(); + } + } + + std::string Behavior::get_name() + { + return this->name; + } + + std::string Behavior::id_str() + { + std::stringstream ss; + ss << "B" << this->id; + return ss.str(); + } + + verona::interpreter::Bytecode* Behavior::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 behavior has a function + // to complete a behavior. All of this should really be in one place. It + // might be better to move all of this into the scheduler. + void Behavior::complete() + { + this->status = Status::Done; + rt::remove_reference(nullptr, this->code); + this->code = nullptr; + + for (auto c : this->cowns) + { + rt::release_cown(c); + rt::remove_reference(nullptr, c); + } + this->cowns.clear(); + + for (auto [cown, waiting_on] : cown_deps) + { + if (waiting_on == this) + { + cown_deps.erase(cown); + } + } + } +} // namespace rt::core diff --git a/src/rt/core/builtin.cc b/src/rt/core/builtin.cc index fb28da2..865554d 100644 --- a/src/rt/core/builtin.cc +++ b/src/rt/core/builtin.cc @@ -1,3 +1,4 @@ +#include "../behavior.h" #include "../core.h" #include "../rt.h" @@ -181,14 +182,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; }); @@ -352,6 +360,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 +418,50 @@ namespace rt::core }); } - void init_builtins(ui::UI* ui) + void concurrency_builtins(verona::interpreter::Scheduler* scheduler) + { + add_builtin("spawn_behavior", [=](auto frame, auto args) { + // cowns (Stored on the stack in reverse order) + // -1 since the first argument is the actual behavior + 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 behavior + 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 behavior = frame->stack_pop("behavior"); + scheduler->add( + std::make_shared(behavior, cowns, 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 behavior region. + freeze(behavior); + + return std::nullopt; + }); + } + + 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..b137da7 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 behaviors 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; }); @@ -281,15 +275,6 @@ namespace rt::objects return; } - if (to_close_reg) - { - std::cout << "Cleaning LRCs and closing " << to_close_reg << std::endl; - } - else - { - std::cout << "Cleaning LRCs" << std::endl; - } - for (auto r : dirty_regions) { r->local_reference_count = 0; @@ -297,6 +282,9 @@ namespace rt::objects bool continue_visit = true; std::set seen; + // FIXME: This works only for the current behavior that has + // set the local region. And only because the `dirty_regions` + // has been cleared except the current region. visit(get_local_region(), [&](Edge e) { auto src = e.src; auto dst = e.target; @@ -342,8 +330,6 @@ namespace rt::objects for (auto r : dirty_regions) { - std::cout << "Corrected LRC of " << r << " to " - << r->local_reference_count << std::endl; r->is_lrc_dirty = false; if (r->combined_lrc() == 0) { @@ -352,13 +338,19 @@ namespace rt::objects } dirty_regions.clear(); - assert( - (!to_close_reg || to_close_reg->is_closed()) && - "The region should be closed now"); + if (to_close_reg && !to_close_reg->is_closed()) + { + ui::error("Unable to close the region"); + } } 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); } @@ -436,7 +428,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 +439,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 +462,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 +532,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 +553,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..4a9066b 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) @@ -190,8 +195,8 @@ namespace rt::objects /// 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(); + void clean_lrcs_and_close(Region* reg = nullptr); + void clean_lrcs(); bool is_closed() { @@ -233,7 +238,6 @@ namespace rt::objects collecting = true; - std::cout << "Starting collection" << std::endl; while (!to_collect.empty()) { auto r = *to_collect.begin(); @@ -249,7 +253,6 @@ namespace rt::objects delete r; } - std::cout << "Finished collection" << std::endl; collecting = false; } }; @@ -258,9 +261,13 @@ namespace rt::objects // encode special regions. using RegionPointer = utils::TaggedPointer; + // The immutable region stays the same, regardless of which interpreter + // or behavior 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 behavior 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..388a885 100644 --- a/src/rt/rt.cc +++ b/src/rt/rt.cc @@ -30,6 +30,18 @@ namespace rt return nullptr; } + 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 +75,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 +105,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 +115,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 +196,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 +219,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 +250,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 +313,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 +333,46 @@ namespace rt return reinterpret_cast(cown)->is_released(); } - void cown_update_state(objects::DynObject* cown) + void aquire_cown(objects::DynObject* cown, core::Behavior* behavior) { 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(behavior); + } + + void release_cown(objects::DynObject* cown) + { + if (cown->get_prototype() != core::cownPrototypeObject()) + { + ui::error("The given object is not a cown", cown); + } + + reinterpret_cast(cown)->release(); + } + + 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::behavior_ptr get_active_behavior() { + return core::Behavior::get_active_behavior(); + } + void set_active_behavior(rt::core::behavior_ptr behavior) { + core::Behavior::set_active_behavior(behavior); + } + } // namespace rt diff --git a/src/rt/rt.h b/src/rt/rt.h index 6870c04..4f28b6a 100644 --- a/src/rt/rt.h +++ b/src/rt/rt.h @@ -22,7 +22,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 +53,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 +69,15 @@ namespace rt /// released. void cown_update_state(objects::DynObject* cown); bool is_cown_released(objects::DynObject* cown); + + void aquire_cown(objects::DynObject* cown, core::Behavior* behavior); + void release_cown(objects::DynObject* cown); + 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::behavior_ptr get_active_behavior(); + void set_active_behavior(rt::core::behavior_ptr behavior); } // namespace rt diff --git a/src/rt/ui.h b/src/rt/ui.h index 2bb7d6c..d5e9ee4 100644 --- a/src/rt/ui.h +++ b/src/rt/ui.h @@ -1,5 +1,6 @@ #pragma once +#include "behavior.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..38e0a1c 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 behavior_node_name(core::Behavior* behavior) + { + std::stringstream ss; + ss << "info_" << behavior->id_str(); + return ss.str(); + } + + void draw_behavior_info(core::Behavior* behavior) + { + out << " " << this->behavior_node_name(behavior) << "([\"" + << behavior->get_name() << "
Status: " + << core::Behavior::status_to_string(behavior->status) << "\"])" + << std::endl; + // Set background color + auto background = ERROR_NODE_COLOR; + switch (behavior->status) + { + case core::Behavior::Status::Running: + background = BEHAVIOR_RUNNING_COLOR; + break; + case core::Behavior::Status::Ready: + background = BEHAVIOR_READY_COLOR; + break; + case core::Behavior::Status::Pending: + background = BEHAVIOR_PENDING_COLOR; + break; + } + out << " style " << this->behavior_node_name(behavior) + << " 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 behaviors = aggregate_behaviors(); + + draw_behavior_nodes(behaviors); + draw_nodes(roots); + draw_behaviors(behaviors); + 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_behaviors() + { + // Clone the vector + std::vector pending = + *this->info->scheduler_ready_list; + std::map behaviors; + + while (!pending.empty()) + { + auto b = pending.back(); + pending.pop_back(); + + auto [_, inserted] = behaviors.insert({b->id, b}); + if (inserted) + { + for (auto succ : b->succ) + { + pending.push_back(succ); + } + } + } + + return behaviors; + } + /// @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_behavior_nodes(std::map& behaviors) + { + for (auto [id, b] : behaviors) + { + draw_behavior_info(b.get()); + } + } + + void draw_behaviors(std::map& behaviors) + { + std::string ident = ""; + for (auto [id, b] : behaviors) + { + if (b->status == core::Behavior::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->behavior_node_name(b.get()); + out << " --> |"; + out << escape(cown->get_name()); + out << "| "; + + auto pred = b->cown_deps[cown]; + if (pred) + { + out << this->behavior_node_name(pred); + } + else + { + out << this->nodes[cown]; + } + out << std::endl; + edge_counter += 1; + } + } + } + } + + void draw_region( + objects::Region* r, + std::string& indent, + core::Behavior* behavior = nullptr) { auto info = ®ions[r]; if (info->drawn) @@ -335,13 +474,21 @@ namespace rt::ui out << "reg" << r << "[\" \"]" << std::endl; // Content + if (behavior) + { + out << " " << indent << this->behavior_node_name(behavior) + << 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 behavior : *this->scheduler_ready_list) { - if (always_hide.contains(item) || unreachable_hide.contains(item)) + if (!behavior->local_region) { continue; } - nodes_vec.push_back(item); + auto local_set = &behavior->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/example_1.frank b/tests/example_1.frank index c5ff04b..84aa88e 100644 --- a/tests/example_1.frank +++ b/tests/example_1.frank @@ -1,3 +1,14 @@ -x = {} -y = {} -x.y = {} +# For debugging: +# mermaid_show_functions() + +r = Region() +c1 = Cown(r, "c1") +r.f = {} + +@name("B1") +when (c1): + print("Running B1") + +is_closed(r) +close(r) + diff --git a/tests/exprs/when.frank b/tests/exprs/when.frank new file mode 100644 index 0000000..a8721d4 --- /dev/null +++ b/tests/exprs/when.frank @@ -0,0 +1,17 @@ +# For debugging: +# mermaid_show_functions() + +c1 = Cown(Region()) +c2 = Cown(Region()) + +when (c1): + r = c1 + +when (c1, c2): + r = c1 + +when (): + r = c1 + +when: + r = None \ No newline at end of file