diff --git a/.github/workflows/lint-build.yml b/.github/workflows/lint-build.yml index 9376c94..eb65deb 100644 --- a/.github/workflows/lint-build.yml +++ b/.github/workflows/lint-build.yml @@ -6,7 +6,7 @@ name: Lint Builds paths: # It would be nice if I could specify this per-job, but I'm not making a whole new file just to avoid running # clang for web-only changes. - - src/* + - src/** - wasm/* - .clang-format - .github/workflows/lint-build.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e3e214..430eba8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ name: Run Tests branches: - '**' paths: - - src/* + - src/** - tests/** - .github/workflows/test.yml - .gitmodules @@ -37,12 +37,17 @@ jobs: shell: bash run: | g++ -Og -std=gnu++17 src/*.cpp -o trilangle - export PATH="${PATH}:${PWD}" for script in tests/cli/*/index.sh do echo "Running ${script}..." - "$script" + TRILANGLE=./trilangle "$script" done + for script in tests/compiler/*/index.sh + do + echo "Running ${script}..." + TRILANGLE=./trgc.sh "$script" + done + echo 'Done.' cli-tests-windows: runs-on: windows-latest name: Run CLI Tests for Windows @@ -57,9 +62,8 @@ jobs: # Screw it, I'm not figuring out how to make this work in PowerShell. shell: bash run: | - export PATH="${PATH}:${PWD}" for script in tests/cli/*/index.sh do echo "Running ${script}..." - "$script" + TRILANGLE=./trilangle "$script" done diff --git a/.gitignore b/.gitignore index 8e43067..9d3118e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ wasm/*.css *.obj trilangle *.nativecodeanalysis.xml +out.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a9ef7d..901acdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. See [Keep a - Added prebuilt binaries for ARM64 macOS, x86 Windows, and ARM64 Windows. - Added a 'play' feature to the online debugger, to automatically step through the code. +- Added a compile mode to emit C code. +- Added `-a` flag to assume ASCII I/O. ## [1.5.0] - 2023-08-31 diff --git a/src/compiler.cpp b/src/compiler.cpp new file mode 100644 index 0000000..1f337d2 --- /dev/null +++ b/src/compiler.cpp @@ -0,0 +1,160 @@ +// This file is not in the compiler folder because the MSVC build script requires this folder to contain all the .cpp +// files. +#include "compiler.hh" +#include +#include "compiler/strings.hh" + +using std::cerr; +using std::endl; + +void compiler::write_state(std::ostream& os) { + if (m_fragments == nullptr) { + build_state(); + } + + os << header; + + for (size_t i = 0; i < m_fragments->size(); ++i) { + const std::vector& frag = *m_fragments->at(i); + for (size_t j = 0; j < frag.size(); ++j) { + os << "\nlbl" << i << '_' << j << ": "; + get_c_code(frag[j], os, m_flags.assume_ascii); + } + } + + os << footer << std::flush; +} + +void compiler::get_c_code(const instruction& i, std::ostream& os, bool assume_ascii) { + using op = instruction::operation; + + switch (i.m_op) { + case op::BNG: { + os << "if (lws_top(stack) < 0) goto lbl"; + const auto& choice = i.m_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; + os << dest.first << '_' << dest.second << ';'; + return; + } + case op::TKL: + case op::EXT: + os << "lws_deinit(stack); return 0;"; + return; + case op::TJN: + case op::TSP: + cerr << "Threading is not supported for compiled programs." << endl; + exit(EXIT_FAILURE); + case op::NOP: + break; + case op::ADD: + os << "lws_push(stack, lws_pop(stack) + lws_pop(stack));"; + return; + case op::SUB: + // The calls need to be well-ordered + os << "{ int32_t tmp = lws_pop(stack); lws_push(stack, lws_pop(stack) - tmp); }"; + return; + case op::MUL: + os << "lws_push(stack, lws_pop(stack) * lws_pop(stack));"; + return; + case op::DIV: + os << "{ int32_t tmp = lws_pop(stack); lws_push(stack, lws_pop(stack) / tmp); }"; + return; + case op::MOD: + os << "{ int32_t tmp = lws_pop(stack); lws_push(stack, lws_pop(stack) % tmp); }"; + return; + case op::PSC: { + os << "lws_push(stack,"; + const auto value = i.m_arg.number; + os << static_cast(value) << ");"; + return; + } + case op::PSI: { + os << "lws_push(stack,"; + const auto value = i.m_arg.number; + os << static_cast(value - (int24_t)'0') << ");"; + return; + } + case op::POP: + os << "(void)lws_pop(stack);"; + return; + case op::INC: + os << "lws_push(stack, (lws_pop(stack) + 1) << 8 >> 8);"; + return; + case op::DEC: + os << "lws_push(stack, (lws_pop(stack) - 1) << 8 >> 8);"; + return; + case op::AND: + os << "lws_push(stack, lws_pop(stack) & lws_pop(stack));"; + return; + case op::IOR: + os << "lws_push(stack, lws_pop(stack) | lws_pop(stack));"; + return; + case op::XOR: + os << "lws_push(stack, lws_pop(stack) ^ lws_pop(stack));"; + return; + case op::NOT: + os << "lws_push(stack, ~lws_pop(stack));"; + return; + case op::GTC: + if (assume_ascii) { + os << "{ int temp = getchar(); lws_push(stack, temp == EOF ? -1 : temp); }"; + } else { + os << "lws_push(stack, get_unichar());"; + } + return; + case op::PTC: + if (assume_ascii) { + os << "putchar"; + } else { + os << "print_unichar"; + } + os << "(lws_top(stack));"; + return; + case op::GTI: + os << "{" + "int32_t i = -1;" + "while (!(feof(stdin) || scanf(\"%\" SCNi32, &i))) (void)getchar();" + "lws_push(stack, i);" + "}"; + return; + case op::PTI: + os << R"( printf("%" PRId32 "\n", lws_top(stack)); )"; + return; + case op::IDX: + os << "lws_push(stack, lws_index(stack, lws_pop(stack)));"; + return; + case op::DUP: + os << "lws_push(stack, lws_top(stack));"; + return; + case op::DP2: { + const char* str = "lws_push(stack, lws_index(stack, 1));"; + os << str << str; + return; + } + case op::RND: + case op::GDT: + case op::GTM: + cerr << "Nondeterministic instructions are not yet supported for compiled programs." << endl; + exit(EXIT_FAILURE); + case op::EXP: + os << "lws_push(stack, 1 << lws_pop(stack));"; + return; + case op::SWP: + os << "{" + "int32_t temp1 = lws_pop(stack);" + "int32_t temp2 = lws_pop(stack);" + "lws_push(stack, temp1);" + "lws_push(stack, temp2);" + "}"; + return; + default: + cerr << "Unknown opcode." << endl; + exit(EXIT_FAILURE); + } +} diff --git a/src/compiler.hh b/src/compiler.hh new file mode 100644 index 0000000..a16874d --- /dev/null +++ b/src/compiler.hh @@ -0,0 +1,12 @@ +#pragma once + +#include "instruction_scanner.hh" + +class compiler : public instruction_scanner { +public: + CONSTEXPR_VECTOR compiler(NONNULL_PTR(const program) p, flags f) : instruction_scanner(p), m_flags(f) {} + void write_state(std::ostream& os); +private: + flags m_flags; + static void get_c_code(const instruction& i, std::ostream& os, bool assume_ascii); +}; diff --git a/src/compiler/lightweight_stack.h b/src/compiler/lightweight_stack.h new file mode 100644 index 0000000..16be997 --- /dev/null +++ b/src/compiler/lightweight_stack.h @@ -0,0 +1,110 @@ +// This entire file is a string. Comment out the opening quote marker to get syntax highlighting while editing. +R"( +#include +#include +#include + +#ifndef __GNUC__ +#define __builtin_expect(exp, c) exp +#endif + +struct lightweight_stack_s { + int32_t* base; + size_t length; + size_t capacity; +}; + +typedef struct lightweight_stack_s* lws; + +static inline int32_t lws_pop(lws stack) { + return stack->base[--stack->length]; +} + +static inline int lws_push(lws stack, int32_t value) { + if (__builtin_expect(stack->length == stack->capacity, 0L)) { + size_t new_cap = stack->capacity * 7 / 4; + int32_t* new_ptr = (int32_t*)realloc(stack->base, new_cap * sizeof(int32_t)); + if (new_ptr == NULL) { + return 0; + } + stack->base = new_ptr; + stack->capacity = new_cap; + } + stack->base[stack->length++] = value; + return 1; +} + +static inline int32_t lws_index(const lws stack, int32_t index) { + return stack->base[stack->length - index - 1]; +} + +static inline int32_t lws_top(const lws stack) { + return lws_index(stack, 0); +} + +static inline void lws_init(lws stack) { + stack->length = 0; + stack->base = (int32_t*)calloc(4, sizeof(int32_t)); + // Note: actually handling errors prevents the compiler from doing optimizations we want + stack->capacity = 4; +} + +static inline void lws_deinit(const lws stack) { + free(stack->base); +} + +#define INVALID_CHAR 0xfffd +// Basically copied from parse_unichar in string_processing.hh +static inline int32_t get_unichar() { + unsigned char buf[4]; + size_t buf_max = 4; + for (size_t i = 0; i < buf_max; ++i) { + int c = getchar(); + if (c == EOF) { + if (i == 0) { + return -1; + } else { + return INVALID_CHAR; + } + } + buf[i] = c; + if (i != 0 && (buf[i] & 0xc0) != 0x80) { + return INVALID_CHAR; + } + if (i == 0) { + if (buf[0] < 0x80) { + return buf[0]; + } else if ((buf[0] & 0xe0) == 0xc0) { + buf_max = 2; + } else if ((buf[0] & 0xf0) == 0xe0) { + buf_max = 3; + } else if ((buf[0] & 0xf8) != 0xf0) { + return INVALID_CHAR; + } + } + } + switch (buf_max) { + case 2: + return ((buf[0] & 0x1f) << 6) | (buf[1] & 0x3f); + case 3: + return ((buf[0] & 0x0f) << 12) | ((buf[1] & 0x3f) << 6) | (buf[2] & 0x3f); + case 4: + return ((buf[0] & 0x07) << 18) | ((buf[1] & 0x3f) << 12) | ((buf[2] & 0x3f) << 6) | (buf[3] & 0x3f); + } +} + +static inline void print_unichar(int32_t c) { + if (c <= 0x7f) { + putchar(c); + } else if (c <= 0x07ff) { + char buffer[] = { 0xc0 | (c >> 6), 0x80 | (c & 0x3f), 0 }; + printf(buffer); + } else if (c <= 0xffff) { + char buffer[] = { 0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f), 0 }; + printf(buffer); + } else { + char buffer[] = { 0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f), 0 }; + printf(buffer); + } +} +//)" diff --git a/src/compiler/strings.hh b/src/compiler/strings.hh new file mode 100644 index 0000000..f511f34 --- /dev/null +++ b/src/compiler/strings.hh @@ -0,0 +1,12 @@ +#pragma once + +constexpr const char* header = + "#include " +#include "lightweight_stack.h" + R"#( +int main(int argc, const char** argv) { + struct lightweight_stack_s stack_storage; + lws stack = &stack_storage; + lws_init(stack);)#"; + +constexpr const char* footer = "}\n"; diff --git a/src/disassembler.cpp b/src/disassembler.cpp index 4b23026..0ac9b90 100644 --- a/src/disassembler.cpp +++ b/src/disassembler.cpp @@ -1,10 +1,89 @@ #include "disassembler.hh" -#include +#include +#include #include -#include +#include +#include "output.hh" +using std::cerr; +using std::ostringstream; using std::pair; -using std::vector; + +#define STRING_NAME(x) \ + case operation::x: \ + os << #x; \ + return + +void disassembler::to_str(const instruction& i, std::ostream& os) { + using operation = instruction::operation; + + switch (i.m_op) { + STRING_NAME(NOP); + STRING_NAME(ADD); + STRING_NAME(SUB); + STRING_NAME(MUL); + STRING_NAME(DIV); + STRING_NAME(MOD); + STRING_NAME(POP); + STRING_NAME(EXT); + STRING_NAME(INC); + STRING_NAME(DEC); + STRING_NAME(AND); + STRING_NAME(IOR); + STRING_NAME(XOR); + STRING_NAME(NOT); + STRING_NAME(GTC); + STRING_NAME(PTC); + STRING_NAME(GTI); + STRING_NAME(PTI); + STRING_NAME(IDX); + STRING_NAME(DUP); + STRING_NAME(RND); + STRING_NAME(EXP); + STRING_NAME(SWP); + STRING_NAME(GTM); + STRING_NAME(GDT); + STRING_NAME(DP2); + STRING_NAME(TKL); + STRING_NAME(TJN); + case operation::PSI: { + int24_t value = i.m_arg.number; + os << "PSI #"; + print_unichar(value, os); + return; + } + case operation::PSC: { + int24_t value = i.m_arg.number; + os << "PSC '"; + print_unichar(value, os); + os << "' ; 0x"; + char buf[7]; + snprintf(buf, sizeof buf, "%" PRIx32, static_cast(value)); + os << buf; + return; + } + case operation::JMP: { + pair target = i.m_arg.next; + os << "JMP " << target.first << "." << target.second; + return; + } + case operation::BNG: { + pair target = i.m_arg.choice.second; + os << "BNG " << target.first << "." << target.second; + return; + } + case operation::TSP: { + pair target = i.m_arg.choice.second; + os << "TSP " << target.first << "." << target.second; + return; + } + default: + cerr << "Unrecognized opcode '"; + print_unichar(static_cast(i.m_op), cerr); + cerr << '\'' << std::endl; + exit(EXIT_FAILURE); + } +} void disassembler::write_state(std::ostream& os) { if (m_fragments == nullptr) { @@ -12,7 +91,7 @@ void disassembler::write_state(std::ostream& os) { } for (size_t i = 0; i < m_fragments->size(); ++i) { - const vector& frag = *m_fragments->at(i); + const std::vector& frag = *m_fragments->at(i); for (size_t j = 0; j < frag.size(); ++j) { // skip NOPs if requested const instruction& ins = frag[j]; @@ -23,10 +102,10 @@ void disassembler::write_state(std::ostream& os) { // write out the label os << i << '.' << j << ":\t"; // write out the instruction name - os << ins.to_str(); + to_str(ins, os); // if it's a branch followed by a jump, write that - const pair* next = ins.first_if_branch(); + const std::pair* next = ins.first_if_branch(); if (next != nullptr && (next->first != i + 1 || next->second != 0)) { os << "\n\tJMP " << next->first << '.' << next->second; } @@ -39,143 +118,3 @@ void disassembler::write_state(std::ostream& os) { // Flush the buffer, if applicable os << std::flush; } - -void disassembler::build_state() { - // https://langdev.stackexchange.com/a/659/15 - - m_fragments = new vector*>{ nullptr }; - // A map of IP -> location in fragments - std::unordered_map> location_map; - - // A collection of fragments that have yet to be visited, corresponding to nulls in m_fragments. The first item of - // the pair is the index within m_fragments, and the second item is the IP where that fragment begins. - std::list> unvisited_fragments = { - { SIZE_C(0), { { SIZE_C(0), SIZE_C(0) }, direction::southwest } } - }; - - // !empty-back-pop reflects the while let loop in the Rust code in the answer linked above - while (!unvisited_fragments.empty()) { - pair p = unvisited_fragments.back(); - unvisited_fragments.pop_back(); - size_t& index = p.first; - instruction_pointer& ip = p.second; - - auto* fragment = new vector(); - - int24_t op = m_program->at(ip.coords.first, ip.coords.second); - while (!is_branch(op, ip.dir)) { - auto loc = location_map.find(ip); - if (loc == location_map.end()) { - // Special-case TJN - // Unlike all other instructions, if TJN at the same location is hit from different directions, it - // MUST be emitted into the assembly exactly once. - instruction i(ip, *m_program); - if (i.is_join()) { - // flip the north/south bit on the direction and check again - loc = location_map.find({ ip.coords, static_cast(static_cast(ip.dir) ^ 0b100) }); - if (loc != location_map.end()) { - location_map.insert({ ip, loc->second }); - - // jump to the other one - fragment->push_back(instruction::jump_to(loc->second)); - // Yeah, yeah, goto is bad practice. You look at this loop and tell me you'd rather do it - // another way. - goto continue_outer; - } - } - - location_map.insert({ ip, { index, fragment->size() } }); - fragment->push_back(i); - - if (i.is_exit()) { - // doesn't end in a jump nor a branch - goto continue_outer; - } - - 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_W: - case BNG_NE: - case BNG_NW: - case BNG_SE: - case BNG_SW: - case THR_E: - case THR_W: - program_walker::branch(ip.dir, op, []() NOEXCEPT_T -> bool { - unreachable("is_branch should've returned true"); - }); - break; - case SKP: - case PSI: - case PSC: - program_walker::advance(ip, m_program->side_length()); - break; - default: - break; - } - - program_walker::advance(ip, m_program->side_length()); - } else { - fragment->push_back(instruction::jump_to(loc->second)); - goto continue_outer; - } - - op = m_program->at(ip.coords.first, ip.coords.second); - } - - // Putting all the variables in this scope so that way continue_outer can't see them - { - // Determine whether to emit a jump or branch. If it's a branch, determine what the targets are. - auto loc = location_map.find(ip); - if (loc == location_map.end()) { - location_map.insert({ ip, { index, fragment->size() } }); - - // BNG requires that its first target go right and its second go left. TSP doesn't really care. - instruction_pointer first_ip = ip; - program_walker::branch(first_ip.dir, op, []() NOEXCEPT_T { return false; }); - program_walker::advance(first_ip, m_program->side_length()); - - instruction_pointer second_ip = ip; - program_walker::branch(second_ip.dir, op, []() NOEXCEPT_T { return true; }); - program_walker::advance(second_ip, m_program->side_length()); - - pair first_dest, second_dest; - - // Pushing the first one to the end we're about to read from, and the second one to the far end, - // minimizes the total number of jumps needed. The second target is going to be pointed to by the BNG or - // TSP instruction anyways, but the first target only needs to be explicitly named if it's not the - // next instruction. - loc = location_map.find(first_ip); - if (loc == location_map.end()) { - first_dest = { m_fragments->size(), SIZE_C(0) }; - unvisited_fragments.push_back({ m_fragments->size(), first_ip }); - m_fragments->push_back(nullptr); - } else { - first_dest = loc->second; - } - - loc = location_map.find(second_ip); - if (loc == location_map.end()) { - second_dest = { m_fragments->size(), SIZE_C(0) }; - unvisited_fragments.push_front({ m_fragments->size(), second_ip }); - m_fragments->push_back(nullptr); - } else { - second_dest = loc->second; - } - - fragment->push_back(instruction::branch_to({ first_dest, second_dest })); - } else { - fragment->push_back(instruction::jump_to(loc->second)); - } - } - - continue_outer: - m_fragments->at(index) = fragment; - } -} diff --git a/src/disassembler.hh b/src/disassembler.hh index 888a038..56263ab 100644 --- a/src/disassembler.hh +++ b/src/disassembler.hh @@ -1,33 +1,15 @@ #pragma once -#include -#include "input.hh" -#include "instruction.hh" +#include "instruction_scanner.hh" -class disassembler : public program_walker { +class disassembler : public instruction_scanner { public: - CONSTEXPR_VECTOR disassembler(NONNULL_PTR(const program) p, flags f) : - program_walker(p), m_fragments(nullptr), m_flags(f) {} - - disassembler(const disassembler&) = delete; - - CONSTEXPR_VECTOR ~disassembler() noexcept { - if (m_fragments == nullptr) { - return; - } - for (std::vector* frag : *m_fragments) { - delete frag; - } - delete m_fragments; - } + CONSTEXPR_VECTOR disassembler(NONNULL_PTR(const program) p, flags f) noexcept : + instruction_scanner(p), m_flags(f) {} // Write the state to the specified output stream. void write_state(std::ostream& os); -protected: - void build_state(); - - // The list of program "fragments". Each one ends with a branch, jump, thread-kill, or exit. - std::vector*>* m_fragments; private: const flags m_flags; + static void to_str(const instruction& i, std::ostream& os); }; diff --git a/src/input.cpp b/src/input.cpp index b0de9dc..c4ff3a5 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -36,21 +36,28 @@ static constexpr const char* FLAGS_HELP = "\t--show-stack, -s \tShow the stack while debugging. Requires\n" "\t \t--debug.\n" "\t--warnings, -w \tShow warnings for unspecified behavior.\n" - "\t--pipekill, -f \tEnd the program once STDOUT is closed.\n\n" + "\t--pipekill, -f \tEnd the program once STDOUT is closed.\n" + "\t--ascii, -a \tAssume all I/O is ASCII-only. Incompatible with" + "\t \t--disassemble and --expand.\n\n" "\t--disassemble, -D\tOutput a pseudo-assembly representation of the\n" "\t \tcode. Incompatible with --debug, --warnings, and\n" "\t \t--pipekill.\n" "\t--hide-nops, -n \tDon't include NOPs in the disassembly. Requires\n" - "\t \t--disassemble.\n\n" + "\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" + "\t \t--null.\n\n" "\t--expand, -e \tSpace the program out to fit the triangle.\n" - "\t \tIncompatible with all other flags.\n\n" + "\t \tIncompatible with all other flags except --null.\n" "\t--null, -z \tRead the program until null terminator instead of EOF."; namespace flag_container { static CONSTINIT_LAMBDA std::tuple FLAGS[] = { { "null", 'z', [](flags& f) NOEXCEPT_T { f.null_terminated = true; } }, { "debug", 'd', [](flags& f) NOEXCEPT_T { f.debug = true; } }, + { "ascii", 'a', [](flags& f) NOEXCEPT_T { f.assume_ascii = true; } }, { "expand", 'e', [](flags& f) NOEXCEPT_T { f.expand = true; } }, + { "compile", 'c', [](flags& f) NOEXCEPT_T { f.compile = true; } }, { "warnings", 'w', [](flags& f) NOEXCEPT_T { f.warnings = true; } }, { "pipekill", 'f', [](flags& f) NOEXCEPT_T { f.pipekill = true; } }, { "hide-nops", 'n', [](flags& f) NOEXCEPT_T { f.hide_nops = true; } }, diff --git a/src/input.hh b/src/input.hh index 44b94de..89be6f8 100644 --- a/src/input.hh +++ b/src/input.hh @@ -11,6 +11,8 @@ struct flags { bool hide_nops : 1; bool expand : 1; bool null_terminated : 1; + bool compile : 1; + bool assume_ascii : 1; constexpr flags() noexcept : debug(false), @@ -20,12 +22,16 @@ struct flags { disassemble(false), hide_nops(false), expand(false), - null_terminated(false) {} + null_terminated(false), + compile(false), + assume_ascii(false) {} constexpr bool is_valid() const noexcept { return !( (show_stack && !debug) || ((debug || warnings || pipekill) && disassemble) || (hide_nops && !disassemble) || (expand && (debug || warnings || pipekill || disassemble)) + || (compile && (debug || warnings || pipekill || disassemble || expand)) + || (assume_ascii && (expand || disassemble)) ); } }; diff --git a/src/instruction.cpp b/src/instruction.cpp index cd61b53..f79b3ab 100644 --- a/src/instruction.cpp +++ b/src/instruction.cpp @@ -1,16 +1,6 @@ #include "instruction.hh" -#include -#include -#include #include "output.hh" -using std::cerr; -using std::ostringstream; - -#define STRING_NAME(x) \ - case operation::x: \ - return #x - instruction::instruction(instruction_pointer ip, const program& program) noexcept : m_arg(), m_op(operation::NOP) { int24_t op = program.at(ip.coords.first, ip.coords.second); assert(!is_branch(op, ip.dir)); @@ -70,77 +60,3 @@ instruction::instruction(instruction_pointer ip, const program& program) noexcep m_op = static_cast(op); } } - -std::string instruction::to_str() const noexcept { - switch (m_op) { - STRING_NAME(NOP); - STRING_NAME(ADD); - STRING_NAME(SUB); - STRING_NAME(MUL); - STRING_NAME(DIV); - STRING_NAME(MOD); - STRING_NAME(POP); - STRING_NAME(EXT); - STRING_NAME(INC); - STRING_NAME(DEC); - STRING_NAME(AND); - STRING_NAME(IOR); - STRING_NAME(XOR); - STRING_NAME(NOT); - STRING_NAME(GTC); - STRING_NAME(PTC); - STRING_NAME(GTI); - STRING_NAME(PTI); - STRING_NAME(IDX); - STRING_NAME(DUP); - STRING_NAME(RND); - STRING_NAME(EXP); - STRING_NAME(SWP); - STRING_NAME(GTM); - STRING_NAME(GDT); - STRING_NAME(DP2); - STRING_NAME(TKL); - STRING_NAME(TJN); - case operation::PSI: { - int24_t value = m_arg.number; - ostringstream result; - result << "PSI #"; - print_unichar(value, result); - return result.str(); - } - case operation::PSC: { - int24_t value = m_arg.number; - ostringstream result; - result << "PSC '"; - print_unichar(value, result); - result << "' ; 0x"; - char buf[7]; - snprintf(buf, sizeof buf, "%" PRIx32, static_cast(value)); - result << buf; - return result.str(); - } - case operation::JMP: { - pair target = m_arg.next; - ostringstream result; - result << "JMP " << target.first << "." << target.second; - return result.str(); - } - case operation::BNG: { - pair target = m_arg.choice.second; - ostringstream result; - result << "BNG " << target.first << "." << target.second; - return result.str(); - } - case operation::TSP: { - pair target = m_arg.choice.second; - ostringstream result; - result << "TSP " << target.first << "." << target.second; - return result.str(); - } - default: - cerr << "Unrecognized opcode '"; - print_unichar(static_cast(m_op), cerr); - cerr << '\'' << std::endl; - exit(EXIT_FAILURE); - } -} diff --git a/src/instruction.hh b/src/instruction.hh index 517894e..cba4e87 100644 --- a/src/instruction.hh +++ b/src/instruction.hh @@ -25,6 +25,9 @@ constexpr bool is_branch(int24_t op, direction dir) noexcept { // A variant-like type. class instruction { + friend class disassembler; + friend class compiler; + using instruction_pointer = program_walker::instruction_pointer; template @@ -69,7 +72,6 @@ public: constexpr bool is_exit() const noexcept { return m_op == operation::EXT || m_op == operation::TKL; } constexpr bool is_join() const noexcept { return m_op == operation::TJN; } constexpr bool is_nop() const noexcept { return m_op == operation::NOP; } - std::string to_str() const noexcept; CONSTEXPR_UNION const pair* first_if_branch() const noexcept { switch (m_op) { @@ -80,9 +82,9 @@ public: return nullptr; } } -private: - CONSTEXPR_UNION instruction(operation op, argument arg) noexcept : m_arg(arg), m_op(op) {} - +protected: 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/instruction_scanner.cpp b/src/instruction_scanner.cpp new file mode 100644 index 0000000..5cc7ab3 --- /dev/null +++ b/src/instruction_scanner.cpp @@ -0,0 +1,146 @@ +#include "instruction_scanner.hh" +#include +#include + +using std::pair; +using std::vector; + +void instruction_scanner::build_state() { + // https://langdev.stackexchange.com/a/659/15 + + m_fragments = new vector*>{ nullptr }; + // A map of IP -> location in fragments + std::unordered_map> location_map; + + // A collection of fragments that have yet to be visited, corresponding to nulls in m_fragments. The first item of + // the pair is the index within m_fragments, and the second item is the IP where that fragment begins. + std::list> unvisited_fragments = { + { SIZE_C(0), { { SIZE_C(0), SIZE_C(0) }, direction::southwest } } + }; + + // !empty-back-pop reflects the while let loop in the Rust code in the answer linked above + while (!unvisited_fragments.empty()) { + pair p = unvisited_fragments.back(); + unvisited_fragments.pop_back(); + size_t& index = p.first; + instruction_pointer& ip = p.second; + + auto* fragment = new vector(); + + int24_t op = m_program->at(ip.coords.first, ip.coords.second); + while (!is_branch(op, ip.dir)) { + auto loc = location_map.find(ip); + if (loc == location_map.end()) { + // Special-case TJN + // Unlike all other instructions, if TJN at the same location is hit from different directions, it + // MUST be emitted into the assembly exactly once. + instruction i(ip, *m_program); + if (i.is_join()) { + // flip the north/south bit on the direction and check again + loc = location_map.find({ ip.coords, static_cast(static_cast(ip.dir) ^ 0b100) }); + if (loc != location_map.end()) { + location_map.insert({ ip, loc->second }); + + // jump to the other one + fragment->push_back(instruction::jump_to(loc->second)); + // Yeah, yeah, goto is bad practice. You look at this loop and tell me you'd rather do it + // another way. + goto continue_outer; + } + } + + location_map.insert({ ip, { index, fragment->size() } }); + fragment->push_back(i); + + if (i.is_exit()) { + // doesn't end in a jump nor a branch + goto continue_outer; + } + + 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_W: + case BNG_NE: + case BNG_NW: + case BNG_SE: + case BNG_SW: + case THR_E: + case THR_W: + program_walker::branch(ip.dir, op, []() NOEXCEPT_T -> bool { + unreachable("is_branch should've returned true"); + }); + break; + case SKP: + case PSI: + case PSC: + program_walker::advance(ip, m_program->side_length()); + break; + default: + break; + } + + program_walker::advance(ip, m_program->side_length()); + } else { + fragment->push_back(instruction::jump_to(loc->second)); + goto continue_outer; + } + + op = m_program->at(ip.coords.first, ip.coords.second); + } + + // Putting all the variables in this scope so that way continue_outer can't see them + { + // Determine whether to emit a jump or branch. If it's a branch, determine what the targets are. + auto loc = location_map.find(ip); + if (loc == location_map.end()) { + location_map.insert({ ip, { index, fragment->size() } }); + + // BNG requires that its first target go right and its second go left. TSP doesn't really care. + instruction_pointer first_ip = ip; + program_walker::branch(first_ip.dir, op, []() NOEXCEPT_T { return false; }); + program_walker::advance(first_ip, m_program->side_length()); + + instruction_pointer second_ip = ip; + program_walker::branch(second_ip.dir, op, []() NOEXCEPT_T { return true; }); + program_walker::advance(second_ip, m_program->side_length()); + + pair first_dest, second_dest; + + // Pushing the first one to the end we're about to read from, and the second one to the far end, + // minimizes the total number of jumps needed. The second target is going to be pointed to by the BNG or + // TSP instruction anyways, but the first target only needs to be explicitly named if it's not the + // next instruction. + loc = location_map.find(first_ip); + if (loc == location_map.end()) { + first_dest = { m_fragments->size(), SIZE_C(0) }; + unvisited_fragments.push_back({ m_fragments->size(), first_ip }); + m_fragments->push_back(nullptr); + } else { + first_dest = loc->second; + } + + loc = location_map.find(second_ip); + if (loc == location_map.end()) { + second_dest = { m_fragments->size(), SIZE_C(0) }; + unvisited_fragments.push_front({ m_fragments->size(), second_ip }); + m_fragments->push_back(nullptr); + } else { + second_dest = loc->second; + } + + fragment->push_back(instruction::branch_to({ first_dest, second_dest })); + } else { + fragment->push_back(instruction::jump_to(loc->second)); + } + } + + continue_outer: + m_fragments->at(index) = fragment; + } +} diff --git a/src/instruction_scanner.hh b/src/instruction_scanner.hh new file mode 100644 index 0000000..fb4b95f --- /dev/null +++ b/src/instruction_scanner.hh @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "input.hh" +#include "instruction.hh" + +class instruction_scanner : public program_walker { +public: + CONSTEXPR_VECTOR instruction_scanner(NONNULL_PTR(const program) p) noexcept : + program_walker(p), m_fragments(nullptr) {} + instruction_scanner(const instruction_scanner&) = delete; + + CONSTEXPR_VECTOR ~instruction_scanner() noexcept { + if (m_fragments == nullptr) { + return; + } + for (std::vector* frag : *m_fragments) { + delete frag; + } + delete m_fragments; + } + + virtual void write_state(std::ostream& os) = 0; +protected: + void build_state(); + + // The list of program "fragments". Each one ends with a branch, jump, thread-kill, or exit. + std::vector*>* m_fragments; +}; diff --git a/src/main.cpp b/src/main.cpp index 639a27d..0208186 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include +#include "compiler.hh" #include "disassembler.hh" #include "interpreter.hh" @@ -13,6 +14,9 @@ inline void execute(const std::string& prg, flags f) { d.write_state(std::cout); } else if (f.expand) { std::cout << p << std::flush; + } else if (f.compile) { + compiler c(&p, f); + c.write_state(std::cout); } else { interpreter i(&p, f); i.run(); diff --git a/src/thread.cpp b/src/thread.cpp index 44e77fe..782fb4a 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -313,13 +313,25 @@ void thread::tick() { } break; case GTC: - m_stack.push_back(get_unichar()); + 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)) { + 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; @@ -330,7 +342,15 @@ void thread::tick() { } if (should_print) { - print_unichar(m_stack.back()); + if (m_flags.assume_ascii) { + int24_t back = m_stack.back(); + if (m_flags.warnings && (back & 0x7f) != back) UNLIKELY { + cerr << "Warning: Printing non-ASCII value.\n"; + } + putchar(back); + } else { + print_unichar(m_stack.back()); + } #ifdef __EMSCRIPTEN__ if (m_flags.debug) { diff --git a/src/thread.hh b/src/thread.hh index c000509..65ff30c 100644 --- a/src/thread.hh +++ b/src/thread.hh @@ -47,6 +47,11 @@ protected: NO_UNIQUE_ADDRESS instruction_pointer 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: unsigned long m_number; }; diff --git a/tests/cli/ascii/fine.trg b/tests/cli/ascii/fine.trg new file mode 100644 index 0000000..db95ae8 --- /dev/null +++ b/tests/cli/ascii/fine.trg @@ -0,0 +1,3 @@ + " + e @ +o . . diff --git a/tests/cli/ascii/index.sh b/tests/cli/ascii/index.sh new file mode 100755 index 0000000..aea466b --- /dev/null +++ b/tests/cli/ascii/index.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -eu + +folder=$(dirname "$0") + +test -n "$($TRILANGLE -aw "${folder}/read.trg" 2>&1 <<<'é')" +test -n "$($TRILANGLE -aw "${folder}/write.trg" 2>&1 1<&-)" + +# This value will vary depending on the platform. Some will normalize it to a multi-byte substitution character. +# Similarly, using `-m` will cause an error on some platforms and report '1' on others. +bytecount=$($TRILANGLE -aw "${folder}/write.trg" 2>/dev/null | wc -c) +test 4 -ge "$bytecount" +test 1 -le "$bytecount" + +test -z "$($TRILANGLE -aw "${folder}/read.trg" 2>&1 <<<'e')" +test e = "$($TRILANGLE -aw "${folder}/fine.trg")" diff --git a/tests/cli/ascii/read.trg b/tests/cli/ascii/read.trg new file mode 100644 index 0000000..3b8efe6 --- /dev/null +++ b/tests/cli/ascii/read.trg @@ -0,0 +1,2 @@ + i +@ . diff --git a/tests/cli/ascii/write.trg b/tests/cli/ascii/write.trg new file mode 100644 index 0000000..8a1444e --- /dev/null +++ b/tests/cli/ascii/write.trg @@ -0,0 +1,3 @@ + " + é @ +o . . diff --git a/tests/cli/cat/index.sh b/tests/cli/cat/index.sh index 76d246c..c2c16d0 100755 --- a/tests/cli/cat/index.sh +++ b/tests/cli/cat/index.sh @@ -8,9 +8,8 @@ text=' __^ __( \.__) ) (@)<_____>__(_____)____/' -folder="$(dirname "$0")" - -output="$(trilangle "${folder}/cat.trg" <<<"$text")" +folder=$(dirname "$0") +output=$($TRILANGLE "${folder}/cat.trg" <<<"$text") # Okay so on Windows the newline characters are different. Isn't this fun? test "$text" = "${output//$'\r'/}" diff --git a/tests/cli/disassembler/index.sh b/tests/cli/disassembler/index.sh index 03a72ac..445971e 100755 --- a/tests/cli/disassembler/index.sh +++ b/tests/cli/disassembler/index.sh @@ -1,11 +1,8 @@ #!/bin/bash -set -e +set -euo pipefail -folder="$(dirname "$0")" +folder=$(dirname "$0") -with_nops="$(trilangle -D "${folder}/nopful.trg")" -no_nops="$(trilangle -Dn "${folder}/nopful.trg")" - -test 6 -eq "$(wc -l <<<"$with_nops")" -test 1 -eq "$(wc -l <<<"$no_nops")" +test 6 -eq "$($TRILANGLE -D "${folder}/nopful.trg" | wc -l)" +test 1 -eq "$($TRILANGLE -Dn "${folder}/nopful.trg" | wc -l)" diff --git a/tests/cli/error/index.sh b/tests/cli/error/index.sh index bffbd78..73e1780 100755 --- a/tests/cli/error/index.sh +++ b/tests/cli/error/index.sh @@ -1,8 +1,10 @@ #!/bin/bash -folder="$(dirname "$0")" +set -u -errors="$(trilangle "${folder}/error.trg" 2>&1 1<&-)" +folder=$(dirname "$0") + +errors=$($TRILANGLE "${folder}/error.trg" 2>&1 1<&-) result=$? set -e test 0 -ne $result diff --git a/tests/cli/exp/index.sh b/tests/cli/exp/index.sh index da55ab1..6ff4a17 100755 --- a/tests/cli/exp/index.sh +++ b/tests/cli/exp/index.sh @@ -1,11 +1,8 @@ #!/bin/bash -set -e +set -eu -folder="$(dirname "$0")" +folder=$(dirname "$0") -sixteen="$(trilangle "${folder}/sixteen.trg")" -big_negative="$(trilangle "${folder}/big_negative.trg")" - -test 16 = "$sixteen" -test "$big_negative" -eq $((-0x800000)) +test 16 = "$($TRILANGLE "${folder}/sixteen.trg")" +test "$($TRILANGLE "${folder}/big_negative.trg")" -eq $((-0x800000)) diff --git a/tests/cli/input/index.sh b/tests/cli/input/index.sh index cbe9098..5edb75c 100755 --- a/tests/cli/input/index.sh +++ b/tests/cli/input/index.sh @@ -1,17 +1,11 @@ #!/bin/bash -set -e +set -eu -folder="$(dirname "$0")" +folder=$(dirname "$0") -one="$(trilangle "${folder}/int.trg" <<<1)" -two="$(trilangle "${folder}/int.trg" <<<'some junk and then a 2')" -three="$(trilangle "${folder}/int.trg" <<<'3 with some trailing junk')" -sixteen="$(trilangle "${folder}/int.trg" <<<'0x10')" -minus_one="$(trilangle "${folder}/int.trg" <<<'')" - -test 1 = "$one" -test 2 = "$two" -test 3 = "$three" -test 16 = "$sixteen" -test -1 = "$minus_one" +test 1 = "$($TRILANGLE "${folder}/int.trg" <<<1)" +test 2 = "$($TRILANGLE "${folder}/int.trg" <<<'some junk and then a 2')" +test 3 = "$($TRILANGLE "${folder}/int.trg" <<<'3 with some trailing junk')" +test 16 = "$($TRILANGLE "${folder}/int.trg" <<<'0x10')" +test -1 = "$($TRILANGLE "${folder}/int.trg" <<<'')" diff --git a/tests/cli/null/index.sh b/tests/cli/null/index.sh index 5992231..4313ff3 100755 --- a/tests/cli/null/index.sh +++ b/tests/cli/null/index.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -eo pipefail +set -euo pipefail text=' /\_/\ ___ @@ -9,12 +9,12 @@ text=' (@)<_____>__(_____)____/' run_with_z () { - printf '%s\0%s' "$1" "$2" | trilangle -z + printf '%s\0%s' "$1" "$2" | $TRILANGLE -z } -one="$(run_with_z '?!@' 1)" -z="$(run_with_z 'io@' z)" -output="$(run_with_z '<>i,@##o' "$text")" +one=$(run_with_z '?!@' 1) +z=$(run_with_z 'io@' z) +output=$(run_with_z '<>i,@##o' "$text") test 1 = "$one" test z = "$z" diff --git a/tests/cli/pipekill/index.sh b/tests/cli/pipekill/index.sh index 3a6b0a6..1a8072e 100755 --- a/tests/cli/pipekill/index.sh +++ b/tests/cli/pipekill/index.sh @@ -1,8 +1,8 @@ #!/bin/bash -set -e +set -eu -folder="$(dirname "$0")" +folder=$(dirname "$0") # https://stackoverflow.com/a/35512328/6253337 # Because macOS doesn't have timeout for some reason @@ -15,4 +15,4 @@ fi # Testing this has been weird. On some systems this works fine without --pipekill; on others it doesn't. For the # sake of my sanity I'm not testing the behavior without the flag, so it may become a no-op in the future. -timeout 2s bash -c "trilangle --pipekill '${folder}/yes.trg' | head -n 10 >/dev/null" +timeout 2s bash -c "$TRILANGLE -af '${folder}/yes.trg' | head -n 10 >/dev/null" diff --git a/tests/cli/print1/index.sh b/tests/cli/print1/index.sh index 054401d..45b2997 100755 --- a/tests/cli/print1/index.sh +++ b/tests/cli/print1/index.sh @@ -1,11 +1,8 @@ #!/bin/bash -set -e +set -eu -folder="$(dirname "$0")" +folder=$(dirname "$0") -int="$(trilangle "${folder}/int.trg")" -char="$(trilangle "${folder}/char.trg")" - -test 1 = "$int" -test 1 = "$char" +test 1 = "$($TRILANGLE "${folder}/int.trg")" +test 1 = "$($TRILANGLE -a "${folder}/char.trg")" diff --git a/tests/cli/warnings/index.sh b/tests/cli/warnings/index.sh index b9a8f89..1122916 100755 --- a/tests/cli/warnings/index.sh +++ b/tests/cli/warnings/index.sh @@ -1,14 +1,14 @@ #!/bin/bash -folder="$(dirname "$0")" +set -u + +folder=$(dirname "$0") # Without the braces, the &> doesn't silence the floating-point exception -{ errors1="$(trilangle -w "${folder}/worse.trg" 2>&1 1<&-)"; } &>/dev/null +{ errors=$($TRILANGLE -w "${folder}/worse.trg" 2>&1 1<&-); } &>/dev/null result=$? set -e test 0 -ne $result -errors2="$(trilangle -w "${folder}/bad.trg" 2>&1)" - -test -n "$errors1" -test -n "$errors2" +test -n "$errors" +test -n "$($TRILANGLE -w "${folder}/bad.trg" 2>&1)" diff --git a/tests/compiler/cat b/tests/compiler/cat new file mode 120000 index 0000000..fc1b4c7 --- /dev/null +++ b/tests/compiler/cat @@ -0,0 +1 @@ +../cli/cat \ No newline at end of file diff --git a/tests/compiler/input b/tests/compiler/input new file mode 120000 index 0000000..33183a5 --- /dev/null +++ b/tests/compiler/input @@ -0,0 +1 @@ +../cli/input \ No newline at end of file diff --git a/tests/compiler/primes/index.sh b/tests/compiler/primes/index.sh new file mode 100755 index 0000000..527efe6 --- /dev/null +++ b/tests/compiler/primes/index.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -eu + +folder=$(dirname "$0") + +test -z "$($TRILANGLE "${folder}/primes.trg" <<<1)" +test 0 = "$($TRILANGLE "${folder}/primes.trg" <<<2)" +test -z "$($TRILANGLE "${folder}/primes.trg" <<<4)" diff --git a/tests/compiler/primes/primes.trg b/tests/compiler/primes/primes.trg new file mode 100644 index 0000000..706b759 --- /dev/null +++ b/tests/compiler/primes/primes.trg @@ -0,0 +1,8 @@ + < + ' ? + < # 2 + % . _ z + S < . > ( + > . , ) 2 - + / \ \ _ / ! @ +@ . . . . . . . diff --git a/tests/compiler/print1 b/tests/compiler/print1 new file mode 120000 index 0000000..e527b0a --- /dev/null +++ b/tests/compiler/print1 @@ -0,0 +1 @@ +../cli/print1 \ No newline at end of file diff --git a/tests/unit/disassemble.hh b/tests/unit/disassemble.hh index c27b4e3..b1a8b50 100644 --- a/tests/unit/disassemble.hh +++ b/tests/unit/disassemble.hh @@ -1,12 +1,12 @@ #pragma once -#include +#include #include "test-framework/test_framework.hh" namespace { -class test_disassemble : public disassembler { +class test_disassemble : public instruction_scanner { public: - inline test_disassemble(NONNULL_PTR(const program) p, flags f) : disassembler(p, f) {} + inline test_disassemble(NONNULL_PTR(const program) p) : instruction_scanner(p) {} inline NONNULL_PTR(const std::vector*>) get_fragments() noexcept { if (m_fragments == nullptr) { @@ -14,16 +14,15 @@ public: } return m_fragments; } + + inline void write_state(std::ostream& os) {} }; } // namespace testgroup (disassemble) { testcase (exit_immediately) { program p("@"); - flags f; - f.disassemble = true; - - test_disassemble td(&p, f); + test_disassemble td(&p); auto* fragments = td.get_fragments(); test_assert(fragments->size() == 1, "Program without branches or threads should have 1 fragment"); @@ -33,10 +32,7 @@ testgroup (disassemble) { } , testcase (infinite_loop) { program p("."); - flags f; - f.disassemble = true; - - test_disassemble td(&p, f); + test_disassemble td(&p); auto* fragments = td.get_fragments(); test_assert(fragments->size() == 1, "Program without branches or threads should have 1 fragment"); @@ -58,10 +54,8 @@ testgroup (disassemble) { . > . . @ . . )"); - flags f; - f.disassemble = true; - test_disassemble td(&p, f); + test_disassemble td(&p); auto* fragments = td.get_fragments(); test_assert(fragments->size() == 3, "Program with one branch should have 3 fragments"); diff --git a/trgc.sh b/trgc.sh new file mode 100755 index 0000000..a373faf --- /dev/null +++ b/trgc.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euo pipefail +./trilangle -c "$@" | "${CC:-gcc}" -o ./trg.out -xc - +./trg.out diff --git a/wasm/lint.sh b/wasm/lint.sh new file mode 100755 index 0000000..249d405 --- /dev/null +++ b/wasm/lint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Note: 'npx' should be omitted in this file + +shopt -s globstar +set -e + +engine-check +stylelint ./*.scss +coffeelint ./*.coffee + +cd .. +clang-format --Werror --dry-run src/*.cpp src/**/*.hh tests/unit/*.cpp tests/unit/*.hh diff --git a/wasm/package.json b/wasm/package.json index c3f49a1..288ddf8 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -14,7 +14,7 @@ }, "scripts": { "start": "node -e \"(e => e().use(e.static('.')).listen(3000))(require('express'))\"", - "lint": "engine-check && stylelint ./*.scss && coffeelint in.coffee worker.coffee library.coffee && clang-format --Werror --dry-run ../src/* ../tests/unit/*.cpp ../tests/unit/*.hh" + "lint": "./lint.sh" }, "engines": { "node": "^16.13.0 || >=18.0.0"