| @ -0,0 +1,8 @@ | |||||
| # Default ignored files | |||||
| /shelf/ | |||||
| /workspace.xml | |||||
| # Editor-based HTTP Client requests | |||||
| /httpRequests/ | |||||
| # Datasource local storage ignored files | |||||
| /dataSources/ | |||||
| /dataSources.local.xml | |||||
| @ -0,0 +1,2 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <module classpath="CMake" type="CPP_MODULE" version="4" /> | |||||
| @ -0,0 +1,4 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" /> | |||||
| </project> | |||||
| @ -0,0 +1,8 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="ProjectModuleManager"> | |||||
| <modules> | |||||
| <module fileurl="file://$PROJECT_DIR$/.idea/UserScript.iml" filepath="$PROJECT_DIR$/.idea/UserScript.iml" /> | |||||
| </modules> | |||||
| </component> | |||||
| </project> | |||||
| @ -0,0 +1,6 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="VcsDirectoryMappings"> | |||||
| <mapping directory="" vcs="Git" /> | |||||
| </component> | |||||
| </project> | |||||
| @ -0,0 +1,48 @@ | |||||
| cmake_minimum_required(VERSION 3.24) | |||||
| project(UserScript) | |||||
| set(CMAKE_CXX_STANDARD 23) | |||||
| set(CMAKE_VERBOSE_MAKEFILE ON) | |||||
| set(FETCHCONTENT_QUIET OFF) | |||||
| set(CATCH_CONFIG_DISABLE_EXCEPTIONS ON) | |||||
| Include(FetchContent) | |||||
| FetchContent_Declare( | |||||
| Catch2 | |||||
| GIT_REPOSITORY https://github.com/catchorg/Catch2.git | |||||
| GIT_TAG v3.3.2 | |||||
| ) | |||||
| FetchContent_MakeAvailable(Catch2) | |||||
| enable_testing() | |||||
| include(CTest) | |||||
| include(Catch) | |||||
| add_library(UserScript STATIC | |||||
| src/interpreter.cpp | |||||
| src/lex_parse.cpp) | |||||
| add_executable(ushell script_exe/main.cpp) | |||||
| target_link_libraries(ushell PUBLIC UserScript) | |||||
| include_directories(include) | |||||
| add_executable(tests tests/lexer_test.cpp tests/parser_test.cpp) | |||||
| target_link_libraries(tests PUBLIC UserScript Catch2::Catch2WithMain) | |||||
| catch_discover_tests(tests) | |||||
| function(add_script_test [testname filename resultname]) | |||||
| message("Added test: ${ARGV0}") | |||||
| add_test( | |||||
| NAME "${ARGV0}" | |||||
| WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" | |||||
| COMMAND $<TARGET_FILE:ushell> "compare" "${ARGV1}" "${ARGV2}" | |||||
| ) | |||||
| endfunction() | |||||
| add_script_test("Scripting 001: Operators" tests/scripts/001.script tests/scripts/001.results) | |||||
| add_script_test("Scripting 002: Statements and Conditionals" tests/scripts/002.script tests/scripts/002.results) | |||||
| add_script_test("Scripting 003: While loops" tests/scripts/003.script tests/scripts/003.results) | |||||
| add_script_test("Scripting 004: While loops with bad terminator" tests/scripts/004.script tests/scripts/004.results) | |||||
| add_script_test("Scripting 005: If statements with bad terminator" tests/scripts/005.script tests/scripts/005.results) | |||||
| @ -0,0 +1,59 @@ | |||||
| #pragma once | |||||
| #include <memory> | |||||
| #include <string> | |||||
| #include <variant> | |||||
| #include <vector> | |||||
| #include <optional> | |||||
| namespace scripting { | |||||
| struct null {}; | |||||
| struct array; | |||||
| using script_value = std::variant<null, int32_t, std::string, array>; | |||||
| struct script_variable { | |||||
| std::string name; | |||||
| }; | |||||
| struct code_location { | |||||
| std::shared_ptr<const std::string> line_contents; | |||||
| int32_t line_number; | |||||
| int32_t column_number; | |||||
| }; | |||||
| struct script_error { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::string message; | |||||
| }; | |||||
| struct array { | |||||
| std::vector<script_value> value; | |||||
| operator std::vector<script_value>&() { | |||||
| return value; | |||||
| } | |||||
| }; | |||||
| using argument = std::variant<script_value, script_variable>; | |||||
| class UserScript; | |||||
| struct function_impl { | |||||
| virtual std::optional<script_value> apply(UserScript* self, std::vector<argument>, std::optional<script_error>&) = 0; | |||||
| virtual ~function_impl() = default; | |||||
| }; | |||||
| using function = std::unique_ptr<function_impl>; | |||||
| class UserScript { | |||||
| public: | |||||
| virtual std::optional<std::reference_wrapper<script_value>> getValue(const std::string& name) = 0; | |||||
| virtual bool setValue(const std::string& name, script_value value) = 0; | |||||
| virtual void registerFunction(std::string name, function fn) = 0; | |||||
| virtual script_value resolve(const std::string& name) = 0; | |||||
| virtual std::variant<script_value, std::vector<script_error>> executeAtOnce(std::string code) = 0; | |||||
| virtual std::vector<script_error> prepare(std::string code) = 0; | |||||
| virtual std::optional<script_error> stepOnce() = 0; | |||||
| virtual ~UserScript() = default; | |||||
| }; | |||||
| std::unique_ptr<UserScript> prepare_interpreter(const std::string& code); | |||||
| } | |||||
| @ -0,0 +1,169 @@ | |||||
| #pragma once | |||||
| #include <variant> | |||||
| #include <memory> | |||||
| #include <vector> | |||||
| #include <span> | |||||
| #include <UserScript.h> | |||||
| namespace scripting { | |||||
| namespace ast { | |||||
| enum class operator_t : uint8_t { | |||||
| logical_not = 0b00000, | |||||
| binary_not = 0b00001, | |||||
| divide = 0b00010, | |||||
| modulo = 0b00100, | |||||
| multiply = 0b00101, | |||||
| subtract = 0b00110, | |||||
| add = 0b01000, | |||||
| bitshift_left = 0b01001, | |||||
| bitshift_right = 0b01010, | |||||
| rotate_left = 0b01100, | |||||
| rotate_right = 0b01101, | |||||
| less_than = 0b01110, | |||||
| greater_than = 0b10000, | |||||
| less_or_equal_than = 0b10001, | |||||
| greater_or_equal_than = 0b10010, | |||||
| equals = 0b10100, | |||||
| different = 0b10101, | |||||
| binary_and = 0b10110, | |||||
| binary_or = 0b11000, | |||||
| binary_xor = 0b11001, | |||||
| logical_and = 0b11010, | |||||
| logical_or = 0b11100, | |||||
| }; | |||||
| enum class symbol_t { | |||||
| l_paren, r_paren, | |||||
| logical_not, | |||||
| binary_not, | |||||
| divide, | |||||
| modulo, | |||||
| multiply, | |||||
| subtract, | |||||
| add, | |||||
| bitshift_left, | |||||
| bitshift_right, | |||||
| rotate_left, | |||||
| rotate_right, | |||||
| less_than, | |||||
| greater_than, | |||||
| less_or_equal_than, | |||||
| greater_or_equal_than, | |||||
| equals, | |||||
| different, | |||||
| binary_and, | |||||
| binary_or, | |||||
| binary_xor, | |||||
| logical_and, | |||||
| logical_or, | |||||
| new_line | |||||
| }; | |||||
| struct identifier { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::string value; | |||||
| }; | |||||
| inline auto operator<=>(const identifier& lhs, const identifier& rhs) { | |||||
| // TODO: check if the stdlib evolves to support ALL THE HELLA <=> THAT SHOULD BE THERE | |||||
| return -1 * (lhs.value < rhs.value) + (lhs.value > rhs.value); | |||||
| } | |||||
| inline auto operator==(const identifier& lhs, const identifier& rhs) { | |||||
| return lhs.value == rhs.value; | |||||
| } | |||||
| struct expression; | |||||
| struct unary_algebraic_expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| operator_t op; | |||||
| std::unique_ptr<expression> content; | |||||
| }; | |||||
| struct binary_algebraic_expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::unique_ptr<expression> lhs; | |||||
| operator_t op; | |||||
| std::unique_ptr<expression> rhs; | |||||
| }; | |||||
| struct command_expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| identifier name; | |||||
| std::vector<std::unique_ptr<expression>> arguments; | |||||
| }; | |||||
| struct variable_expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| identifier name; | |||||
| }; | |||||
| struct paren_expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::variant< | |||||
| std::unique_ptr<expression>, | |||||
| std::unique_ptr<command_expression> | |||||
| > content; | |||||
| }; | |||||
| struct literal_int_expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| int32_t value; | |||||
| }; | |||||
| struct literal_string_expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::string value; | |||||
| }; | |||||
| struct expression { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::variant< | |||||
| std::unique_ptr<unary_algebraic_expression>, | |||||
| std::unique_ptr<binary_algebraic_expression>, | |||||
| std::unique_ptr<paren_expression>, | |||||
| std::unique_ptr<variable_expression>, | |||||
| std::unique_ptr<literal_int_expression>, | |||||
| std::unique_ptr<literal_string_expression> | |||||
| > contents; | |||||
| }; | |||||
| struct statement; | |||||
| struct block { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::vector<statement> contents; | |||||
| }; | |||||
| struct conditional { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::unique_ptr<expression> condition; | |||||
| std::unique_ptr<block> on_condition; | |||||
| std::unique_ptr<block> otherwise; | |||||
| }; | |||||
| struct while_loop { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::unique_ptr<expression> condition; | |||||
| std::unique_ptr<block> on_condition; | |||||
| }; | |||||
| struct statement { | |||||
| std::shared_ptr<const code_location> location; | |||||
| std::variant< | |||||
| std::unique_ptr<command_expression>, | |||||
| std::unique_ptr<conditional>, | |||||
| std::unique_ptr<while_loop> | |||||
| > contents; | |||||
| }; | |||||
| struct token { | |||||
| std::shared_ptr<const scripting::code_location> location; | |||||
| std::variant<scripting::ast::identifier, int32_t, std::string, symbol_t> value; | |||||
| }; | |||||
| std::vector<token> lex(const std::string& code, std::vector<scripting::script_error>& errors); | |||||
| scripting::ast::block parse(std::span<token> code, std::vector<scripting::script_error>& errors); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,347 @@ | |||||
| #include <iostream> | |||||
| #include <iomanip> | |||||
| #include <algorithm> | |||||
| #include <sstream> | |||||
| #include <cmath> | |||||
| #include <chrono> | |||||
| #include <fstream> | |||||
| #include <span> | |||||
| #include <cstring> | |||||
| #include "UserScript.h" | |||||
| void print_value(std::ostream& stream, const scripting::script_value& res) { | |||||
| if(std::holds_alternative<scripting::array>(res)) { | |||||
| stream << "["; | |||||
| auto max = std::get<scripting::array>(res).value.size(); | |||||
| auto no_comma = max - 1; | |||||
| for(size_t idx = 0; idx < max; ++idx) { | |||||
| print_value(stream, std::get<scripting::array>(res).value[idx]); | |||||
| stream << (idx != no_comma ? ", " : ""); | |||||
| } | |||||
| stream << "]"; | |||||
| } else if(std::holds_alternative<std::string>(res)) { | |||||
| stream << std::get<std::string>(res); | |||||
| } else if(std::holds_alternative<scripting::null>(res)) { | |||||
| stream << "null"; | |||||
| } else { | |||||
| stream << std::get<int32_t>(res); | |||||
| } | |||||
| } | |||||
| struct identity : public scripting::function_impl { | |||||
| std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final { | |||||
| if(args.size() != 1) { | |||||
| errors = scripting::script_error{.message = "identity expects a single argument"}; | |||||
| } else { | |||||
| if(std::holds_alternative<scripting::script_value>(args.front())) { | |||||
| return std::get<scripting::script_value>(args.front()); | |||||
| } else { | |||||
| return self->resolve(std::get<scripting::script_variable>(args.front()).name); | |||||
| } | |||||
| } | |||||
| return scripting::script_value({}); | |||||
| } | |||||
| }; | |||||
| struct print : public scripting::function_impl { | |||||
| std::ostream& stream; | |||||
| print(std::ostream& _stream) : stream(_stream) {} | |||||
| std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final { | |||||
| while(not args.empty()) { | |||||
| auto& arg = args.back(); | |||||
| if(std::holds_alternative<scripting::script_value>(arg)) { | |||||
| print_value(stream, std::get<scripting::script_value>(arg)); | |||||
| } else { | |||||
| print_value(stream, self->resolve(std::get<scripting::script_variable>(arg).name)); | |||||
| } | |||||
| args.pop_back(); | |||||
| } | |||||
| return scripting::script_value({}); | |||||
| } | |||||
| }; | |||||
| struct set : public scripting::function_impl { | |||||
| std::optional<scripting::script_value> apply(scripting::UserScript* self,std::vector<scripting::argument> args, std::optional<scripting::script_error>& errors) final { | |||||
| if(args.size() != 2) { | |||||
| errors = scripting::script_error{ | |||||
| .message = "set expects 2 arguments" | |||||
| }; | |||||
| return scripting::script_value{}; | |||||
| } | |||||
| auto& var = args.back(); | |||||
| if(not holds_alternative<scripting::script_variable>(var)) { | |||||
| errors = scripting::script_error{ | |||||
| .message = "set expects the first argument to be a target variable" | |||||
| }; | |||||
| return scripting::script_value{}; | |||||
| } | |||||
| auto& arg = args.front(); | |||||
| if(std::holds_alternative<scripting::script_value>(arg)) { | |||||
| self->setValue(get<scripting::script_variable>(var).name, std::get<scripting::script_value>(arg)); | |||||
| } else { | |||||
| self->setValue(get<scripting::script_variable>(var).name, self->resolve(std::get<scripting::script_variable>(arg).name)); | |||||
| } | |||||
| if(auto v = self->getValue(get<scripting::script_variable>(var).name); v) { | |||||
| return v.value(); | |||||
| } else { | |||||
| return scripting::script_value{}; | |||||
| } | |||||
| } | |||||
| }; | |||||
| struct terminate : public scripting::function_impl { | |||||
| std::optional<scripting::script_value> apply(scripting::UserScript*,std::vector<scripting::argument>, std::optional<scripting::script_error>&) final { | |||||
| std::exit(1); | |||||
| // PLEASE DO NOT ACTUALLY EXIT YOU FUCKING IDIOT | |||||
| return scripting::script_value({}); | |||||
| } | |||||
| }; | |||||
| void process_bench(std::string target = "./tests/scripts/testfile.test") { | |||||
| auto engine = scripting::prepare_interpreter(std::string{}); | |||||
| engine->registerFunction("identity", std::make_unique<identity>()); | |||||
| engine->registerFunction("exit", std::make_unique<terminate>()); | |||||
| engine->registerFunction("set", std::make_unique<set>()); | |||||
| /*** | |||||
| * This is a half assed benchmark, | |||||
| * Document results here to keep the thingy in check performance wise (release mode only) | |||||
| * | |||||
| * 2023-07-04 Archivist -> 2618ns - 308ns - 49ns (clang+libstdc++) | |||||
| * 2023-07-07 Archivist -> 2481ns - 291ns - 46ns (clang+libc++) | |||||
| * 2023-07-07 Archivist -> 106ns - 12ns - 2ns (clang+march=native+libc++) | |||||
| */ | |||||
| engine->registerFunction("print", std::make_unique<print>(std::cout)); | |||||
| std::ifstream src_str(target); | |||||
| std::stringstream code; | |||||
| code << src_str.rdbuf(); | |||||
| int steps = 0; | |||||
| decltype(std::chrono::high_resolution_clock::now()-std::chrono::high_resolution_clock::now()) per_exec{}, per_step{}, per_op{}; | |||||
| for(int runs = 0; runs < 20; runs++) { | |||||
| auto res = engine->prepare(code.str()); | |||||
| auto begin = std::chrono::high_resolution_clock::now(); | |||||
| while (not engine->getValue("exit_ctr").has_value()) { | |||||
| engine->stepOnce(); | |||||
| steps++; | |||||
| } | |||||
| auto end = std::chrono::high_resolution_clock::now(); | |||||
| per_exec += (end - begin) / 5000; | |||||
| per_step += (end - begin) / steps; | |||||
| per_op += (end - begin) / (5000 * 53); | |||||
| } | |||||
| per_exec /= 20; | |||||
| per_step /= 20; | |||||
| per_op /= 20; | |||||
| std::cout << "time per exec = " << std::chrono::duration_cast<std::chrono::nanoseconds>(per_exec).count() << "ns\n"; | |||||
| std::cout << "time per step = " << std::chrono::duration_cast<std::chrono::nanoseconds>(per_step).count() << "ns\n"; | |||||
| std::cout << "time per avg op = " << std::chrono::duration_cast<std::chrono::nanoseconds>(per_op).count() << "ns\n"; | |||||
| } | |||||
| void compile_bench(std::string target = "./tests/scripts/testfile.test") { | |||||
| auto engine = scripting::prepare_interpreter(std::string{}); | |||||
| engine->registerFunction("identity", std::make_unique<identity>()); | |||||
| engine->registerFunction("exit", std::make_unique<terminate>()); | |||||
| engine->registerFunction("set", std::make_unique<set>()); | |||||
| /*** | |||||
| * Same as above but for compilation times | |||||
| * | |||||
| * 2023-07-04 Archivist -> 386µs | |||||
| */ | |||||
| engine->registerFunction("print", std::make_unique<print>(std::cout)); | |||||
| std::ifstream src_str("./tests/scripts/testfile.test"); | |||||
| std::stringstream code; | |||||
| code << src_str.rdbuf(); | |||||
| auto begin = std::chrono::high_resolution_clock::now(); | |||||
| [&]() __attribute__((optimize("O0"))) { | |||||
| auto res = engine->prepare(code.str()); | |||||
| res = engine->prepare(code.str()); | |||||
| res = engine->prepare(code.str()); | |||||
| res = engine->prepare(code.str()); | |||||
| res = engine->prepare(code.str()); | |||||
| }(); | |||||
| auto end = std::chrono::high_resolution_clock::now(); | |||||
| auto per_exec = (end - begin)/5; | |||||
| std::cout << "time per exec = " << std::chrono::duration_cast<std::chrono::microseconds>(per_exec).count() << "µs\n"; | |||||
| } | |||||
| void compare(std::string target, std::string expect) { | |||||
| auto engine = scripting::prepare_interpreter(std::string{}); | |||||
| engine->registerFunction("identity", std::make_unique<identity>()); | |||||
| engine->registerFunction("exit", std::make_unique<terminate>()); | |||||
| engine->registerFunction("set", std::make_unique<set>()); | |||||
| std::stringstream str; | |||||
| std::string_view filename_source = target; | |||||
| std::string_view filename_output = expect; | |||||
| engine->registerFunction("print", std::make_unique<print>(str)); | |||||
| std::ifstream src_str(std::string{filename_source}); | |||||
| std::stringstream code; | |||||
| code << src_str.rdbuf(); | |||||
| std::ifstream out_str(std::string{filename_output}); | |||||
| std::stringstream output; | |||||
| output << out_str.rdbuf(); | |||||
| auto res = engine->executeAtOnce(code.str()); | |||||
| if (std::holds_alternative<scripting::script_value>(res)) { | |||||
| } else { | |||||
| auto &errors = std::get<std::vector<scripting::script_error>>(res); | |||||
| for (auto &line: errors) { | |||||
| str << line.message << "\n at line " << line.location->line_number << ":" | |||||
| << line.location->column_number << "\n"; | |||||
| str << " " << *line.location->line_contents << "\n"; | |||||
| str << " " << std::string(line.location->column_number - 1, ' ') << "^\n"; | |||||
| } | |||||
| } | |||||
| int status = 0; | |||||
| while(not output.eof()) { | |||||
| std::string expected, found; | |||||
| std::getline(output, expected); | |||||
| std::getline(str, found); | |||||
| bool ok = (expected != found); | |||||
| status+= ok ; | |||||
| (ok ? std::cerr : std::cout) | |||||
| << (not ok ? "\033[21;32m" : "\033[1;31m") << expected | |||||
| << std::string(std::max<size_t>(0, 40 - expected.size()), ' ')<< "| " << found << std::endl; | |||||
| } | |||||
| if(status) std::exit(status); | |||||
| } | |||||
| void immediate_interactive() { | |||||
| auto engine = scripting::prepare_interpreter(std::string{}); | |||||
| engine->registerFunction("identity", std::make_unique<identity>()); | |||||
| engine->registerFunction("exit", std::make_unique<terminate>()); | |||||
| engine->registerFunction("set", std::make_unique<set>()); | |||||
| engine->registerFunction("print", std::make_unique<print>(std::cout)); | |||||
| bool exit = false; | |||||
| while (not exit) { | |||||
| std::string code; | |||||
| std::getline(std::cin, code); | |||||
| auto res = engine->executeAtOnce(code); | |||||
| if (std::holds_alternative<scripting::script_value>(res)) { | |||||
| } else { | |||||
| auto &errors = std::get<std::vector<scripting::script_error>>(res); | |||||
| for (auto &line: errors) { | |||||
| std::cout << line.message << "\n at line "; | |||||
| if(line.location) { | |||||
| std::cout << line.location->line_number << ":" | |||||
| << line.location->column_number << "\n"; | |||||
| std::cout << " " << *line.location->line_contents << "\n"; | |||||
| std::cout << " " << std::string(line.location->column_number - 1, ' ') << "^\n"; | |||||
| } else std::cout << "UNKNOWN\n"; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| void exec(std::span<std::string_view> args) { | |||||
| std::vector<decltype(scripting::prepare_interpreter(std::string{}))> batch; | |||||
| auto engine = scripting::prepare_interpreter(std::string{}); | |||||
| engine->registerFunction("identity", std::make_unique<identity>()); | |||||
| engine->registerFunction("terminate", std::make_unique<terminate>()); | |||||
| engine->registerFunction("set", std::make_unique<set>()); | |||||
| engine->registerFunction("print", std::make_unique<print>(std::cout)); | |||||
| bool exit = false; | |||||
| while (not exit) { | |||||
| std::string code; | |||||
| std::getline(std::cin, code); | |||||
| auto res = engine->executeAtOnce(code); | |||||
| if (std::holds_alternative<scripting::script_value>(res)) { | |||||
| } else { | |||||
| auto &errors = std::get<std::vector<scripting::script_error>>(res); | |||||
| for (auto &line: errors) { | |||||
| std::cout << line.message << "\n at line "; | |||||
| if(line.location) { | |||||
| std::cout << line.location->line_number << ":" | |||||
| << line.location->column_number << "\n"; | |||||
| std::cout << " " << *line.location->line_contents << "\n"; | |||||
| std::cout << " " << std::string(line.location->column_number - 1, ' ') << "^\n"; | |||||
| } else std::cout << "UNKNOWN\n"; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| #if defined(__linux__) or defined(WIN32) | |||||
| constexpr bool trim_first_argument = true; | |||||
| #else | |||||
| constexpr bool trim_first_argument = false; | |||||
| static_assert(false, "Undefined status of the first argument"); | |||||
| #endif | |||||
| int cpp_main(std::span<std::string_view> args) { | |||||
| if constexpr (trim_first_argument) { | |||||
| args = args.subspan(1); | |||||
| } | |||||
| if(args.empty() || args.front() == "immediate") { | |||||
| immediate_interactive(); | |||||
| std::exit(0); | |||||
| } else if(args.front() == "compare") { | |||||
| args = args.subspan(1); | |||||
| if(args.size() != 2) { | |||||
| std::cerr << "compare expects 2 files as arguments" << std::endl; | |||||
| std::terminate(); | |||||
| } | |||||
| } else if(args.front() == "bench_exec") { | |||||
| args = args.subspan(1); | |||||
| if(args.size() > 1) { | |||||
| std::cerr << "bench_exec expects 0 or 1 file as arguments" << std::endl; | |||||
| std::terminate(); | |||||
| } | |||||
| if(args.empty()) process_bench(); | |||||
| else process_bench(std::string{args.front()}); | |||||
| } else if(args.front() == "bench_compile") { | |||||
| args = args.subspan(1); | |||||
| if(args.size() > 1) { | |||||
| std::cerr << "bench_exec expects 0 or 1 file as arguments" << std::endl; | |||||
| std::terminate(); | |||||
| } | |||||
| if(args.empty()) compile_bench(); | |||||
| else compile_bench(std::string{args.front()}); | |||||
| } else if(args.front() == "exec") { | |||||
| // exec(args.subspan(1)); | |||||
| } else { | |||||
| std::cerr << "Unknown option" << std::endl; | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| int main(int argc, char** argv) { | |||||
| std::vector<std::string_view> args; | |||||
| for(auto& arg : std::span(argv, argv+argc)) { | |||||
| args.emplace_back(arg, arg+strlen(arg)); | |||||
| } | |||||
| return cpp_main(args); | |||||
| } | |||||
| @ -0,0 +1,118 @@ | |||||
| #include <catch2/catch_test_macros.hpp> | |||||
| #include "UserScript/parser.h" | |||||
| using token = scripting::ast::token; | |||||
| using symbol_t = scripting::ast::symbol_t; | |||||
| using identifier = scripting::ast::identifier; | |||||
| TEST_CASE("Lexer Test 01") { | |||||
| std::string code = "/salad 12 13 \"hello\" ident\n"; | |||||
| std::vector<token> expected = { | |||||
| token{.value = symbol_t::divide}, | |||||
| token{.value = identifier{.value = "salad"}}, | |||||
| token{.value = 12}, | |||||
| token{.value = 13}, | |||||
| token{.value = "hello"}, | |||||
| token{.value = identifier{.value = "ident"}}, | |||||
| token{.value = symbol_t::new_line} | |||||
| }; | |||||
| std::vector<scripting::script_error> errors; | |||||
| auto lexed = scripting::ast::lex(code, errors); | |||||
| REQUIRE(errors.empty()); | |||||
| REQUIRE(lexed.size() == expected.size()); | |||||
| for(size_t idx = 0; idx < lexed.size(); ++idx) { | |||||
| REQUIRE(lexed[idx].value.index() == expected[idx].value.index()); | |||||
| REQUIRE(lexed[idx].value == expected[idx].value); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Lexer Test 01 (Doubled)") { | |||||
| std::string code = "/salad 12 13 \"hello\" ident\n/salad 12 13 \"hello\" ident\n"; | |||||
| std::vector<token> expected = { | |||||
| token{.value = symbol_t::divide}, | |||||
| token{.value = identifier{.value = "salad"}}, | |||||
| token{.value = 12}, | |||||
| token{.value = 13}, | |||||
| token{.value = "hello"}, | |||||
| token{.value = identifier{.value = "ident"}}, | |||||
| token{.value = symbol_t::new_line}, | |||||
| token{.value = symbol_t::divide}, | |||||
| token{.value = identifier{.value = "salad"}}, | |||||
| token{.value = 12}, | |||||
| token{.value = 13}, | |||||
| token{.value = "hello"}, | |||||
| token{.value = identifier{.value = "ident"}}, | |||||
| token{.value = symbol_t::new_line} | |||||
| }; | |||||
| std::vector<scripting::script_error> errors; | |||||
| auto lexed = scripting::ast::lex(code, errors); | |||||
| REQUIRE(errors.empty()); | |||||
| REQUIRE(lexed.size() == expected.size()); | |||||
| for(size_t idx = 0; idx < lexed.size(); ++idx) { | |||||
| REQUIRE(lexed[idx].value.index() == expected[idx].value.index()); | |||||
| REQUIRE(lexed[idx].value == expected[idx].value); | |||||
| } | |||||
| } | |||||
| TEST_CASE("Lexer Test 02") { | |||||
| std::string code = "/salad 12 13 \"hello\" ident\n" | |||||
| "/salad 12 13 \"hello\" ident\n" | |||||
| "if(/test)\n" | |||||
| " /nice\n" | |||||
| "endif"; | |||||
| auto line1 = std::make_shared<const std::string>("/salad 12 13 \"hello\" ident"); | |||||
| auto line2 = line1; | |||||
| auto line3 = std::make_shared<const std::string>("if(/test)"); | |||||
| auto line4 = std::make_shared<const std::string>(" /nice"); | |||||
| auto line5 = std::make_shared<const std::string>("endif"); | |||||
| using cl = scripting::code_location; | |||||
| std::vector<token> expected = { | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line1, .line_number = 1, .column_number = 1}), .value = symbol_t::divide}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line1, .line_number = 1, .column_number = 2}), .value = identifier{.value = "salad"}}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line1, .line_number = 1, .column_number = 8}), .value = 12}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line1, .line_number = 1, .column_number = 11}), .value = 13}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line1, .line_number = 1, .column_number = 14}), .value = "hello"}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line1, .line_number = 1, .column_number = 22}), .value = identifier{.value = "ident"}}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line1, .line_number = 1, .column_number = 27}), .value = symbol_t::new_line}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line2, .line_number = 2, .column_number = 1}), .value = symbol_t::divide}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line2, .line_number = 2, .column_number = 2}), .value = identifier{.value = "salad"}}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line2, .line_number = 2, .column_number = 8}), .value = 12}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line2, .line_number = 2, .column_number = 11}), .value = 13}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line2, .line_number = 2, .column_number = 14}), .value = "hello"}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line2, .line_number = 2, .column_number = 22}), .value = identifier{.value = "ident"}}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line2, .line_number = 2, .column_number = 27}), .value = symbol_t::new_line}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line3, .line_number = 3, .column_number = 1}), .value = identifier{.value = "if"}}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line3, .line_number = 3, .column_number = 3}), .value = symbol_t::l_paren}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line3, .line_number = 3, .column_number = 4}), .value = symbol_t::divide}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line3, .line_number = 3, .column_number = 5}), .value = identifier{.value = "test"}}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line3, .line_number = 3, .column_number = 9}), .value = symbol_t::r_paren}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line3, .line_number = 3, .column_number = 10}), .value = symbol_t::new_line}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line4, .line_number = 4, .column_number = 5}), .value = symbol_t::divide}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line4, .line_number = 4, .column_number = 6}), .value = identifier{.value = "nice"}}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line4, .line_number = 4, .column_number = 10}), .value = symbol_t::new_line}, | |||||
| token{.location = std::make_shared<cl>(cl{.line_contents = line5, .line_number = 5, .column_number = 1}), .value = identifier{.value = "endif"}}, | |||||
| }; | |||||
| std::vector<scripting::script_error> errors; | |||||
| auto lexed = scripting::ast::lex(code, errors); | |||||
| REQUIRE(errors.empty()); | |||||
| REQUIRE(lexed.size() == expected.size()); | |||||
| for(size_t idx = 0; idx < lexed.size(); ++idx) { | |||||
| REQUIRE(lexed[idx].value.index() == expected[idx].value.index()); | |||||
| REQUIRE(lexed[idx].value == expected[idx].value); | |||||
| REQUIRE(lexed[idx].location); | |||||
| if(expected[idx].location) { | |||||
| REQUIRE(expected[idx].location->column_number == lexed[idx].location->column_number); | |||||
| REQUIRE(expected[idx].location->line_number == lexed[idx].location->line_number); | |||||
| REQUIRE((bool)lexed[idx].location->line_contents); | |||||
| REQUIRE(*(expected[idx].location->line_contents) == *(lexed[idx].location->line_contents)); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,168 @@ | |||||
| #include "UserScript/parser.h" | |||||
| #include <catch2/catch_test_macros.hpp> | |||||
| #include <ranges> | |||||
| #include <iostream> | |||||
| #include <fstream> | |||||
| #include <sstream> | |||||
| #include <random> | |||||
| #include <algorithm> | |||||
| TEST_CASE("Can parse") { | |||||
| std::string code = "/salad (/potato) 12 13 \"hello\" ident\n" | |||||
| "/salad 12 13 \"hello\" ident\n" | |||||
| "if(/test)\n" | |||||
| " /nice\n" | |||||
| "endif"; | |||||
| std::vector<scripting::script_error> errors; | |||||
| auto lexed = scripting::ast::lex(code, errors); | |||||
| auto parsed = scripting::ast::parse(lexed, errors); | |||||
| if(not errors.empty()) { | |||||
| for(auto& line : errors) { | |||||
| std::cout << line.message << "\n at line " << line.location->line_number << ":" << line.location->column_number << "\n"; | |||||
| std::cout << " " << *line.location->line_contents << "\n"; | |||||
| std::cout << " " << std::string(line.location->column_number - 1, ' ') << "^\n"; | |||||
| } | |||||
| } | |||||
| auto& block = parsed; | |||||
| REQUIRE(block.contents.size() == 3); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::command_expression>>(block.contents.front().contents)); | |||||
| auto& cmd1 = std::get<std::unique_ptr<scripting::ast::command_expression>>(block.contents.front().contents); | |||||
| REQUIRE(cmd1->name.value == "salad"); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::command_expression>>(std::span(block.contents).subspan(1).front().contents)); | |||||
| auto& cmd2 = std::get<std::unique_ptr<scripting::ast::command_expression>>(std::span(block.contents).subspan(1).front().contents); | |||||
| REQUIRE(cmd2->name.value == "salad"); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::conditional>>(std::span(block.contents).subspan(2).front().contents)); | |||||
| auto& conditional = std::get<std::unique_ptr<scripting::ast::conditional>>(std::span(block.contents).subspan(2).front().contents); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::paren_expression>>(conditional->condition->contents)); | |||||
| auto& paren = std::get<std::unique_ptr<scripting::ast::paren_expression>>(conditional->condition->contents)->content; | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::command_expression>>(paren)); | |||||
| auto& condition = std::get<std::unique_ptr<scripting::ast::command_expression>>(paren); | |||||
| REQUIRE(condition->name.value == "test"); | |||||
| } | |||||
| TEST_CASE("Can parse 2") { | |||||
| std::string code = "/salad (/potato) 12+13*16/(/potato)+myvar \"hello\" ident\n" | |||||
| "/salad 12 13 \"hello\" ident\n" | |||||
| "if !(/test)\n" | |||||
| " /nice\n" | |||||
| "endif"; | |||||
| std::vector<scripting::script_error> errors; | |||||
| auto lexed = scripting::ast::lex(code, errors); | |||||
| auto parsed = scripting::ast::parse(lexed, errors); | |||||
| if(not errors.empty()) { | |||||
| for(auto& line : errors) { | |||||
| std::cout << line.message << "\n at line " << line.location->line_number << ":" << line.location->column_number << "\n"; | |||||
| std::cout << " " << *line.location->line_contents << "\n"; | |||||
| std::cout << " " << std::string(line.location->column_number - 1, ' ') << "^\n"; | |||||
| } | |||||
| } | |||||
| auto& block = parsed; | |||||
| REQUIRE(block.contents.size() == 3); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::command_expression>>(block.contents.front().contents)); | |||||
| auto& cmd1 = std::get<std::unique_ptr<scripting::ast::command_expression>>(block.contents.front().contents); | |||||
| REQUIRE(cmd1->name.value == "salad"); | |||||
| REQUIRE(cmd1->arguments.size() == 4); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::command_expression>>(std::span(block.contents).subspan(1).front().contents)); | |||||
| auto& cmd2 = std::get<std::unique_ptr<scripting::ast::command_expression>>(std::span(block.contents).subspan(1).front().contents); | |||||
| REQUIRE(cmd2->name.value == "salad"); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::conditional>>(std::span(block.contents).subspan(2).front().contents)); | |||||
| auto& conditional = std::get<std::unique_ptr<scripting::ast::conditional>>(std::span(block.contents).subspan(2).front().contents); | |||||
| REQUIRE(std::holds_alternative<std::unique_ptr<scripting::ast::unary_algebraic_expression>>(conditional->condition->contents)); | |||||
| } | |||||
| template<auto seed_template = -1> | |||||
| constexpr auto runner = [](){ | |||||
| std::vector<std::string> sources = { | |||||
| "../tests/scripts/001.script", | |||||
| "../tests/scripts/002.script", | |||||
| "../tests/scripts/003.script", | |||||
| "../tests/scripts/004.script", | |||||
| "../tests/scripts/005.script", | |||||
| }; | |||||
| auto seed = seed_template == -1 ? std::random_device{}() : seed_template; | |||||
| std::cout << "TEST \"Try to crash the parser 1\" with seed " << seed << std::endl; | |||||
| std::mt19937_64 rand(seed); | |||||
| auto mod = [&](std::string tmp) -> std::string { | |||||
| if(tmp.empty()) return tmp; | |||||
| auto alter_idx = rand()%tmp.size(); | |||||
| switch(rand()%3) { | |||||
| case 0:{ | |||||
| tmp.erase(alter_idx); | |||||
| }break; | |||||
| case 1:{ | |||||
| tmp[alter_idx] = rand() % 256; | |||||
| }break; | |||||
| case 2:{ | |||||
| tmp.insert(alter_idx, 1, char(rand() % 256)); | |||||
| }break; | |||||
| } | |||||
| return tmp; | |||||
| }; | |||||
| auto codes = sources | std::ranges::views::transform([](std::string file){ | |||||
| std::ifstream file_str{file}; | |||||
| std::stringstream read; | |||||
| read << file_str.rdbuf(); | |||||
| return read.str(); | |||||
| }); | |||||
| std::vector<std::string> vec; | |||||
| std::copy(codes.begin(), codes.end(), std::back_inserter(vec)); | |||||
| size_t count = 0; | |||||
| size_t error_cnt = 0; | |||||
| size_t success_cnt = 0; | |||||
| constexpr size_t max_count = 5000000; | |||||
| auto begin = std::chrono::high_resolution_clock::now(); | |||||
| while(count < max_count) { | |||||
| std::cout << 100.0*double(count)/max_count <<"%"<< std::endl; | |||||
| for(auto& code : vec) { | |||||
| std::vector<scripting::script_error> errors; | |||||
| auto lexed = scripting::ast::lex(code, errors); | |||||
| auto parsed = scripting::ast::parse(lexed, errors); | |||||
| if(errors.empty()) success_cnt++; | |||||
| else error_cnt++; | |||||
| count++; | |||||
| } | |||||
| auto limit = std::min<size_t>(vec.size(), 5000) ; | |||||
| for(size_t idx = 0; idx < limit; ++idx) { | |||||
| vec.push_back(mod(vec[idx])); | |||||
| } | |||||
| std::transform(vec.begin(), vec.end(), vec.begin(), mod); | |||||
| std::shuffle(vec.begin(), vec.end(), rand); | |||||
| if(vec.size()>30000) vec.resize(30000); | |||||
| } | |||||
| auto end = std::chrono::high_resolution_clock::now(); | |||||
| std::cout | |||||
| << "Successes: " << success_cnt << "\n" | |||||
| << "Failures: " << error_cnt << "\n" | |||||
| << "Ratio: " << double(success_cnt)/double(success_cnt+error_cnt) << "\n" | |||||
| << "Total time: " << std::chrono::duration_cast<std::chrono::microseconds>(end-begin).count() << "µs\n" | |||||
| << "Time per iteration: " << (std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin)/(error_cnt+success_cnt)).count() << "ns\n"; | |||||
| }; | |||||
| TEST_CASE("Try to crash the parser (known seeds)") { | |||||
| runner<1547293717>(); | |||||
| runner<1759257947>(); | |||||
| runner<2909912711>(); | |||||
| runner<1236548620>(); | |||||
| } | |||||
| TEST_CASE("Try to crash the parser (new seeds)") { | |||||
| runner<>(); | |||||
| } | |||||
| @ -0,0 +1,13 @@ | |||||
| 8 | |||||
| 12 | |||||
| 12 | |||||
| 14 | |||||
| 14 | |||||
| 10 | |||||
| 2 | |||||
| 2 | |||||
| 0 | |||||
| 7 | |||||
| 0 | |||||
| 3 | |||||
| true | |||||
| @ -0,0 +1,18 @@ | |||||
| /print 2+6 "\n" | |||||
| /print 2*6 "\n" | |||||
| /print 6*2 "\n" | |||||
| /print 2+6*2 "\n" | |||||
| /print 6*2+2 "\n" | |||||
| /print 6*2-2 "\n" | |||||
| /print 6-2-2 "\n" | |||||
| /print 6-2*2 "\n" | |||||
| /print 6&1 "\n" | |||||
| /print 6|1 "\n" | |||||
| /print 21%7 "\n" | |||||
| /print 21/7 "\n" | |||||
| if(2*3 == 6) | |||||
| /print "true" | |||||
| else | |||||
| /print "false" | |||||
| endif | |||||
| /print "\n" | |||||
| @ -0,0 +1 @@ | |||||
| 1 | |||||
| @ -0,0 +1,17 @@ | |||||
| if(counter == (/null)) | |||||
| /set counter 0 | |||||
| endif | |||||
| /bigDoNothing 17 12 36*78 | |||||
| if(counter % 2 == 1) | |||||
| /bigDoNothing 17 12 36*78 | |||||
| /set counter counter+1 | |||||
| else | |||||
| /bigDoNothing 17 12 36*78 | |||||
| /set counter counter+1 | |||||
| endif | |||||
| if(counter == 5000) | |||||
| /set exit_ctr 1 | |||||
| endif | |||||
| /print counter "\n" | |||||
| @ -0,0 +1,10 @@ | |||||
| 0 | |||||
| 1 | |||||
| 2 | |||||
| 3 | |||||
| 4 | |||||
| 5 | |||||
| 6 | |||||
| 7 | |||||
| 8 | |||||
| 9 | |||||
| @ -0,0 +1,5 @@ | |||||
| /set counter 0 | |||||
| while(counter < 10) | |||||
| /print counter "\n" | |||||
| /set counter (counter+1) | |||||
| endwhile | |||||
| @ -0,0 +1,4 @@ | |||||
| Unexpected statement in block | |||||
| at line 5:1 | |||||
| endif | |||||
| ^ | |||||
| @ -0,0 +1,5 @@ | |||||
| /set counter 0 | |||||
| while(counter < 10) | |||||
| /print counter "\n" | |||||
| /set counter (counter+1) | |||||
| endif | |||||
| @ -0,0 +1,4 @@ | |||||
| Unexpected expression content | |||||
| at line 5:1 | |||||
| endwhile | |||||
| ^ | |||||
| @ -0,0 +1,5 @@ | |||||
| /set counter 0 | |||||
| if(counter < 10) | |||||
| /print counter "\n" | |||||
| /set counter (counter+1) | |||||
| endwhile | |||||
| @ -0,0 +1,16 @@ | |||||
| if(counter == (/null)) | |||||
| /set counter 0 | |||||
| /print "Init...\n" | |||||
| endif | |||||
| /bigDoNothing 17 12 36*78 | |||||
| if(counter % 2 == 1) | |||||
| /bigDoNothing 17 12 36*78 | |||||
| /set counter counter+1 | |||||
| else | |||||
| /bigDoNothing 17 12 36*78 | |||||
| /set counter counter+1 | |||||
| endif | |||||
| if(counter == 5000) | |||||
| /set exit_ctr 1 | |||||
| endif | |||||