From 01bca966ef3495a40eb4c651e47adc0cceae143b Mon Sep 17 00:00:00 2001 From: William Baker Date: Sat, 21 Sep 2024 19:28:49 -0400 Subject: [PATCH 01/21] Add initial assembly_scanner assembly_scanner and program_walker should inherit from a common generic abstract class, and obviously a *ton* of testing is needed for assembly_scanner itelf. Once that's in place, thread can use a generic form of IP and advancement, and threads can be held by a subclass of either of the above. --- .clang-format | 1 + src/assembly_scanner.cpp | 290 +++++++++++++++++++++++++++++++++++++++ src/assembly_scanner.hh | 33 +++++ src/instruction.hh | 5 +- src/interpreter.cpp | 1 - 5 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 src/assembly_scanner.cpp create mode 100644 src/assembly_scanner.hh diff --git a/.clang-format b/.clang-format index 1bb8990..f885cd9 100644 --- a/.clang-format +++ b/.clang-format @@ -37,3 +37,4 @@ WhitespaceSensitiveMacros: - testcase - testgroup - TEST_ITEM + - DESTRINGIFY_NAME diff --git a/src/assembly_scanner.cpp b/src/assembly_scanner.cpp new file mode 100644 index 0000000..2bc6c3e --- /dev/null +++ b/src/assembly_scanner.cpp @@ -0,0 +1,290 @@ +#include "assembly_scanner.hh" +#include +#include +#include +#include + +using std::cerr; +using std::endl; +using std::pair; +using std::string; + +#define WHITESPACE " \n\r\t" + +[[noreturn]] static void invalid_literal(const string& argument) { + cerr << "Invalid format for literal: " << argument << endl; + exit(EXIT_FAILURE); +} + +#ifdef __clang__ +// ... +#elif REALLY_MSVC +// ... +#else +#pragma GCC diagnostic ignored "-Wshift-count-overflow" +#endif + +NONNULL_PTR(const std::vector*>) assembly_scanner::get_fragments() { + if (m_fragments != nullptr) { + return m_fragments; + } + + m_fragments = new std::vector*>(); + + // We need to do two passes: one to resolve labels, and one to assign targets to jumps. During the first pass, the + // fragments are actually constructed. However, jumps may not have valid targets yet, so we need some way to store + // the label's name inside a pair. This code relies on the following assumption: + static_assert( + sizeof(NONNULL_PTR(const string)) <= 2 * sizeof(size_t), "Cannot fit string pointer inside pair of size_t" + ); + // Using an ordered set over any other container so that references are not invalidated after insertion + std::set label_names; + + auto label_to_fake_location = [&](const string& name) -> pair { + auto iter = label_names.find(name); + if (iter == label_names.end()) { + auto p = label_names.insert(name); + iter = p.first; + } + NONNULL_PTR(const string) ptr = &*iter; + size_t bottom_half = reinterpret_cast(ptr) & SIZE_MAX; + size_t top_half = reinterpret_cast(ptr) >> (8 * sizeof(size_t)); + return { top_half, bottom_half }; + }; + + // First pass + for (string curr_line; std::getline(m_program, curr_line);) { + // Unquoted semicolons are comments. Remove them. + size_t i; + for (i = 0; i < curr_line.size(); ++i) { + if (curr_line[i] != ';') { + continue; + } + if (i == 0 || curr_line[i - 1] != '\'') { + break; + } + } + if (i < curr_line.size()) { + curr_line.erase(i); + } + // If the line is only a comment, move on + if (curr_line.empty()) { + continue; + } + // Remove trailing whitespace. If there's only whitespace, skip this line + i = curr_line.find_last_not_of(WHITESPACE); + if (i == string::npos) { + continue; + } else { + curr_line.erase(i + 1); + } + + // Look for labels (non-whitespace in the first column) + i = curr_line.find_first_not_of(WHITESPACE ":"); + assert(i != string::npos); + if (i == 0) { + // Label, find end + i = curr_line.find_first_of(WHITESPACE ":"); + if (i == string::npos) { + i = curr_line.size(); + } + string label = curr_line.substr(0, i); + + DISCARD label_names.insert(label); + auto p = label_locations.insert({ label, get_current_location() }); + + if (!p.second) { + cerr << "Label '" << label << "' appears twice" << endl; + exit(EXIT_FAILURE); + } + + // Set i to the first non-whitespace character after the label + i = curr_line.find_first_not_of(WHITESPACE ":", i + 1); + if (i == string::npos) { + // Line was only a label + continue; + } + } + + // Remove leading whitespace, and label if there is one + curr_line.erase(0, i); + + // Line should only be the opcode and, if there is one, the argument + if (curr_line.size() < 3) { + cerr << "Instruction too short: " << curr_line << endl; + exit(EXIT_FAILURE); + } + + string instruction_name = curr_line.substr(0, 3); + instruction::operation opcode = assembly_scanner::opcode_for_name(instruction_name); + switch (opcode) { + case instruction::operation::JMP: { + size_t label_start = curr_line.find_first_not_of(WHITESPACE, 3); + instruction::argument arg; + string label = curr_line.substr(label_start); + arg.next = label_to_fake_location(label); + add_instruction({ opcode, arg }); + break; + } + case instruction::operation::BNG: + case instruction::operation::TSP: { + size_t label_start = curr_line.find_first_not_of(WHITESPACE, 3); + instruction::argument arg; + string label = curr_line.substr(label_start); + arg.choice = { { get_current_location().first + 1, SIZE_C(0) }, label_to_fake_location(label) }; + add_instruction({ opcode, arg }); + break; + } + case instruction::operation::PSI: + case instruction::operation::PSC: { + size_t arg_start = curr_line.find_first_not_of(WHITESPACE, 3); + if (arg_start == string::npos) { + cerr << "Missing argument for push instruction" << endl; + exit(EXIT_FAILURE); + } + + string argument = curr_line.substr(arg_start); + int24_t arg_value; + // Should be in one of three formats: + // - 'c' (single UTF-8 character) + // - 0xff (arbitrary length hex number) + // - #9 (single decimal digit) + if (argument[0] == '\'' && argument.back() == '\'') { + if (argument.size() < 3 || argument.size() > 6) { + // One UTF-8 character is 1 to 4 bytes + invalid_literal(argument); + } + i = 1; + arg_value = parse_unichar([&]() { return curr_line[i++]; }); + if (arg_value < INT24_C(0) || i != argument.size() - 1) { + invalid_literal(argument); + } + } else if (argument[0] == '0' && argument[1] == 'x') { + char* last = nullptr; + unsigned long ul = strtoul(argument.c_str(), &last, 16); + if (*last != '\0') { + invalid_literal(argument); + } + arg_value = static_cast(ul); + } else if (argument[0] == '#' && argument.size() == 2) { + arg_value = static_cast(argument[1] - '0'); + if (arg_value < INT24_C(0) || arg_value > INT24_C(9)) { + invalid_literal(argument); + } + } else { + invalid_literal(argument); + } + + instruction::argument arg; + arg.number = arg_value; + add_instruction({ opcode, arg }); + break; + } + default: + add_instruction({ opcode, instruction::argument() }); + break; + } + } + + // Second pass + for (auto* fragment : *m_fragments) { + for (auto& instr : *fragment) { + if (instr.m_op == instruction::operation::JMP) { + fake_location_to_real(instr.m_arg.next); + } else if (instr.m_op == instruction::operation::TSP || instr.m_op == instruction::operation::BNG) { + fake_location_to_real(instr.m_arg.choice.second); + } + } + } + + return m_fragments; +} + +pair assembly_scanner::get_current_location() const { + size_t first = this->m_fragments->size(); + size_t second = 0; + if (first > 0) { + --first; + const auto* ptr = this->m_fragments->at(first); + if (ptr == nullptr) { + second = ptr->size(); + if (second > 0) { + --second; + } + } + } + return { first, second }; +} + +void assembly_scanner::add_instruction(instruction&& i) { + assert(m_fragments != nullptr); + if (m_fragments->empty()) { + m_fragments->push_back(new std::vector{ std::move(i) }); + } else { + auto last = m_fragments->back(); + if (last == nullptr) { + size_t idx = m_fragments->size() - 1; + m_fragments->at(idx) = new std::vector{ std::move(i) }; + } else { + if (last->back().is_exit() || last->back().first_if_branch() + || last->back().m_op == instruction::operation::JMP) { + m_fragments->push_back(new std::vector{ std::move(i) }); + } else { + last->push_back(std::forward(i)); + } + } + } +} + +void assembly_scanner::fake_location_to_real(pair& p) const { + uintptr_t reconstructed = + (static_cast(p.first) << (8 * sizeof(size_t))) | static_cast(p.second); + auto ptr = reinterpret_cast(reconstructed); + p = label_locations.find(*ptr)->second; +} + +#define DESTRINGIFY_NAME(op) \ + if (name == #op) \ + return instruction::operation::op + +instruction::operation assembly_scanner::opcode_for_name(const std::string& name) noexcept { + DESTRINGIFY_NAME(BNG); + DESTRINGIFY_NAME(JMP); + DESTRINGIFY_NAME(TKL); + DESTRINGIFY_NAME(TSP); + DESTRINGIFY_NAME(TJN); + DESTRINGIFY_NAME(TKL); + DESTRINGIFY_NAME(NOP); + DESTRINGIFY_NAME(ADD); + DESTRINGIFY_NAME(SUB); + DESTRINGIFY_NAME(MUL); + DESTRINGIFY_NAME(DIV); + DESTRINGIFY_NAME(UDV); + DESTRINGIFY_NAME(MOD); + DESTRINGIFY_NAME(PSI); + DESTRINGIFY_NAME(PSC); + DESTRINGIFY_NAME(POP); + DESTRINGIFY_NAME(EXT); + DESTRINGIFY_NAME(INC); + DESTRINGIFY_NAME(DEC); + DESTRINGIFY_NAME(AND); + DESTRINGIFY_NAME(IOR); + DESTRINGIFY_NAME(XOR); + DESTRINGIFY_NAME(NOT); + DESTRINGIFY_NAME(GTC); + DESTRINGIFY_NAME(PTC); + DESTRINGIFY_NAME(GTI); + DESTRINGIFY_NAME(PTI); + DESTRINGIFY_NAME(PTU); + DESTRINGIFY_NAME(IDX); + DESTRINGIFY_NAME(DUP); + DESTRINGIFY_NAME(DP2); + DESTRINGIFY_NAME(RND); + DESTRINGIFY_NAME(EXP); + DESTRINGIFY_NAME(SWP); + DESTRINGIFY_NAME(GTM); + DESTRINGIFY_NAME(GDT); + + cerr << "Unrecognized opcode '" << name << '\'' << endl; + exit(EXIT_FAILURE); +} diff --git a/src/assembly_scanner.hh b/src/assembly_scanner.hh new file mode 100644 index 0000000..499aaaf --- /dev/null +++ b/src/assembly_scanner.hh @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include "instruction.hh" + +class assembly_scanner { +public: + inline assembly_scanner(std::istream& program) : m_program(program), m_fragments(nullptr) {} + + inline ~assembly_scanner() noexcept { + if (m_fragments == nullptr) { + return; + } + for (std::vector* frag : *m_fragments) { + delete frag; + } + delete m_fragments; + } + + NONNULL_PTR(const std::vector*>) get_fragments(); +protected: + void fake_location_to_real(std::pair& p) const; + static instruction::operation opcode_for_name(const std::string& name) noexcept; + + std::unordered_map> label_locations; +private: + std::pair get_current_location() const; + void add_instruction(instruction&& i); + + std::istream& m_program; + std::vector*>* m_fragments; +}; diff --git a/src/instruction.hh b/src/instruction.hh index f540f37..d1685b5 100644 --- a/src/instruction.hh +++ b/src/instruction.hh @@ -27,6 +27,7 @@ constexpr bool is_branch(int24_t op, direction dir) noexcept { class instruction { friend class disassembler; friend class compiler; + friend class assembly_scanner; using instruction_pointer = program_walker::instruction_pointer; @@ -89,8 +90,8 @@ public: } } protected: + CONSTEXPR_UNION instruction(operation op, argument arg) noexcept : m_arg(arg), m_op(op) {} + argument m_arg; operation m_op; -private: - CONSTEXPR_UNION instruction(operation op, argument arg) noexcept : m_arg(arg), m_op(op) {} }; diff --git a/src/interpreter.cpp b/src/interpreter.cpp index 2d16768..ec0e80b 100644 --- a/src/interpreter.cpp +++ b/src/interpreter.cpp @@ -1,5 +1,4 @@ #include "interpreter.hh" -#include #include #include #include From 3ad235a2a01ff8c7d95783d87134ede5f460dac9 Mon Sep 17 00:00:00 2001 From: William Baker Date: Sat, 21 Sep 2024 19:34:14 -0400 Subject: [PATCH 02/21] ignore shift overflow on all platforms --- src/assembly_scanner.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/assembly_scanner.cpp b/src/assembly_scanner.cpp index 2bc6c3e..a5948da 100644 --- a/src/assembly_scanner.cpp +++ b/src/assembly_scanner.cpp @@ -16,10 +16,11 @@ using std::string; exit(EXIT_FAILURE); } +// Disable warnings relating to shifting by too much #ifdef __clang__ -// ... +#pragma clang diagnostic ignored "-Wshift-count-overflow" #elif REALLY_MSVC -// ... +#pragma warning(disable: 4293) #else #pragma GCC diagnostic ignored "-Wshift-count-overflow" #endif From 4cae529f5a784c8580c3cd3975075c540419e366 Mon Sep 17 00:00:00 2001 From: William Baker Date: Sun, 22 Sep 2024 12:26:21 -0400 Subject: [PATCH 03/21] WIP: Completely changing how the interpreter works fix some of the tests fix more warnings Refactors fix some errors, I hope fix more errors WIP: more progress actually put it in --- src/any_program_holder.hh | 13 + src/assembly_scanner.cpp | 81 +++--- src/assembly_scanner.hh | 28 +- src/compiler.cpp | 13 +- src/disassembler.cpp | 18 +- src/input.cpp | 5 +- src/input.hh | 9 +- src/instruction.cpp | 3 +- src/instruction.hh | 34 +-- src/instruction_scanner.hh | 3 +- src/interpreter.cpp | 163 ------------ src/interpreter.hh | 198 ++++++++++++++- src/main.cpp | 16 +- src/program_walker.hh | 95 ++++++- src/thread.cpp | 469 +--------------------------------- src/thread.hh | 506 ++++++++++++++++++++++++++++++++++--- tests/unit/ip_movement.hh | 2 +- 17 files changed, 881 insertions(+), 775 deletions(-) create mode 100644 src/any_program_holder.hh diff --git a/src/any_program_holder.hh b/src/any_program_holder.hh new file mode 100644 index 0000000..d48310f --- /dev/null +++ b/src/any_program_holder.hh @@ -0,0 +1,13 @@ +#pragma once + +#include +#include "instruction.hh" + +template +class any_program_holder { +public: + virtual void advance(IP& ip, std::function go_left) const = 0; + virtual instruction at(const IP& ip) const = 0; + virtual std::string raw_at(const IP& ip) const = 0; + virtual std::pair get_coords(const IP& ip) const = 0; +}; diff --git a/src/assembly_scanner.cpp b/src/assembly_scanner.cpp index a5948da..b3b6d8c 100644 --- a/src/assembly_scanner.cpp +++ b/src/assembly_scanner.cpp @@ -2,11 +2,9 @@ #include #include #include -#include using std::cerr; using std::endl; -using std::pair; using std::string; #define WHITESPACE " \n\r\t" @@ -16,21 +14,12 @@ using std::string; exit(EXIT_FAILURE); } -// Disable warnings relating to shifting by too much -#ifdef __clang__ -#pragma clang diagnostic ignored "-Wshift-count-overflow" -#elif REALLY_MSVC -#pragma warning(disable: 4293) -#else -#pragma GCC diagnostic ignored "-Wshift-count-overflow" -#endif - -NONNULL_PTR(const std::vector*>) assembly_scanner::get_fragments() { +NONNULL_PTR(const std::vector)>) assembly_scanner::get_fragments() { if (m_fragments != nullptr) { return m_fragments; } - m_fragments = new std::vector*>(); + m_fragments = new std::vector)>(); // We need to do two passes: one to resolve labels, and one to assign targets to jumps. During the first pass, the // fragments are actually constructed. However, jumps may not have valid targets yet, so we need some way to store @@ -41,7 +30,7 @@ NONNULL_PTR(const std::vector*>) assembly_scanner::get_ // Using an ordered set over any other container so that references are not invalidated after insertion std::set label_names; - auto label_to_fake_location = [&](const string& name) -> pair { + auto label_to_fake_location = [&](const string& name) -> IP { auto iter = label_names.find(name); if (iter == label_names.end()) { auto p = label_names.insert(name); @@ -49,12 +38,18 @@ NONNULL_PTR(const std::vector*>) assembly_scanner::get_ } NONNULL_PTR(const string) ptr = &*iter; size_t bottom_half = reinterpret_cast(ptr) & SIZE_MAX; - size_t top_half = reinterpret_cast(ptr) >> (8 * sizeof(size_t)); + size_t top_half = +#if UINTPTR_MAX > SIZE_MAX + reinterpret_cast(ptr) >> (8 * sizeof(size_t)) +#else + SIZE_C(0) +#endif + ; return { top_half, bottom_half }; }; // First pass - for (string curr_line; std::getline(m_program, curr_line);) { + for (string curr_line; std::getline(*m_program, curr_line);) { // Unquoted semicolons are comments. Remove them. size_t i; for (i = 0; i < curr_line.size(); ++i) { @@ -201,17 +196,39 @@ NONNULL_PTR(const std::vector*>) assembly_scanner::get_ return m_fragments; } -pair assembly_scanner::get_current_location() const { +void assembly_scanner::advance(IP& ip, std::function go_left) const { + assert(m_fragments != nullptr); + + const IP* to_left = at(ip).first_if_branch(); + if (to_left != nullptr && go_left()) { + ip = *to_left; + return; + } + + ip.second++; + if (ip.second > m_fragments->at(ip.first)->size()) { + ip.first++; + ip.second = 0; + } + if (ip.first > m_fragments->size()) { + ip.first = 0; + } +} + +string assembly_scanner::raw_at(const IP& ip) const { + // TODO: call disassembler::to_str (move it to somewhere common?) + return ""; +} + +assembly_scanner::IP assembly_scanner::get_current_location() const { size_t first = this->m_fragments->size(); size_t second = 0; if (first > 0) { --first; const auto* ptr = this->m_fragments->at(first); - if (ptr == nullptr) { - second = ptr->size(); - if (second > 0) { - --second; - } + second = ptr->size(); + if (second > 0) { + --second; } } return { first, second }; @@ -223,23 +240,21 @@ void assembly_scanner::add_instruction(instruction&& i) { m_fragments->push_back(new std::vector{ std::move(i) }); } else { auto last = m_fragments->back(); - if (last == nullptr) { - size_t idx = m_fragments->size() - 1; - m_fragments->at(idx) = new std::vector{ std::move(i) }; + if (last->back().is_exit() || last->back().first_if_branch() + || last->back().get_op() == instruction::operation::JMP) { + m_fragments->push_back(new std::vector{ std::move(i) }); } else { - if (last->back().is_exit() || last->back().first_if_branch() - || last->back().m_op == instruction::operation::JMP) { - m_fragments->push_back(new std::vector{ std::move(i) }); - } else { - last->push_back(std::forward(i)); - } + last->push_back(std::forward(i)); } } } -void assembly_scanner::fake_location_to_real(pair& p) const { +void assembly_scanner::fake_location_to_real(IP& p) const { uintptr_t reconstructed = - (static_cast(p.first) << (8 * sizeof(size_t))) | static_cast(p.second); +#if UINTPTR_MAX > SIZE_MAX + (static_cast(p.first) << (8 * sizeof(size_t))) | +#endif + static_cast(p.second); auto ptr = reinterpret_cast(reconstructed); p = label_locations.find(*ptr)->second; } diff --git a/src/assembly_scanner.hh b/src/assembly_scanner.hh index 499aaaf..72f3d8d 100644 --- a/src/assembly_scanner.hh +++ b/src/assembly_scanner.hh @@ -2,32 +2,40 @@ #include #include -#include "instruction.hh" +#include "any_program_holder.hh" -class assembly_scanner { +class assembly_scanner : public any_program_holder > { public: - inline assembly_scanner(std::istream& program) : m_program(program), m_fragments(nullptr) {} + using IP = std::pair; + + inline assembly_scanner(std::istream* program) : m_program(program), m_fragments(nullptr) {} inline ~assembly_scanner() noexcept { if (m_fragments == nullptr) { return; } - for (std::vector* frag : *m_fragments) { + for (auto* frag : *m_fragments) { delete frag; } delete m_fragments; } - NONNULL_PTR(const std::vector*>) get_fragments(); + NONNULL_PTR(const std::vector)>) get_fragments(); + + void advance(IP& ip, std::function go_left) const; + std::string raw_at(const IP& ip) const; + + inline std::pair get_coords(const IP& ip) const { return ip; } + inline instruction at(const IP& ip) const { return m_fragments->at(ip.first)->at(ip.second); } protected: - void fake_location_to_real(std::pair& p) const; + void fake_location_to_real(IP& p) const; static instruction::operation opcode_for_name(const std::string& name) noexcept; - std::unordered_map> label_locations; + std::unordered_map label_locations; private: - std::pair get_current_location() const; + IP get_current_location() const; void add_instruction(instruction&& i); - std::istream& m_program; - std::vector*>* m_fragments; + std::istream* m_program; + std::vector)>* m_fragments; }; diff --git a/src/compiler.cpp b/src/compiler.cpp index 86b4504..ff2dc83 100644 --- a/src/compiler.cpp +++ b/src/compiler.cpp @@ -2,7 +2,6 @@ // files. #include "compiler.hh" #include -#include #include "compiler/strings.hh" using std::cerr; @@ -17,7 +16,7 @@ void compiler::write_state(std::ostream& os) { if (std::any_of(m_fragments->cbegin(), m_fragments->cend(), [](const auto& v) { return std::any_of(v->begin(), v->end(), [](const auto& i) { - return i.m_op == instruction::operation::RND; + return i.get_op() == instruction::operation::RND; }); })) { os << "srand(time(NULL));"; @@ -37,17 +36,17 @@ void compiler::write_state(std::ostream& os) { void compiler::get_c_code(const instruction& i, std::ostream& os, bool assume_ascii) { using op = instruction::operation; - switch (i.m_op) { + switch (i.get_op()) { case op::BNG: { os << "if (lws_top(stack) < 0) goto lbl"; - const auto& choice = i.m_arg.choice; + const auto& choice = i.get_arg().choice; const auto &dest1 = choice.first, &dest2 = choice.second; os << dest2.first << '_' << dest2.second << "; goto lbl" << dest1.first << '_' << dest1.second << ';'; return; } case op::JMP: { os << "goto lbl"; - const auto& dest = i.m_arg.next; + const auto& dest = i.get_arg().next; os << dest.first << '_' << dest.second << ';'; return; } @@ -87,13 +86,13 @@ void compiler::get_c_code(const instruction& i, std::ostream& os, bool assume_as return; case op::PSC: { os << "lws_push(stack,"; - const auto value = i.m_arg.number; + const auto value = i.get_arg().number; os << static_cast(value) << ");"; return; } case op::PSI: { os << "lws_push(stack,"; - const auto value = i.m_arg.number; + const auto value = i.get_arg().number; os << static_cast(value - (int24_t)'0') << ");"; return; } diff --git a/src/disassembler.cpp b/src/disassembler.cpp index b7aa5be..172e248 100644 --- a/src/disassembler.cpp +++ b/src/disassembler.cpp @@ -1,10 +1,6 @@ #include "disassembler.hh" #include -#include #include -#include -#include -#include "output.hh" using std::cerr; using std::ostringstream; @@ -18,7 +14,7 @@ using std::pair; void disassembler::to_str(const instruction& i, std::ostream& os) { using operation = instruction::operation; - switch (i.m_op) { + switch (i.get_op()) { STRING_NAME(NOP); STRING_NAME(ADD); STRING_NAME(SUB); @@ -50,13 +46,13 @@ void disassembler::to_str(const instruction& i, std::ostream& os) { STRING_NAME(TKL); STRING_NAME(TJN); case operation::PSI: { - int24_t value = i.m_arg.number; + int24_t value = i.get_arg().number; os << "PSI #"; print_unichar(value, os); return; } case operation::PSC: { - int24_t value = i.m_arg.number; + int24_t value = i.get_arg().number; os << "PSC '"; print_unichar(value, os); os << "' ; 0x"; @@ -66,23 +62,23 @@ void disassembler::to_str(const instruction& i, std::ostream& os) { return; } case operation::JMP: { - pair target = i.m_arg.next; + pair target = i.get_arg().next; os << "JMP " << target.first << "." << target.second; return; } case operation::BNG: { - pair target = i.m_arg.choice.second; + pair target = i.get_arg().choice.second; os << "BNG " << target.first << "." << target.second; return; } case operation::TSP: { - pair target = i.m_arg.choice.second; + pair target = i.get_arg().choice.second; os << "TSP " << target.first << "." << target.second; return; } default: cerr << "Unrecognized opcode '"; - print_unichar(static_cast(i.m_op), cerr); + print_unichar(static_cast(i.get_op()), cerr); cerr << '\'' << std::endl; exit(EXIT_FAILURE); } diff --git a/src/input.cpp b/src/input.cpp index adaf229..6c6636f 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -46,7 +46,9 @@ static constexpr const char* FLAGS_HELP = "\t \t--disassemble.\n" "\t--compile, -c \tOutput C code for the program. Doesn't support all\n" "\t \toperations. Incompatible with all other flags except\n" - "\t \t--null.\n\n" + "\t \t--null.\n" + "\t--assembly, -A \tTake the input in the pseudo-assembly format.\n" + "\t \tIncompatible with --disassemble.\n\n" "\t--expand, -e \tSpace the program out to fit the triangle.\n" "\t \tIncompatible with all other flags except --null.\n" "\t--null, -z \tRead the program until null terminator instead of EOF."; @@ -60,6 +62,7 @@ static CONSTINIT_LAMBDA std::tuple using pair = std::pair; - +public: // The underlying operation performed. enum class operation : int32_t { // direction-sensitive ones must be negative to ensure they never appear in source. @@ -55,7 +32,7 @@ class instruction { CONSTEXPR_UNION argument() noexcept { none = {}; } }; -public: + instruction(instruction_pointer ip, const program& program) noexcept; static CONSTEXPR_UNION instruction jump_to(pair next) noexcept { @@ -89,6 +66,9 @@ public: return nullptr; } } + + constexpr operation get_op() const noexcept { return m_op; } + CONSTEXPR_UNION const argument& get_arg() const noexcept { return m_arg; } protected: CONSTEXPR_UNION instruction(operation op, argument arg) noexcept : m_arg(arg), m_op(op) {} diff --git a/src/instruction_scanner.hh b/src/instruction_scanner.hh index fb4b95f..a705677 100644 --- a/src/instruction_scanner.hh +++ b/src/instruction_scanner.hh @@ -1,8 +1,7 @@ #pragma once -#include #include "input.hh" -#include "instruction.hh" +#include "program_walker.hh" class instruction_scanner : public program_walker { public: diff --git a/src/interpreter.cpp b/src/interpreter.cpp index ec0e80b..fe08194 100644 --- a/src/interpreter.cpp +++ b/src/interpreter.cpp @@ -1,168 +1,5 @@ #include "interpreter.hh" -#include -#include -#include -#include -#include - -using std::cerr; -using std::vector; - -using status = thread::status; #ifndef __EMSCRIPTEN__ extern "C" void send_thread_count(size_t) {} #endif - -struct pair_hash { - constexpr size_t operator()(const std::pair& p) const noexcept { return p.first ^ (p.second << 4); } -}; - -void interpreter::run() { - thread::thread_count = 0UL; - - // Begin the execution loop. - while (true) { - vector removal_indices; - vector pending_threads; - std::unordered_map, size_t, pair_hash> waiting_coords; - - send_thread_count(m_threads.size()); - - for (size_t i = 0; i < m_threads.size(); ++i) { - thread& curr_thread = m_threads[i]; - - curr_thread.tick(); - - switch (curr_thread.m_status) { - case status::splitting: - switch (curr_thread.m_ip.dir) { - case direction::east: - pending_threads.emplace_back(curr_thread, direction::northeast); - pending_threads.emplace_back(curr_thread, direction::southeast); - break; - case direction::west: - pending_threads.emplace_back(curr_thread, direction::northwest); - pending_threads.emplace_back(curr_thread, direction::southwest); - break; - default: - unreachable("Only west- and east-moving threads can split"); - } - FALLTHROUGH - case status::terminated: - removal_indices.push_back(i); - break; - case status::waiting: { - auto location = waiting_coords.find(curr_thread.m_ip.coords); - - if (location != waiting_coords.end()) { - size_t value = location->second; - - pending_threads.push_back(join_threads(value, i)); - - removal_indices.push_back(value); - removal_indices.push_back(i); - - waiting_coords.erase(location); - } else { - waiting_coords.insert({ curr_thread.m_ip.coords, i }); - } - - break; - } - case status::idle: - break; - case status::active: - curr_thread.advance(); - break; - } - } - - // Sort the removal indices descending, so that we can erase them in a single loop - std::sort(removal_indices.begin(), removal_indices.end(), std::greater{}); - - for (size_t i : removal_indices) { - m_threads.erase(m_threads.cbegin() + static_cast(i)); - } - - // Move the pending threads into the thread vector - m_threads.insert( - m_threads.cend(), - std::make_move_iterator(pending_threads.begin()), - std::make_move_iterator(pending_threads.end()) - ); - - if (m_threads.empty()) { - std::cout << std::flush; - cerr << std::flush; - return; - } - } -} - -thread interpreter::join_threads(size_t first_index, size_t second_index) { - thread& first_thread = m_threads[first_index]; - thread& second_thread = m_threads[second_index]; - - direction new_dir; - // They should either be facing both NW/SW or both NE/SE, never one going east and one going west - assert((static_cast(first_thread.m_ip.dir) & 0b011) == (static_cast(second_thread.m_ip.dir) & 0b011)); - switch (first_thread.m_ip.dir) { - case direction::northeast: - case direction::southeast: - new_dir = direction::east; - break; - case direction::northwest: - case direction::southwest: - new_dir = direction::west; - break; - default: - unreachable("East- and west-going IPs cannot merge"); - } - - int24_t first_stack_amount = first_thread.m_stack.back(); - first_thread.m_stack.pop_back(); - - if (first_thread.m_flags.warnings && first_stack_amount > static_cast(first_thread.m_stack.size())) - UNLIKELY { - cerr << "Warning: Attempt to move " << first_stack_amount << " values out of thread with stack size " - << first_thread.m_stack.size() << '\n'; - } - - int24_t second_stack_amount = second_thread.m_stack.back(); - second_thread.m_stack.pop_back(); - - bool whole_second_stack = second_stack_amount < INT24_C(0); - - vector new_stack; - if (first_stack_amount < INT24_C(0)) { - new_stack = std::move(first_thread.m_stack); - } else { - // The iterator-based constructor copies elementwise and leaves no extra space. To prevent the unneeded - // reallocation, reserve the needed size before copying out of the first stack. - new_stack.reserve( - static_cast(first_stack_amount) - + (whole_second_stack ? second_thread.m_stack.size() : static_cast(second_stack_amount)) - ); - new_stack.insert( - new_stack.cbegin(), - first_thread.m_stack.end() - static_cast(first_stack_amount), - first_thread.m_stack.end() - ); - } - - if (second_thread.m_flags.warnings && second_stack_amount > static_cast(second_thread.m_stack.size())) - UNLIKELY { - cerr << "Warning: Attempt to move " << second_stack_amount << " values out of thread with stack size " - << second_thread.m_stack.size() << '\n'; - } - - new_stack.insert( - new_stack.cend(), - whole_second_stack ? second_thread.m_stack.begin() - : (second_thread.m_stack.end() - static_cast(second_stack_amount)), - second_thread.m_stack.end() - ); - - return thread(first_thread, new_dir, std::move(new_stack)); -} diff --git a/src/interpreter.hh b/src/interpreter.hh index 5914e2b..9502963 100644 --- a/src/interpreter.hh +++ b/src/interpreter.hh @@ -1,19 +1,205 @@ #pragma once -#include "input.hh" +#include +#include +#include "assembly_scanner.hh" #include "thread.hh" +struct pair_hash { + constexpr size_t operator()(const std::pair& p) const noexcept { return p.first ^ (p.second << 4); } +}; + +extern "C" void send_thread_count(size_t tc); + +template class interpreter { public: - inline interpreter(NONNULL_PTR(const program) p, flags f) noexcept : m_threads{ thread(p, f) } {} + inline interpreter(const ProgramHolder& p, flags f) noexcept : + m_program_holder(p), m_threads{ thread(p, f) } {} + + inline void run() { + using status = typename thread::status; + + thread_count = 0UL; + + // Begin the execution loop. + while (true) { + std::vector removal_indices; + std::vector> pending_threads; + std::unordered_map, size_t, pair_hash> waiting_coords; + + send_thread_count(m_threads.size()); + + for (size_t i = 0; i < m_threads.size(); ++i) { + thread& curr_thread = m_threads[i]; + + curr_thread.tick(); + + switch (curr_thread.m_status) { + case status::splitting: + split_thread(pending_threads, curr_thread); + FALLTHROUGH + case status::terminated: + removal_indices.push_back(i); + break; + case status::waiting: { + std::pair coords = m_program_holder.get_coords(curr_thread.m_ip); + auto location = waiting_coords.find(coords); + + if (location != waiting_coords.end()) { + size_t value = location->second; + + pending_threads.push_back(join_threads(value, i)); + + removal_indices.push_back(value); + removal_indices.push_back(i); + + waiting_coords.erase(location); + } else { + waiting_coords.insert({ coords, i }); + } + + break; + } + case status::idle: + break; + case status::active: + curr_thread.advance(); + break; + } + } + + // Sort the removal indices descending, so that we can erase them in a single loop + std::sort(removal_indices.begin(), removal_indices.end(), std::greater{}); - void run(); + for (size_t i : removal_indices) { + m_threads.erase(m_threads.cbegin() + static_cast(i)); + } + + // Move the pending threads into the thread vector + m_threads.insert( + m_threads.cend(), + std::make_move_iterator(pending_threads.begin()), + std::make_move_iterator(pending_threads.end()) + ); + + if (m_threads.empty()) { + std::cout << std::flush; + std::cerr << std::flush; + return; + } + } + } private: // Combine two threads given their indices in m_threads, and return the result of joining them. May operate // destructively; the threads are assumed to not be used after this call. - thread join_threads(size_t first_index, size_t second_index); + inline thread join_threads(size_t first_index, size_t second_index) { + thread& first_thread = m_threads[first_index]; + thread& second_thread = m_threads[second_index]; + + std::vector new_stack = join_threads_work(first_thread, second_thread); + return thread(first_thread, std::move(new_stack)); + } + + inline std::vector + join_threads_work(thread& first_thread, thread& second_thread) { + int24_t first_stack_amount = first_thread.m_stack.back(); + first_thread.m_stack.pop_back(); + + if (first_thread.m_flags.warnings && first_stack_amount > static_cast(first_thread.m_stack.size())) + UNLIKELY { + std::cerr << "Warning: Attempt to move " << first_stack_amount << " values out of thread with stack size " + << first_thread.m_stack.size() << '\n'; + } + + int24_t second_stack_amount = second_thread.m_stack.back(); + second_thread.m_stack.pop_back(); + + bool whole_second_stack = second_stack_amount < INT24_C(0); + + std::vector new_stack; + if (first_stack_amount < INT24_C(0)) { + new_stack = std::move(first_thread.m_stack); + } else { + // The iterator-based constructor copies elementwise and leaves no extra space. To prevent the unneeded + // reallocation, reserve the needed size before copying out of the first stack. + new_stack.reserve( + static_cast(first_stack_amount) + + (whole_second_stack ? second_thread.m_stack.size() : static_cast(second_stack_amount)) + ); + new_stack.insert( + new_stack.cbegin(), + first_thread.m_stack.end() - static_cast(first_stack_amount), + first_thread.m_stack.end() + ); + } - std::vector m_threads; + if (second_thread.m_flags.warnings && second_stack_amount > static_cast(second_thread.m_stack.size())) + UNLIKELY { + std::cerr << "Warning: Attempt to move " << second_stack_amount << " values out of thread with stack size " + << second_thread.m_stack.size() << '\n'; + } + + new_stack.insert( + new_stack.cend(), + whole_second_stack ? second_thread.m_stack.begin() + : (second_thread.m_stack.end() - static_cast(second_stack_amount)), + second_thread.m_stack.end() + ); + + return new_stack; + } + + inline void split_thread(std::vector>& new_threads, const thread& old_thread) + const { + new_threads.push_back(old_thread); + new_threads.push_back(old_thread); + } + + const ProgramHolder& m_program_holder; + std::vector> m_threads; }; -extern "C" void send_thread_count(size_t thread_count); +template<> +inline void interpreter::split_thread( + std::vector>& new_threads, + const thread& old_thread +) const { + switch (old_thread.m_ip.dir) { + case direction::east: + new_threads.emplace_back(old_thread, direction::northeast); + new_threads.emplace_back(old_thread, direction::southeast); + break; + case direction::west: + new_threads.emplace_back(old_thread, direction::northwest); + new_threads.emplace_back(old_thread, direction::southwest); + break; + default: + unreachable("Only west- and east-moving threads can split"); + } +} + +template<> +inline thread interpreter::join_threads(size_t first_index, size_t second_index) { + thread& first_thread = m_threads[first_index]; + thread& second_thread = m_threads[second_index]; + + direction new_dir; + // They should either be facing both NW/SW or both NE/SE, never one going east and one going west + assert((static_cast(first_thread.m_ip.dir) & 0b011) == (static_cast(second_thread.m_ip.dir) & 0b011)); + switch (first_thread.m_ip.dir) { + case direction::northeast: + case direction::southeast: + new_dir = direction::east; + break; + case direction::northwest: + case direction::southwest: + new_dir = direction::west; + break; + default: + unreachable("East- and west-going IPs cannot merge"); + } + + std::vector new_stack = join_threads_work(first_thread, second_thread); + return thread(first_thread, new_dir, std::move(new_stack)); +} diff --git a/src/main.cpp b/src/main.cpp index 2132875..89821a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ -#include -#include +#define _CRT_SECURE_NO_WARNINGS 1 + +#include "assembly_scanner.hh" #include "compiler.hh" #include "disassembler.hh" #include "interpreter.hh" @@ -12,6 +13,14 @@ inline void execute(const std::string& prg, flags f) { std::ios::sync_with_stdio(f.pipekill); #endif + if (f.assembly) { + std::istringstream iss(prg); + assembly_scanner as(&iss); + interpreter i(as, f); + i.run(); + return; + } + program p(prg); if (p.side_length() == 0) { @@ -28,7 +37,8 @@ inline void execute(const std::string& prg, flags f) { compiler c(&p, f); c.write_state(std::cout); } else { - interpreter i(&p, f); + program_walker pw(&p); + interpreter i(pw, f); i.run(); } } diff --git a/src/program_walker.hh b/src/program_walker.hh index 42609c7..cd55ab5 100644 --- a/src/program_walker.hh +++ b/src/program_walker.hh @@ -1,6 +1,8 @@ #pragma once -#include "program.hh" +#include +#include "any_program_holder.hh" +#include "output.hh" enum class direction : char { // Bitfield representation: { north, !(north || south), east }. This is designed so the default direction @@ -14,18 +16,91 @@ enum class direction : char { southeast = 0b001, }; -class program_walker { +constexpr bool is_branch(int24_t op, direction dir) noexcept { + switch (op) { + case BNG_E: + case THR_E: + return dir == direction::west; + case BNG_W: + case THR_W: + return dir == direction::east; + case BNG_NE: + return dir == direction::southwest; + case BNG_NW: + return dir == direction::southeast; + case BNG_SE: + return dir == direction::northwest; + case BNG_SW: + return dir == direction::northeast; + default: + return false; + } +} + +struct instruction_pointer { + std::pair coords; + direction dir; + + constexpr bool operator==(const instruction_pointer& rhs) const noexcept { + return this->coords == rhs.coords && this->dir == rhs.dir; + } +}; + +class program_walker : public any_program_holder { public: + using IP = instruction_pointer; + constexpr program_walker(NONNULL_PTR(const program) p) noexcept : m_program(p) {} - struct instruction_pointer { - std::pair coords; - direction dir; + inline void advance(IP& ip, std::function go_left) const { + int24_t op = m_program->at(ip.coords.first, ip.coords.second); + switch (op) { + case MIR_EW: + case MIR_NESW: + case MIR_NS: + case MIR_NWSE: + program_walker::reflect(ip.dir, op); + break; + case BNG_E: + case BNG_NE: + case BNG_NW: + case BNG_SE: + case BNG_SW: + case BNG_W: + case THR_E: + case THR_W: + program_walker::branch(ip.dir, op, go_left); + break; + case SKP: + case PSC: + case PSI: + program_walker::advance(ip, m_program->side_length()); + break; + } + program_walker::advance(ip, m_program->side_length()); + } - constexpr bool operator==(const instruction_pointer& rhs) const noexcept { - return this->coords == rhs.coords && this->dir == rhs.dir; + inline instruction at(const IP& ip) const noexcept { + int24_t op = m_program->at(ip.coords.first, ip.coords.second); + if (is_branch(op, ip.dir)) { + if (op == static_cast(THR_E) || op == static_cast(THR_W)) { + return instruction::spawn_to({ { SIZE_C(0), SIZE_C(0) }, { SIZE_C(0), SIZE_C(0) } }); + } else { + return instruction::branch_to({ { SIZE_C(0), SIZE_C(0) }, { SIZE_C(0), SIZE_C(0) } }); + } + } else { + return instruction(ip, *m_program); } - }; + } + + + inline std::pair get_coords(const IP& ip) const { return ip.coords; } + + inline std::string raw_at(const IP& ip) const { + std::ostringstream oss; + print_unichar(m_program->at(ip.coords.first, ip.coords.second), oss); + return oss.str(); + } // Advance the IP one step. static constexpr void advance(instruction_pointer& ip, size_t program_size) noexcept { @@ -383,8 +458,8 @@ protected: namespace std { template<> -struct hash { - inline size_t operator()(const program_walker::instruction_pointer& key) const noexcept { +struct hash { + inline size_t operator()(const instruction_pointer& key) const noexcept { return key.coords.first ^ (key.coords.second << 3) ^ (static_cast(key.dir) << 7); } }; diff --git a/src/thread.cpp b/src/thread.cpp index da6ac3d..dc4368a 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -1,35 +1,8 @@ -#define _CRT_SECURE_NO_WARNINGS 1 - #include "thread.hh" -#include -#include -#include -#include -#include "output.hh" -#include "time.hh" - -#ifndef TRILANGLE_CLOCK -#define TRILANGLE_CLOCK std::chrono::system_clock -#endif - -#define EMPTY_PROTECT(name) \ - if (m_stack.empty() && m_flags.warnings) UNLIKELY { \ - cerr << "Warning: Attempt to " name " empty stack.\n"; \ - } else -#define SIZE_CHECK(name, count) \ - if (m_flags.warnings && m_stack.size() < (count)) UNLIKELY \ - cerr << "Warning: Attempt to " name " stack with fewer than " << (count) << " elements.\n" - -using std::cerr; using std::cout; -using std::flush; -using std::pair; -using trilangle_clock = TRILANGLE_CLOCK; - -using status = thread::status; -unsigned long thread::thread_count; +unsigned long thread_count; #ifndef __EMSCRIPTEN__ extern "C" void send_debug_info( @@ -38,7 +11,7 @@ extern "C" void send_debug_info( size_t stack_depth, size_t y, size_t x, - int32_t instruction + const char* instruction ) { cout << "Thread " << thread_number << '\n'; if (stack != nullptr) { @@ -54,444 +27,8 @@ extern "C" void send_debug_info( cout << "]\n"; } - cout << "Coords: (" << x << ", " << y << ")\nInstruction: "; - print_unichar(static_cast(instruction)); - cout << std::endl; + cout << "Coords: (" << x << ", " << y << ")\nInstruction: " << instruction << std::endl; DISCARD getchar(); } #endif - -// Emscripten doesn't flush after every putchar call, so ensure we flush at all -[[noreturn]] static inline void flush_and_exit(int code) { - cout << flush; - cerr << flush; -#ifdef __EMSCRIPTEN__ - // https://github.com/bbrk24/Trilangle/issues/4 - while (!feof(stdin)) { - DISCARD getchar(); - } -#endif - exit(code); -} - -void thread::tick() { - static std::default_random_engine engine((std::random_device())()); - static std::uniform_int_distribution distr(INT24_MIN, INT24_MAX); - - // The operation currently being executed - int24_t op = m_program->at(m_ip.coords.first, m_ip.coords.second); - - // The web interface needs every thread to send debug info. The CLI expects only active threads to do so. -#ifdef __EMSCRIPTEN__ - if (m_flags.debug) { - send_debug_info( - m_number, - m_flags.show_stack ? m_stack.data() : nullptr, - m_stack.size(), - m_ip.coords.first, - m_ip.coords.second, - op - ); - } -#endif - - switch (m_status) { - case status::idle: - m_status = status::active; - return; - case status::waiting: - return; - case status::active: - break; - case status::terminated: - unreachable("tick() should not be called on terminated thread"); - case status::splitting: - unreachable("thread with m_status = splitting should be split"); - } - -#ifndef __EMSCRIPTEN__ - // Print the requisite information in debug mode. - if (m_flags.debug) { - send_debug_info( - m_number, - m_flags.show_stack ? m_stack.data() : nullptr, - m_stack.size(), - m_ip.coords.first, - m_ip.coords.second, - op - ); - } -#endif - - // ...yeah - switch (op) { - case NOP: - break; - case ADD: { - SIZE_CHECK("add from", 2); - int24_t top = m_stack.back(); - m_stack.pop_back(); - - pair result = m_stack.back().add_with_overflow(top); - - if (m_flags.warnings && result.first) UNLIKELY { - cerr << "Warning: Overflow on addition/subtraction is undefined behavior.\n"; - } - - m_stack.back() = result.second; - - break; - } - case SUB: { - SIZE_CHECK("subtract from", 2); - int24_t top = m_stack.back(); - m_stack.pop_back(); - - pair result = m_stack.back().subtract_with_overflow(top); - - if (m_flags.warnings && result.first) UNLIKELY { - cerr << "Warning: Overflow on addition/subtraction is undefined behavior.\n"; - } - - m_stack.back() = result.second; - - break; - } - case MUL: { - SIZE_CHECK("multiply from", 2); - int24_t top = m_stack.back(); - m_stack.pop_back(); - - pair result = m_stack.back().multiply_with_overflow(top); - - if (result.first) UNLIKELY { - cerr << "Warning: Overflow on multiplication is undefined behavior.\n"; - } - - m_stack.back() = result.second; - - break; - } - case DIV: { - if (m_flags.warnings) { - if (m_stack.size() < 2) UNLIKELY { - cerr << "Warning: Attempt to divide from stack with fewer than 2 elements.\n"; - } - if (!m_stack.empty() && m_stack.back() == INT24_C(0)) UNLIKELY { - cerr << "Warning: Attempted division by zero.\n"; - } - } - - int24_t top = m_stack.back(); - m_stack.pop_back(); - - if (m_flags.warnings && m_stack.back() == INT24_MIN && top == INT24_C(-1)) UNLIKELY { - cerr << "Warning: Overflow on division is undefined behavior.\n"; - } - - m_stack.back() /= top; - break; - } - case UDV: { - if (m_flags.warnings) { - if (m_stack.size() < 2) UNLIKELY { - cerr << "Warning: Attempt to divide from stack with fewer than 2 elements.\n"; - } - if (!m_stack.empty() && m_stack.back() == INT24_C(0)) UNLIKELY { - cerr << "Warning: Attempted division by zero.\n"; - } - } - - int24_t top = m_stack.back(); - m_stack.pop_back(); - int24_t second = m_stack.back(); - - uint32_t unsigned_first = static_cast(top) & 0x00ff'ffffU; - uint32_t unsigned_second = static_cast(second) & 0x00ff'ffffU; - - m_stack.back() = static_cast(unsigned_second / unsigned_first); - break; - } - case MOD: { - if (m_flags.warnings) { - if (m_stack.size() < 2) UNLIKELY { - cerr << "Warning: Attempt to divide from stack with fewer than 2 elements.\n"; - } - if (!m_stack.empty() && m_stack.back() == INT24_C(0)) UNLIKELY { - cerr << "Warning: Attempted division by zero.\n"; - } - } - int24_t top = m_stack.back(); - m_stack.pop_back(); - m_stack.back() %= top; - break; - } - case MIR_EW: - case MIR_NESW: - case MIR_NS: - case MIR_NWSE: - program_walker::reflect(m_ip.dir, op); - break; - case BNG_E: - case BNG_NE: - case BNG_NW: - case BNG_SE: - case BNG_SW: - case BNG_W: - program_walker::branch(m_ip.dir, op, [&]() NOEXCEPT_T { - EMPTY_PROTECT("branch on") {} - return m_stack.back() < INT24_C(0); - }); - break; - case PSC: { - advance(); - m_status = status::idle; - int24_t next = m_program->at(m_ip.coords.first, m_ip.coords.second); - m_stack.push_back(next); - break; - } - case PSI: { - advance(); - m_status = status::idle; - int24_t next = m_program->at(m_ip.coords.first, m_ip.coords.second); - - if (m_flags.warnings) { - if (next < (int24_t)'0' || next > (int24_t)'9') UNLIKELY { - cerr << "Warning: Pushing non-ASCII-decimal number with " << static_cast(opcode::PSI) - << " is implementation-defined behavior.\n"; - } - } - - m_stack.push_back(next - (int24_t)'0'); - break; - } - case POP: - EMPTY_PROTECT("pop from") { - m_stack.pop_back(); - } - break; - case EXT: - flush_and_exit(EXIT_SUCCESS); - case INC: - EMPTY_PROTECT("increment") { - ++m_stack.back(); - } - break; - case DEC: - EMPTY_PROTECT("decrement") { - --m_stack.back(); - } - break; - case AND: { - SIZE_CHECK("bitwise and", 2); - int24_t top = m_stack.back(); - m_stack.pop_back(); - m_stack.back() &= top; - break; - } - case IOR: { - SIZE_CHECK("bitwise or", 2); - int24_t top = m_stack.back(); - m_stack.pop_back(); - m_stack.back() |= top; - break; - } - case XOR: { - SIZE_CHECK("bitwise xor", 2); - int24_t top = m_stack.back(); - m_stack.pop_back(); - m_stack.back() ^= top; - break; - } - case NOT: - EMPTY_PROTECT("complement") { - m_stack.back() = ~m_stack.back(); - } - break; - case GTC: - if (m_flags.assume_ascii) { - int character = getchar(); - if (character == EOF) { - m_stack.push_back(INT24_C(-1)); - } else { - if (m_flags.warnings && (character & 0x7f) != character) UNLIKELY { - cerr << "Non-ASCII byte read.\n"; - } - m_stack.emplace_back(character); - } - } else { - m_stack.push_back(get_unichar()); - } - break; - case PTC: - EMPTY_PROTECT("print from") { - bool should_print = true; - - if (m_stack.back() < INT24_C(0)) UNLIKELY { - if (m_flags.warnings) { - cerr << "Warning: Attempt to print character with negative value.\n"; - should_print = false; - } - if (m_flags.pipekill) { - flush_and_exit(EXIT_FAILURE); - } - } - - if (should_print) { - if (m_flags.assume_ascii) { - int24_t back = m_stack.back(); - if (m_flags.warnings && (back & INT24_C(0x7f)) != back) UNLIKELY { - cerr << "Warning: Printing non-ASCII value.\n"; - } - putchar(back); - } else { - print_unichar(m_stack.back()); - } - -#ifdef __EMSCRIPTEN__ - if (m_flags.debug) { - cout << flush; - } -#endif - } - } - - if (m_flags.pipekill && ferror(stdout)) { - // No need to flush stdout, it's already closed - cerr << flush; - exit(EXIT_SUCCESS); - } - - break; - case GTI: { - int32_t i = -1; - - while (!(feof(stdin) || scanf("%" SCNi32, &i))) { - DISCARD getchar(); - } - - m_stack.emplace_back(i); - break; - } - case PTI: - EMPTY_PROTECT("print from") { - cout << m_stack.back() << '\n'; - } - - if (m_flags.pipekill && ferror(stdout)) { - cerr << flush; - exit(EXIT_SUCCESS); - } - break; - case PTU: - EMPTY_PROTECT("print from") { - cout << (static_cast(m_stack.back()) & 0x00ff'ffffU) << '\n'; - } - - if (m_flags.pipekill && ferror(stdout)) { - cerr << flush; - exit(EXIT_SUCCESS); - } - break; - case SKP: - advance(); - m_status = status::idle; - break; - case IDX: { - if (m_flags.warnings && m_stack.empty()) UNLIKELY { - cerr << "Warning: Attempt to read index from empty stack.\n"; - break; - } - - size_t index = static_cast(m_stack.back()) & SIZE_C(0x00ff'ffff); - m_stack.pop_back(); - - if (m_flags.warnings && m_stack.size() < index + 1) UNLIKELY { - cerr << "Warning: Attempt to index out of stack bounds (size = " << m_stack.size() - << ", index = " << index << ")\n"; - } - - size_t i = m_stack.size() - index - 1; - m_stack.push_back(m_stack[i]); - - break; - } - case DUP: - EMPTY_PROTECT("duplicate") { - m_stack.push_back(m_stack.back()); - } - break; - case RND: - m_stack.emplace_back(distr(engine)); - break; - case EXP: - EMPTY_PROTECT("exponentiate") { - m_stack.back() = INT24_C(1) << m_stack.back(); - } - break; - case SWP: { - SIZE_CHECK("swap in", 2); - size_t i = m_stack.size() - 2; - std::swap(m_stack[i], m_stack[i + 1]); - break; - } - case THR_E: - switch (m_ip.dir) { - case direction::east: - m_status = status::terminated; - break; - case direction::west: - m_status = status::splitting; - break; - case direction::northeast: - case direction::southeast: - m_status = status::waiting; - break; - default: - break; - } - break; - case THR_W: - switch (m_ip.dir) { - case direction::west: - m_status = status::terminated; - break; - case direction::east: - m_status = status::splitting; - break; - case direction::northwest: - case direction::southwest: - m_status = status::waiting; - break; - default: - break; - } - break; - case GTM: { - m_stack.push_back(get_time()); - break; - } - case GDT: { - m_stack.push_back(get_date()); - break; - } - case DP2: - SIZE_CHECK("2-dupe", 2); - m_stack.push_back(m_stack[m_stack.size() - 2]); - m_stack.push_back(m_stack[m_stack.size() - 2]); - break; - case INVALID_CHAR: - cout << flush; - cerr << flush; - fprintf( - stderr, "Encoding error detected, or U+%04" PRIX32 " present in source.\n", static_cast(op) - ); - exit(EXIT_FAILURE); - default: - cerr << "Unrecognized opcode '"; - print_unichar(op, cerr); - cerr << "' (at (" << m_ip.coords.first << ", " << m_ip.coords.second << "))\n"; - flush_and_exit(EXIT_FAILURE); - } -} diff --git a/src/thread.hh b/src/thread.hh index ad8d92e..d6faaff 100644 --- a/src/thread.hh +++ b/src/thread.hh @@ -1,13 +1,34 @@ #pragma once +#include +#include +#include #include "input.hh" #include "program_walker.hh" +#include "time.hh" -class thread : public program_walker { +extern unsigned long thread_count; + +#ifndef TRILANGLE_CLOCK +#define TRILANGLE_CLOCK std::chrono::system_clock +#endif + +using trilangle_clock = TRILANGLE_CLOCK; + +extern "C" void send_debug_info( + unsigned long thread_number, + const int24_t* stack, + size_t stack_depth, + size_t y, + size_t x, + const char* instruction +); + +template +class thread { + template friend class interpreter; public: - static unsigned long thread_count; - enum class status : char { active, // Currently executing code idle, // Inactive for a single tick due to e.g. a "skip" operation @@ -19,8 +40,8 @@ public: // Currently only called with T = std::vector and T = const std::vector&. // Generic to allow both move- and copy-construction. template - thread(const thread& other, direction d, T&& stack) noexcept : - program_walker(other.m_program), + thread(const thread& other, direction d, T&& stack) noexcept : + m_program_holder(other.m_program_holder), m_stack(std::forward(stack)), m_ip{ other.m_ip.coords, d }, m_status(status::active), @@ -29,38 +50,463 @@ public: advance(); } - inline thread(const thread& other, direction d) noexcept : thread(other, d, other.m_stack) {} + inline thread(const thread& other, std::vector&& stack) noexcept : + m_program_holder(other.m_program_holder), + m_stack(std::forward&&>(stack)), + m_ip{ other.m_ip }, + m_status(status::active), + m_flags(other.m_flags), + m_number(++thread_count) { + advance(); + } + + inline thread(const thread& other, direction d) noexcept : thread(other, d, other.m_stack) {} + +#define EMPTY_PROTECT(name) \ + if (m_stack.empty() && m_flags.warnings) UNLIKELY { \ + std::cerr << "Warning: Attempt to " name " empty stack.\n"; \ + } else + +#define SIZE_CHECK(name, count) \ + if (m_flags.warnings && m_stack.size() < (count)) UNLIKELY \ + cerr << "Warning: Attempt to " name " stack with fewer than " << (count) << " elements.\n" + + inline void tick() { + using std::cerr; + using std::cout; + using std::flush; + using std::pair; + using operation = instruction::operation; + + static std::default_random_engine engine((std::random_device())()); + static std::uniform_int_distribution distr(INT24_MIN, INT24_MAX); + + // The operation currently being executed + instruction instr = m_program_holder.at(m_ip); + operation op = instr.get_op(); + + // The web interface needs every thread to send debug info. The CLI expects only active threads to do so. +#ifdef __EMSCRIPTEN__ + if (m_flags.debug) { + pair coords = m_program_holder.get_coords(m_ip); + send_debug_info( + m_number, + m_flags.show_stack ? m_stack.data() : nullptr, + m_stack.size(), + coords.first, + coords.second, + m_program_holder.raw_at(m_ip).c_str() + ); + } +#endif + + switch (m_status) { + case status::idle: + m_status = status::active; + return; + case status::waiting: + return; + case status::active: + break; + case status::terminated: + unreachable("tick() should not be called on terminated thread"); + case status::splitting: + unreachable("thread with m_status = splitting should be split"); + } + +#ifndef __EMSCRIPTEN__ + // Print the requisite information in debug mode. + if (m_flags.debug) { + pair coords = m_program_holder.get_coords(m_ip); + send_debug_info( + m_number, + m_flags.show_stack ? m_stack.data() : nullptr, + m_stack.size(), + coords.first, + coords.second, + m_program_holder.raw_at(m_ip).c_str() + ); + } +#endif + + // ...yeah + switch (op) { + case operation::BNG: + case operation::NOP: + break; + case operation::ADD: { + SIZE_CHECK("add from", 2); + int24_t top = m_stack.back(); + m_stack.pop_back(); + + pair result = m_stack.back().add_with_overflow(top); + + if (m_flags.warnings && result.first) UNLIKELY { + cerr << "Warning: Overflow on addition/subtraction is undefined behavior.\n"; + } + + m_stack.back() = result.second; + + break; + } + case operation::SUB: { + SIZE_CHECK("subtract from", 2); + int24_t top = m_stack.back(); + m_stack.pop_back(); + + pair result = m_stack.back().subtract_with_overflow(top); + + if (m_flags.warnings && result.first) UNLIKELY { + cerr << "Warning: Overflow on addition/subtraction is undefined behavior.\n"; + } + + m_stack.back() = result.second; + + break; + } + case operation::MUL: { + SIZE_CHECK("multiply from", 2); + int24_t top = m_stack.back(); + m_stack.pop_back(); + + pair result = m_stack.back().multiply_with_overflow(top); + + if (result.first) UNLIKELY { + cerr << "Warning: Overflow on multiplication is undefined behavior.\n"; + } - void tick(); + m_stack.back() = result.second; + + break; + } + case operation::DIV: { + if (m_flags.warnings) { + if (m_stack.size() < 2) UNLIKELY { + cerr << "Warning: Attempt to divide from stack with fewer than 2 elements.\n"; + } + if (!m_stack.empty() && m_stack.back() == INT24_C(0)) UNLIKELY { + cerr << "Warning: Attempted division by zero.\n"; + } + } + + int24_t top = m_stack.back(); + m_stack.pop_back(); + + if (m_flags.warnings && m_stack.back() == INT24_MIN && top == INT24_C(-1)) UNLIKELY { + cerr << "Warning: Overflow on division is undefined behavior.\n"; + } + + m_stack.back() /= top; + break; + } + case operation::UDV: { + if (m_flags.warnings) { + if (m_stack.size() < 2) UNLIKELY { + cerr << "Warning: Attempt to divide from stack with fewer than 2 elements.\n"; + } + if (!m_stack.empty() && m_stack.back() == INT24_C(0)) UNLIKELY { + cerr << "Warning: Attempted division by zero.\n"; + } + } + + int24_t top = m_stack.back(); + m_stack.pop_back(); + int24_t second = m_stack.back(); + + uint32_t unsigned_first = static_cast(top) & 0x00ff'ffffU; + uint32_t unsigned_second = static_cast(second) & 0x00ff'ffffU; + + m_stack.back() = static_cast(unsigned_second / unsigned_first); + break; + } + case operation::MOD: { + if (m_flags.warnings) { + if (m_stack.size() < 2) UNLIKELY { + cerr << "Warning: Attempt to divide from stack with fewer than 2 elements.\n"; + } + if (!m_stack.empty() && m_stack.back() == INT24_C(0)) UNLIKELY { + cerr << "Warning: Attempted division by zero.\n"; + } + } + int24_t top = m_stack.back(); + m_stack.pop_back(); + m_stack.back() %= top; + break; + } + case operation::PSC: { + m_status = status::idle; + int24_t next = instr.get_arg().number; + m_stack.push_back(next); + break; + } + case operation::PSI: { + m_status = status::idle; + int24_t next = instr.get_arg().number; + + if (m_flags.warnings) { + if (next < (int24_t)'0' || next > (int24_t)'9') UNLIKELY { + cerr << "Warning: Pushing non-ASCII-decimal number with " << static_cast(opcode::PSI) + << " is implementation-defined behavior.\n"; + } + } + + m_stack.push_back(next - (int24_t)'0'); + break; + } + case operation::POP: + EMPTY_PROTECT("pop from") { + m_stack.pop_back(); + } + break; + case operation::EXT: + thread::flush_and_exit(EXIT_SUCCESS); + case operation::INC: + EMPTY_PROTECT("increment") { + ++m_stack.back(); + } + break; + case operation::DEC: + EMPTY_PROTECT("decrement") { + --m_stack.back(); + } + break; + case operation::AND: { + SIZE_CHECK("bitwise and", 2); + int24_t top = m_stack.back(); + m_stack.pop_back(); + m_stack.back() &= top; + break; + } + case operation::IOR: { + SIZE_CHECK("bitwise or", 2); + int24_t top = m_stack.back(); + m_stack.pop_back(); + m_stack.back() |= top; + break; + } + case operation::XOR: { + SIZE_CHECK("bitwise xor", 2); + int24_t top = m_stack.back(); + m_stack.pop_back(); + m_stack.back() ^= top; + break; + } + case operation::NOT: + EMPTY_PROTECT("complement") { + m_stack.back() = ~m_stack.back(); + } + break; + case operation::GTC: + if (m_flags.assume_ascii) { + int character = getchar(); + if (character == EOF) { + m_stack.push_back(INT24_C(-1)); + } else { + if (m_flags.warnings && (character & 0x7f) != character) UNLIKELY { + cerr << "Non-ASCII byte read.\n"; + } + m_stack.emplace_back(character); + } + } else { + m_stack.push_back(get_unichar()); + } + break; + case operation::PTC: + EMPTY_PROTECT("print from") { + bool should_print = true; + + if (m_stack.back() < INT24_C(0)) UNLIKELY { + if (m_flags.warnings) { + cerr << "Warning: Attempt to print character with negative value.\n"; + should_print = false; + } + if (m_flags.pipekill) { + thread::flush_and_exit(EXIT_FAILURE); + } + } + + if (should_print) { + if (m_flags.assume_ascii) { + int24_t back = m_stack.back(); + if (m_flags.warnings && (back & INT24_C(0x7f)) != back) UNLIKELY { + cerr << "Warning: Printing non-ASCII value.\n"; + } + putchar(back); + } else { + print_unichar(m_stack.back()); + } + +#ifdef __EMSCRIPTEN__ + if (m_flags.debug) { + cout << flush; + } +#endif + } + } + + if (m_flags.pipekill && ferror(stdout)) { + // No need to flush stdout, it's already closed + cerr << flush; + exit(EXIT_SUCCESS); + } + + break; + case operation::GTI: { + int32_t i = -1; + + while (!(feof(stdin) || scanf("%" SCNi32, &i))) { + DISCARD getchar(); + } + + m_stack.emplace_back(i); + break; + } + case operation::PTI: + EMPTY_PROTECT("print from") { + cout << m_stack.back() << '\n'; + } + + if (m_flags.pipekill && ferror(stdout)) { + cerr << flush; + exit(EXIT_SUCCESS); + } + break; + case operation::PTU: + EMPTY_PROTECT("print from") { + cout << (static_cast(m_stack.back()) & 0x00ff'ffffU) << '\n'; + } + + if (m_flags.pipekill && ferror(stdout)) { + cerr << flush; + exit(EXIT_SUCCESS); + } + break; + case operation::IDX: { + if (m_flags.warnings && m_stack.empty()) UNLIKELY { + cerr << "Warning: Attempt to read index from empty stack.\n"; + break; + } + + size_t index = static_cast(m_stack.back()) & SIZE_C(0x00ff'ffff); + m_stack.pop_back(); + + if (m_flags.warnings && m_stack.size() < index + 1) UNLIKELY { + cerr << "Warning: Attempt to index out of stack bounds (size = " << m_stack.size() + << ", index = " << index << ")\n"; + } + + size_t i = m_stack.size() - index - 1; + m_stack.push_back(m_stack[i]); + + break; + } + case operation::DUP: + EMPTY_PROTECT("duplicate") { + m_stack.push_back(m_stack.back()); + } + break; + case operation::RND: + m_stack.emplace_back(distr(engine)); + break; + case operation::EXP: + EMPTY_PROTECT("exponentiate") { + m_stack.back() = INT24_C(1) << m_stack.back(); + } + break; + case operation::SWP: { + SIZE_CHECK("swap in", 2); + size_t i = m_stack.size() - 2; + std::swap(m_stack[i], m_stack[i + 1]); + break; + } + case operation::TSP: + m_status = status::splitting; + break; + case operation::TJN: + m_status = status::waiting; + break; + case operation::TKL: + m_status = status::terminated; + break; + case operation::GTM: { + m_stack.push_back(get_time()); + break; + } + case operation::GDT: { + m_stack.push_back(get_date()); + break; + } + case operation::DP2: + SIZE_CHECK("2-dupe", 2); + m_stack.push_back(m_stack[m_stack.size() - 2]); + m_stack.push_back(m_stack[m_stack.size() - 2]); + break; +// Just because they're not defined enum constants doesn't mean the input program won't have them +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wswitch" +#elif REALLY_MSVC +#pragma warning(push) +#pragma warning(disable : 4063) +#endif + case static_cast(SKP): + m_status = status::idle; + break; + case static_cast(INVALID_CHAR): + cout << flush; + cerr << flush; + fprintf( + stderr, + "Encoding error detected, or U+%04" PRIX32 " present in source.\n", + static_cast(op) + ); + exit(EXIT_FAILURE); + default: { + cerr << "Unrecognized opcode '"; + print_unichar(static_cast(op), cerr); + pair coords = m_program_holder.get_coords(m_ip); + cerr << "' (at (" << coords.first << ", " << coords.second << "))\n"; + thread::flush_and_exit(EXIT_FAILURE); + } +#ifdef __clang__ +#pragma clang diagnostic pop +#elif REALLY_MSVC +#pragma warning(pop) +#endif + } + } + +#undef SIZE_CHECK protected: - inline thread(NONNULL_PTR(const program) p, flags f) noexcept : - program_walker(p), - m_stack(), - m_ip{ { SIZE_C(0), SIZE_C(0) }, direction::southwest }, - m_status(status::active), - m_flags(f), - m_number(thread_count++) {} + inline thread(const ProgramHolder& ph, flags f) noexcept : + m_program_holder(ph), m_stack(), m_ip(), m_status(status::active), m_flags(f), m_number(thread_count++) {} - constexpr void advance() noexcept { program_walker::advance(m_ip, m_program->side_length()); } + constexpr void advance() noexcept { + // FIXME: Segfaults when spawning a new thread with an empty stack + m_program_holder.advance(m_ip, [&]() NOEXCEPT_T { + EMPTY_PROTECT("branch on") {} + return m_stack.back() < INT24_C(0); + }); + } +#undef EMPTY_PROTECT + ProgramHolder m_program_holder; std::vector m_stack; - NO_UNIQUE_ADDRESS instruction_pointer m_ip; + NO_UNIQUE_ADDRESS typename ProgramHolder::IP m_ip; status m_status; flags m_flags; - - static_assert( - sizeof(instruction_pointer) % sizeof(size_t) + sizeof(status) + sizeof(flags) <= sizeof(size_t), - "flags, status, or instruction_pointer got too big, adding an entire extra word of padding" - ); private: + [[noreturn]] static inline void flush_and_exit(int code) { + std::cout << std::flush; + std::cerr << std::flush; +#ifdef __EMSCRIPTEN__ + // https://github.com/bbrk24/Trilangle/issues/4 + while (!feof(stdin)) { + DISCARD getchar(); + } +#endif + exit(code); + } + unsigned long m_number; }; - -extern "C" void send_debug_info( - unsigned long thread_number, - const int24_t* stack, - size_t stack_depth, - size_t y, - size_t x, - int32_t instruction -); diff --git a/tests/unit/ip_movement.hh b/tests/unit/ip_movement.hh index a38fedb..1020a5c 100644 --- a/tests/unit/ip_movement.hh +++ b/tests/unit/ip_movement.hh @@ -75,7 +75,7 @@ public: test_iter(ip_advance, test_programs, input) { const program& p = input.second; - program_walker::instruction_pointer ip{ { SIZE_C(0), SIZE_C(0) }, input.first }; + instruction_pointer ip{ { SIZE_C(0), SIZE_C(0) }, input.first }; for (int24_t i = '0'; i <= '9'; ++i) { test_assert(p.at(ip.coords.first, ip.coords.second) == i); From 3c94d355410f3c25a777402f173eb7ccfd198e3a Mon Sep 17 00:00:00 2001 From: William Baker Date: Sun, 22 Sep 2024 22:21:41 -0400 Subject: [PATCH 04/21] Get this working, mostly There's still something subtly wrong, but I haven't diagnosed what. The new tests that reference the qdeql interpreter are ccompletely broken as a result, and therefore commented out. Further testing and bugfixing is required. --- qdeql/disassembly.txt | 2 +- src/any_program_holder.hh | 6 +++--- src/assembly_scanner.cpp | 42 ++++++++++++++++++++++++++----------- src/assembly_scanner.hh | 8 +++---- src/instruction.hh | 10 +++++++++ src/interpreter.hh | 17 ++++++++++----- src/program_walker.hh | 6 +++--- src/thread.hh | 20 +++++++++--------- tests/cli/assembly/cat.txt | 8 +++++++ tests/cli/assembly/index.sh | 26 +++++++++++++++++++++++ tests/cli/qdeql/index.sh | 13 ++++++++++++ 11 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 tests/cli/assembly/cat.txt create mode 100755 tests/cli/assembly/index.sh diff --git a/qdeql/disassembly.txt b/qdeql/disassembly.txt index 40eed09..888cdf0 100644 --- a/qdeql/disassembly.txt +++ b/qdeql/disassembly.txt @@ -296,7 +296,7 @@ ct_bgn_not_end: ct_bgn_not_end_cleanup: POP ; Remove the uop from the stack DEC ; Advance the PC - JMP ct_find_end_loop + JMP ct_bgn_find_end_loop ct_bgn_found_end: ; If PC is pointing at the end uop, the stack layout is this: ; +======+------+----+ diff --git a/src/any_program_holder.hh b/src/any_program_holder.hh index d48310f..083c625 100644 --- a/src/any_program_holder.hh +++ b/src/any_program_holder.hh @@ -6,8 +6,8 @@ template class any_program_holder { public: - virtual void advance(IP& ip, std::function go_left) const = 0; - virtual instruction at(const IP& ip) const = 0; - virtual std::string raw_at(const IP& ip) const = 0; + virtual void advance(IP& ip, std::function go_left) = 0; + virtual instruction at(const IP& ip) = 0; + virtual std::string raw_at(const IP& ip) = 0; virtual std::pair get_coords(const IP& ip) const = 0; }; diff --git a/src/assembly_scanner.cpp b/src/assembly_scanner.cpp index b3b6d8c..d194768 100644 --- a/src/assembly_scanner.cpp +++ b/src/assembly_scanner.cpp @@ -87,7 +87,9 @@ NONNULL_PTR(const std::vector)>) assembly_s string label = curr_line.substr(0, i); DISCARD label_names.insert(label); - auto p = label_locations.insert({ label, get_current_location() }); + // Add a NOP for the label to point to + add_instruction({ instruction::operation::NOP, instruction::argument() }); + auto p = m_label_locations.insert({ label, get_current_location() }); if (!p.second) { cerr << "Label '" << label << "' appears twice" << endl; @@ -182,6 +184,10 @@ NONNULL_PTR(const std::vector)>) assembly_s } } + // for (const auto& el : m_label_locations) { + // std::cout << "{ " << el.second.first << ", " << el.second.second << " }: " << el.first << std::endl; + // } + // Second pass for (auto* fragment : *m_fragments) { for (auto& instr : *fragment) { @@ -196,35 +202,41 @@ NONNULL_PTR(const std::vector)>) assembly_s return m_fragments; } -void assembly_scanner::advance(IP& ip, std::function go_left) const { - assert(m_fragments != nullptr); +void assembly_scanner::advance(IP& ip, std::function go_left) { + instruction i = at(ip); - const IP* to_left = at(ip).first_if_branch(); + if (i.get_op() == instruction::operation::JMP) { + ip = i.get_arg().next; + return; + } + + const IP* to_left = i.second_if_branch(); if (to_left != nullptr && go_left()) { ip = *to_left; - return; } ip.second++; - if (ip.second > m_fragments->at(ip.first)->size()) { + if (ip.second >= m_fragments->at(ip.first)->size()) { ip.first++; ip.second = 0; } - if (ip.first > m_fragments->size()) { + if (ip.first >= m_fragments->size()) { ip.first = 0; } } -string assembly_scanner::raw_at(const IP& ip) const { +string assembly_scanner::raw_at(const IP& ip) { // TODO: call disassembler::to_str (move it to somewhere common?) - return ""; + char buf[12]; + snprintf(buf, sizeof buf, "%" PRId32, static_cast(at(ip).get_op())); + return string(buf); } assembly_scanner::IP assembly_scanner::get_current_location() const { size_t first = this->m_fragments->size(); size_t second = 0; - if (first > 0) { - --first; + --first; + if (first != SIZE_MAX) { const auto* ptr = this->m_fragments->at(first); second = ptr->size(); if (second > 0) { @@ -256,7 +268,13 @@ void assembly_scanner::fake_location_to_real(IP& p) const { #endif static_cast(p.second); auto ptr = reinterpret_cast(reconstructed); - p = label_locations.find(*ptr)->second; + const string& str = *ptr; + auto loc = m_label_locations.find(str); + if (loc == m_label_locations.end()) { + cerr << "Undeclared label '" << str << "'" << endl; + exit(EXIT_FAILURE); + } + p = loc->second; } #define DESTRINGIFY_NAME(op) \ diff --git a/src/assembly_scanner.hh b/src/assembly_scanner.hh index 72f3d8d..94569c6 100644 --- a/src/assembly_scanner.hh +++ b/src/assembly_scanner.hh @@ -22,16 +22,16 @@ public: NONNULL_PTR(const std::vector)>) get_fragments(); - void advance(IP& ip, std::function go_left) const; - std::string raw_at(const IP& ip) const; + void advance(IP& ip, std::function go_left); + std::string raw_at(const IP& ip); inline std::pair get_coords(const IP& ip) const { return ip; } - inline instruction at(const IP& ip) const { return m_fragments->at(ip.first)->at(ip.second); } + inline instruction at(const IP& ip) { return get_fragments()->at(ip.first)->at(ip.second); } protected: void fake_location_to_real(IP& p) const; static instruction::operation opcode_for_name(const std::string& name) noexcept; - std::unordered_map label_locations; + std::unordered_map m_label_locations; private: IP get_current_location() const; void add_instruction(instruction&& i); diff --git a/src/instruction.hh b/src/instruction.hh index 3e62283..11038d4 100644 --- a/src/instruction.hh +++ b/src/instruction.hh @@ -67,6 +67,16 @@ public: } } + CONSTEXPR_UNION const pair* second_if_branch() const noexcept { + switch (m_op) { + case operation::BNG: + case operation::TSP: + return &this->m_arg.choice.second; + default: + return nullptr; + } + } + constexpr operation get_op() const noexcept { return m_op; } CONSTEXPR_UNION const argument& get_arg() const noexcept { return m_arg; } protected: diff --git a/src/interpreter.hh b/src/interpreter.hh index 9502963..2284d1e 100644 --- a/src/interpreter.hh +++ b/src/interpreter.hh @@ -14,8 +14,8 @@ extern "C" void send_thread_count(size_t tc); template class interpreter { public: - inline interpreter(const ProgramHolder& p, flags f) noexcept : - m_program_holder(p), m_threads{ thread(p, f) } {} + inline interpreter(ProgramHolder& p, flags f) noexcept : + m_program_holder(p), m_threads{ thread(&p, f) } {} inline void run() { using status = typename thread::status; @@ -152,11 +152,18 @@ private: inline void split_thread(std::vector>& new_threads, const thread& old_thread) const { - new_threads.push_back(old_thread); - new_threads.push_back(old_thread); + thread new_thread_1 = old_thread; + new_thread_1.m_status = thread::status::active; + thread new_thread_2 = new_thread_1; + + m_program_holder.advance(new_thread_1.m_ip, []() { return true; }); + m_program_holder.advance(new_thread_2.m_ip, []() { return false; }); + + new_threads.push_back(new_thread_1); + new_threads.push_back(new_thread_2); } - const ProgramHolder& m_program_holder; + ProgramHolder& m_program_holder; std::vector> m_threads; }; diff --git a/src/program_walker.hh b/src/program_walker.hh index cd55ab5..48edad8 100644 --- a/src/program_walker.hh +++ b/src/program_walker.hh @@ -52,7 +52,7 @@ public: constexpr program_walker(NONNULL_PTR(const program) p) noexcept : m_program(p) {} - inline void advance(IP& ip, std::function go_left) const { + inline void advance(IP& ip, std::function go_left) { int24_t op = m_program->at(ip.coords.first, ip.coords.second); switch (op) { case MIR_EW: @@ -80,7 +80,7 @@ public: program_walker::advance(ip, m_program->side_length()); } - inline instruction at(const IP& ip) const noexcept { + inline instruction at(const IP& ip) noexcept { int24_t op = m_program->at(ip.coords.first, ip.coords.second); if (is_branch(op, ip.dir)) { if (op == static_cast(THR_E) || op == static_cast(THR_W)) { @@ -96,7 +96,7 @@ public: inline std::pair get_coords(const IP& ip) const { return ip.coords; } - inline std::string raw_at(const IP& ip) const { + inline std::string raw_at(const IP& ip) { std::ostringstream oss; print_unichar(m_program->at(ip.coords.first, ip.coords.second), oss); return oss.str(); diff --git a/src/thread.hh b/src/thread.hh index d6faaff..dcc3741 100644 --- a/src/thread.hh +++ b/src/thread.hh @@ -82,20 +82,20 @@ public: static std::uniform_int_distribution distr(INT24_MIN, INT24_MAX); // The operation currently being executed - instruction instr = m_program_holder.at(m_ip); + instruction instr = m_program_holder->at(m_ip); operation op = instr.get_op(); // The web interface needs every thread to send debug info. The CLI expects only active threads to do so. #ifdef __EMSCRIPTEN__ if (m_flags.debug) { - pair coords = m_program_holder.get_coords(m_ip); + pair coords = m_program_holder->get_coords(m_ip); send_debug_info( m_number, m_flags.show_stack ? m_stack.data() : nullptr, m_stack.size(), coords.first, coords.second, - m_program_holder.raw_at(m_ip).c_str() + m_program_holder->raw_at(m_ip).c_str() ); } #endif @@ -117,14 +117,14 @@ public: #ifndef __EMSCRIPTEN__ // Print the requisite information in debug mode. if (m_flags.debug) { - pair coords = m_program_holder.get_coords(m_ip); + pair coords = m_program_holder->get_coords(m_ip); send_debug_info( m_number, m_flags.show_stack ? m_stack.data() : nullptr, m_stack.size(), coords.first, coords.second, - m_program_holder.raw_at(m_ip).c_str() + m_program_holder->raw_at(m_ip).c_str() ); } #endif @@ -133,6 +133,7 @@ public: switch (op) { case operation::BNG: case operation::NOP: + case operation::JMP: break; case operation::ADD: { SIZE_CHECK("add from", 2); @@ -464,7 +465,7 @@ public: default: { cerr << "Unrecognized opcode '"; print_unichar(static_cast(op), cerr); - pair coords = m_program_holder.get_coords(m_ip); + pair coords = m_program_holder->get_coords(m_ip); cerr << "' (at (" << coords.first << ", " << coords.second << "))\n"; thread::flush_and_exit(EXIT_FAILURE); } @@ -478,19 +479,18 @@ public: #undef SIZE_CHECK protected: - inline thread(const ProgramHolder& ph, flags f) noexcept : + inline thread(ProgramHolder* ph, flags f) noexcept : m_program_holder(ph), m_stack(), m_ip(), m_status(status::active), m_flags(f), m_number(thread_count++) {} constexpr void advance() noexcept { - // FIXME: Segfaults when spawning a new thread with an empty stack - m_program_holder.advance(m_ip, [&]() NOEXCEPT_T { + m_program_holder->advance(m_ip, [&]() NOEXCEPT_T { EMPTY_PROTECT("branch on") {} return m_stack.back() < INT24_C(0); }); } #undef EMPTY_PROTECT - ProgramHolder m_program_holder; + ProgramHolder* m_program_holder; std::vector m_stack; NO_UNIQUE_ADDRESS typename ProgramHolder::IP m_ip; status m_status; diff --git a/tests/cli/assembly/cat.txt b/tests/cli/assembly/cat.txt new file mode 100644 index 0000000..e6949a9 --- /dev/null +++ b/tests/cli/assembly/cat.txt @@ -0,0 +1,8 @@ +start: + GTC + BNG end + PTC + POP + JMP start +end: + EXT diff --git a/tests/cli/assembly/index.sh b/tests/cli/assembly/index.sh new file mode 100755 index 0000000..47ffffc --- /dev/null +++ b/tests/cli/assembly/index.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -eu + +# Adapted from https://cedwards.xyz/defer-for-shell/ +DEFER='' +defer () { + DEFER="$*; $DEFER" + # shellcheck disable=SC2064 + trap "{ $DEFER}" EXIT +} + +text=' + /\_/\ ___ + = ಠ_ಠ =_______ \ \ + __^ __( \.__) ) +(@)<_____>__(_____)____/' + +folder=$(dirname "$0") + +errors=$(mktemp) +defer rm "$errors" + +output=$($TRILANGLE -fwA "${folder}/cat.txt" <<<"$text" 2>"$errors") +test ! -s "$errors" +test "$text" = "${output//$'\r'/}" diff --git a/tests/cli/qdeql/index.sh b/tests/cli/qdeql/index.sh index 3ecce90..c732ed6 100755 --- a/tests/cli/qdeql/index.sh +++ b/tests/cli/qdeql/index.sh @@ -14,8 +14,18 @@ run_qdeql () { fi } +run_qdeql_asm () { + if [ "$#" -gt 1 ] + then + printf '%s\0%s' "$(cat "$1")" "$2" | $TRILANGLE -Aaf "${root}/qdeql/disassembly.txt" + else + $TRILANGLE -A "${root}/qdeql/disassembly.txt" <"$1" + fi +} + # hello world test 'hello world' = "$(run_qdeql "${folder}/hello.qd")" +# test 'hello world' = "$(run_qdeql_asm "${folder}/hello.qd")" # cat # Qdeql is limited to bytes, so this cat can't be the same Unicode cat @@ -28,6 +38,9 @@ text=' output=$(run_qdeql "${folder}/cat.qd" "$text") test "$text" = "${output//$'\r'/}" +# output=$(run_qdeql_asm "${folder}/cat.qd" "$text") +# test "$text" = "${output//$'\r'/}" + # truth machine test 0 = "$(run_qdeql "${folder}/tm.qd" 0)" test 1111111111 = "$(run_qdeql "${folder}/tm.qd" 1 | head -c 10)" From d22645b927b56850585660052cc5bb661d0e9128 Mon Sep 17 00:00:00 2001 From: William Baker Date: Sun, 22 Sep 2024 23:33:05 -0400 Subject: [PATCH 05/21] fix PSI argument parsing --- src/assembly_scanner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assembly_scanner.cpp b/src/assembly_scanner.cpp index d194768..c374b16 100644 --- a/src/assembly_scanner.cpp +++ b/src/assembly_scanner.cpp @@ -165,7 +165,7 @@ NONNULL_PTR(const std::vector)>) assembly_s } arg_value = static_cast(ul); } else if (argument[0] == '#' && argument.size() == 2) { - arg_value = static_cast(argument[1] - '0'); + arg_value = argument[1]; if (arg_value < INT24_C(0) || arg_value > INT24_C(9)) { invalid_literal(argument); } From 5c1bf570cd7778de711e1dceb8e209b939e51c35 Mon Sep 17 00:00:00 2001 From: William Baker Date: Fri, 27 Sep 2024 23:29:35 -0400 Subject: [PATCH 06/21] various improvements more work Get more things working more documentation, mostly --- CHANGELOG.md | 4 ++++ README.md | 31 +++++++++++++++++++++++++ src/assembly_scanner.cpp | 11 +++++++-- src/assembly_scanner.hh | 4 ++-- src/input.hh | 2 +- src/main.cpp | 19 +++++++++++---- src/program_walker.hh | 2 ++ tests/cli/invalid_flags/index.sh | 26 ++++++++++++++++----- tests/cli/qdeql/index.sh | 11 ++++++--- wasm/in.civet | 40 +++++++++++++++++++++++++++----- wasm/index.html | 4 ++++ wasm/lowdata.html | 2 +- wasm/worker.civet | 15 ++++++------ 13 files changed, 138 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f8b91f..aa24337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. See [Keep a ## Unreleased +### Added + +- The interpreter can now handle assembly syntax, with the `-A` flag in the command line and a checkbox in the online interpreter. Currently, interactive debugging of assembly is not supported in the web interpreter. + ### Changed - Fixed C++23 support diff --git a/README.md b/README.md index 6e4a49a..443e8e3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Trilangle is a 2-D, stack-based programming language inspired by [Hexagony]. > - [Interpreter flags](#interpreter-flags) > - [Exit codes](#exit-codes) > - [The disassembler](#the-disassembler) +> - [Assembly syntax](#assembly-syntax) > - [C compiler](#c-compiler) > - [Sample programs](#sample-programs) > - [cat](#cat) @@ -225,6 +226,36 @@ For example, when passing [the cat program below](#cat) with the flags `-Dn`, th 2.2: EXT ``` +### Assembly syntax + +In addition to producing this syntax, Trilangle is capable of interpreting this syntax. Currently, the output with `--hide-nops` is not guaranteed to be interpretable, as it may be missing jump targets. Each line can maximally consist of a label, an instruction, and a comment. The syntax can be described with the following extended Backus-Naur form: + +``` +program = line, {newline, line}; +line = [label, [":"]], [multiple_whitespace, instruction], {whitespace}, [comment]; + +newline = ? U+000A END OF LINE ?; +tab = ? U+0009 CHARACTER TABULATION ?; +whitespace = " " | ? U+000D CARRIAGE RETURN ? | tab; +non_whitespace = ? Any single unicode character not in 'newline' or 'whitespace' ?; +multiple_whitespace = whitespace, {whitespace}; + +label = non_whitespace, {non_whitespace}; +comment = ";", {non_whitespace | whitespace}; + +instruction = instruction_with_target | instruction_with_argument | plain_instruction; +instruction_with_target = ("BNG" | "TSP" | "JMP"), multiple_whitespace, label; +instruction_with_argument = ("PSI" | "PSC"), multiple_whitespace, number_literal; +plain_instruction = ? Any three-character instruction besides the five already covered ?; + +number_literal = character_literal | decimal_literal | hex_literal; +character_literal = "'", (non_whitespace | tab), "'"; +decimal_literal = "#", decimal_digit; +hex_literal = "0x", hex_digit, {hex_digit}; +decimal_digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; +hex_digit = "a" | "b" | "c" | "d" | "e" | "f" | "A" | "B" | "C" | "D" | "E" | "F" | decimal_digit; +``` + ## C compiler When using the `-c` flag, the input program will be translated into C code. The C code is not meant to be idiomatic or easy to read, as it is a literal translation of the input. Optimizers such as those used by clang and GCC tend to do a good job of improving the program, in some cases removing the heap allocation altogether; MSVC does not. diff --git a/src/assembly_scanner.cpp b/src/assembly_scanner.cpp index c374b16..77ab98d 100644 --- a/src/assembly_scanner.cpp +++ b/src/assembly_scanner.cpp @@ -153,7 +153,7 @@ NONNULL_PTR(const std::vector)>) assembly_s invalid_literal(argument); } i = 1; - arg_value = parse_unichar([&]() { return curr_line[i++]; }); + arg_value = parse_unichar([&]() { return argument[i++]; }); if (arg_value < INT24_C(0) || i != argument.size() - 1) { invalid_literal(argument); } @@ -165,7 +165,7 @@ NONNULL_PTR(const std::vector)>) assembly_s } arg_value = static_cast(ul); } else if (argument[0] == '#' && argument.size() == 2) { - arg_value = argument[1]; + arg_value = static_cast(argument[1] - '0'); if (arg_value < INT24_C(0) || arg_value > INT24_C(9)) { invalid_literal(argument); } @@ -173,6 +173,13 @@ NONNULL_PTR(const std::vector)>) assembly_s invalid_literal(argument); } + if (opcode == instruction::operation::PSI) { + // PSI expects to be given the digit, not the actual value + auto p = arg_value.add_with_overflow('0'); + assert(!p.first); + arg_value = p.second; + } + instruction::argument arg; arg.number = arg_value; add_instruction({ opcode, arg }); diff --git a/src/assembly_scanner.hh b/src/assembly_scanner.hh index 94569c6..75c1861 100644 --- a/src/assembly_scanner.hh +++ b/src/assembly_scanner.hh @@ -8,7 +8,7 @@ class assembly_scanner : public any_program_holder > { public: using IP = std::pair; - inline assembly_scanner(std::istream* program) : m_program(program), m_fragments(nullptr) {} + inline assembly_scanner(NONNULL_PTR(std::istream) program) : m_program(program), m_fragments(nullptr) {} inline ~assembly_scanner() noexcept { if (m_fragments == nullptr) { @@ -36,6 +36,6 @@ private: IP get_current_location() const; void add_instruction(instruction&& i); - std::istream* m_program; + NONNULL_PTR(std::istream) m_program; std::vector)>* m_fragments; }; diff --git a/src/input.hh b/src/input.hh index 12323bb..dd9aba3 100644 --- a/src/input.hh +++ b/src/input.hh @@ -32,7 +32,7 @@ struct flags { constexpr bool is_valid() const noexcept { return !( (show_stack && !debug) || ((debug || warnings || pipekill || assembly) && disassemble) - || (hide_nops && !disassemble) || (expand && (debug || warnings || pipekill || disassemble)) + || (hide_nops && !disassemble) || (expand && (debug || warnings || pipekill || disassemble || assembly)) || (compile && (debug || warnings || pipekill || disassemble || expand)) || (assume_ascii && (expand || disassemble)) ); diff --git a/src/main.cpp b/src/main.cpp index 89821a9..cbc512e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,11 @@ #include "disassembler.hh" #include "interpreter.hh" +[[noreturn]] inline void empty_program() { + std::cerr << "What program do you want me to run? C'mon, give me something to work with." << std::endl; + exit(EX_DATAERR); +} + inline void execute(const std::string& prg, flags f) { #ifdef NO_BUFFER setvbuf(stdout, nullptr, _IONBF, 0); @@ -16,16 +21,17 @@ inline void execute(const std::string& prg, flags f) { if (f.assembly) { std::istringstream iss(prg); assembly_scanner as(&iss); + if (as.get_fragments()->size() == 0) { + empty_program(); + } interpreter i(as, f); i.run(); return; } program p(prg); - if (p.side_length() == 0) { - std::cerr << "What program do you want me to run? C'mon, give me something to work with." << std::endl; - exit(EX_DATAERR); + empty_program(); } if (f.disassemble) { @@ -46,7 +52,8 @@ inline void execute(const std::string& prg, flags f) { #ifdef __EMSCRIPTEN__ #include -extern "C" EMSCRIPTEN_KEEPALIVE void wasm_entrypoint(CONST_C_STR program_text, int disassemble, int expand, int debug) { +extern "C" EMSCRIPTEN_KEEPALIVE void +wasm_entrypoint(CONST_C_STR program_text, int disassemble, int expand, int debug, int assembly) { // Reset EOF from previous runs clearerr(stdin); // Input and output don't need to be synced on the web @@ -55,10 +62,12 @@ extern "C" EMSCRIPTEN_KEEPALIVE void wasm_entrypoint(CONST_C_STR program_text, i flags f; f.warnings = true; f.disassemble = disassemble; - f.hide_nops = disassemble; + // Disabled so that the output is compatible with -A + // f.hide_nops = disassemble; f.expand = expand; f.debug = debug; f.show_stack = true; + f.assembly = assembly; execute(program_text, f); } diff --git a/src/program_walker.hh b/src/program_walker.hh index 48edad8..97720d6 100644 --- a/src/program_walker.hh +++ b/src/program_walker.hh @@ -76,6 +76,8 @@ public: case PSI: program_walker::advance(ip, m_program->side_length()); break; + default: + break; } program_walker::advance(ip, m_program->side_length()); } diff --git a/tests/cli/invalid_flags/index.sh b/tests/cli/invalid_flags/index.sh index 7ec3e5a..652fff5 100755 --- a/tests/cli/invalid_flags/index.sh +++ b/tests/cli/invalid_flags/index.sh @@ -1,10 +1,24 @@ #!/bin/bash -set -u +set -eu -errors=$($TRILANGLE - 2>&1 1<&-) -result=$? -set -e -test 0 -ne $result +invalid_flags=( + # Not actually a flag + - + # various mode flags together + -Ae -ce -De -AD -Dc + # --assume-ascii without actually executing + -ae -aD + # --show-stack without --debug + -s + # --hide-nops without --disassemble + -n +) -test -n "$errors" +for flag in "${invalid_flags[@]}" +do + result=0 + errors=$($TRILANGLE "$flag" 2>&1 1<&-) || result=$? + test 64 = $result + test -n "$errors" +done diff --git a/tests/cli/qdeql/index.sh b/tests/cli/qdeql/index.sh index c732ed6..015ffc8 100755 --- a/tests/cli/qdeql/index.sh +++ b/tests/cli/qdeql/index.sh @@ -25,7 +25,7 @@ run_qdeql_asm () { # hello world test 'hello world' = "$(run_qdeql "${folder}/hello.qd")" -# test 'hello world' = "$(run_qdeql_asm "${folder}/hello.qd")" +test 'hello world' = "$(run_qdeql_asm "${folder}/hello.qd")" # cat # Qdeql is limited to bytes, so this cat can't be the same Unicode cat @@ -38,15 +38,20 @@ text=' output=$(run_qdeql "${folder}/cat.qd" "$text") test "$text" = "${output//$'\r'/}" -# output=$(run_qdeql_asm "${folder}/cat.qd" "$text") -# test "$text" = "${output//$'\r'/}" +output=$(run_qdeql_asm "${folder}/cat.qd" "$text") +test "$text" = "${output//$'\r'/}" # truth machine test 0 = "$(run_qdeql "${folder}/tm.qd" 0)" test 1111111111 = "$(run_qdeql "${folder}/tm.qd" 1 | head -c 10)" +test 0 = "$(run_qdeql_asm "${folder}/tm.qd" 0)" +test 1111111111 = "$(run_qdeql_asm "${folder}/tm.qd" 1 | head -c 10)" # adder test $'\x05' = "$(run_qdeql "${folder}/sum.qd" $'\x02\x03')" +test $'\x05' = "$(run_qdeql_asm "${folder}/sum.qd" $'\x02\x03')" # bash complains if I pass a null byte in one of the arguments to run_qdeql test $'\x03' = "$(printf '%s\0\0\x03' "$(cat "${folder}/sum.qd")" | $TRILANGLE -a "${root}/qdeql/interpreter.trg")" test $'\x02' = "$(printf '%s\0\x02\0' "$(cat "${folder}/sum.qd")" | $TRILANGLE -a "${root}/qdeql/interpreter.trg")" +test $'\x03' = "$(printf '%s\0\0\x03' "$(cat "${folder}/sum.qd")" | $TRILANGLE -Aa "${root}/qdeql/disassembly.txt")" +test $'\x02' = "$(printf '%s\0\x02\0' "$(cat "${folder}/sum.qd")" | $TRILANGLE -Aa "${root}/qdeql/disassembly.txt")" diff --git a/wasm/in.civet b/wasm/in.civet index 8d4576a..d9d1780 100644 --- a/wasm/in.civet +++ b/wasm/in.civet @@ -34,6 +34,7 @@ interval .= -1 let step: => decoder := new TextDecoder colors := new Colors +isAssembly .= false // This is a clever little hack: elements.fooBar is the element with id="foo-bar". It uses document.getElementById on // first access, but then saves it for fast access later. @@ -49,7 +50,9 @@ clearOutput := :void => elements.stderr.innerHTML = '' generateContracted := :string => - programText .= (elements.program as! HTMLTextAreaElement).value.replace /\n| /gu, '' + programText .= (elements.program as! HTMLTextAreaElement).value + return programText if isAssembly + programText |>= .replace /\n| /gu, '' // programText.length is wrong when there's high Unicode characters programLength := [programText...]# // Calculate the largest triangular number less than the length. @@ -76,6 +79,8 @@ generateURL := :void => url.hash = '#' + encodeURIComponent generateContracted() if elements.includeInput.checked url.searchParams.set 'i', (elements.stdin as! HTMLTextAreaElement).value + if elements.assembly.checked + url.searchParams.set 'a', '1' history.pushState {}, '', url elements.urlOut.textContent = url.href elements.urlOutBox.className = '' @@ -143,10 +148,10 @@ createWorker := (name: string) => :Promise => elements.stdin.oninput = :void -> worker = undefined @oninput = null - elements.expand.disabled = false - elements.disassemble.disabled = false - elements.condense.disabled = false - elements.debug.disabled = false + elements.expand.disabled = isAssembly + elements.disassemble.disabled = isAssembly + elements.condense.disabled = isAssembly + elements.debug.disabled = isAssembly elements.runStop.textContent = 'Run!' elements.runStop.onclick = interpretProgram @@ -184,10 +189,13 @@ createWorker := (name: string) => :Promise => promise -interpretProgram := createWorker 'interpretProgram' +interpretBase := createWorker 'interpretProgram' disassembleProgram := createWorker 'disassembleProgram' expandBase := createWorker 'expandInput' debugBase := createWorker 'debugProgram' +interpretAssembly := createWorker 'interpretAssembly' + +interpretProgram := => if isAssembly then interpretAssembly() else interpretBase() expandInput := :Promise => await expandBase() @@ -389,6 +397,7 @@ do elements.urlButton.onclick = generateURL elements.program.addEventListener 'input', hideUrl, { +passive } elements.includeInput.addEventListener 'change', hideUrl, { +passive } + elements.assembly.addEventListener 'change', hideUrl, { +passive } elements.stdin.addEventListener 'change', => hideUrl() if elements.includeInput.checked, { +passive } // Allow the header itself, or noninteractable children (e.g. the select icon) @@ -436,6 +445,16 @@ do @ontouchcancel = null @ontouchend = null + (elements.assembly as! HTMLInputElement).addEventListener + 'change' + :void -> + isAssembly = @checked + elements.expand.disabled = isAssembly + elements.disassemble.disabled = isAssembly + elements.condense.disabled = isAssembly + elements.debug.disabled = isAssembly + { +passive } + width := elements.runStop.offsetWidth remSize := parseFloat getComputedStyle(document.body).fontSize elements.runStop.textContent = 'Run!' @@ -452,6 +471,15 @@ do if inputParam? := params.get 'i' elements.stdin.value = inputParam elements.includeInput.checked = true + assemblyParam := params.get 'a' + if Number assemblyParam + isAssembly = true catch e console.error e elements.stderr.innerText = 'Error while loading program from URL.' + finally + elements.assembly.checked = isAssembly + elements.expand.disabled = isAssembly + elements.disassemble.disabled = isAssembly + elements.condense.disabled = isAssembly + elements.debug.disabled = isAssembly diff --git a/wasm/index.html b/wasm/index.html index 48f7927..a516fba 100644 --- a/wasm/index.html +++ b/wasm/index.html @@ -86,6 +86,10 @@ +
+ + +
URL: