diff --git a/.gitignore b/.gitignore
index 12404dd..a66640b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,7 @@ compile_commands.json
CTestTestfile.cmake
_deps
+cmake-build-debug/
+cmake-build-release/
+cmake-build-minsizerel/
+cmake-build-relwithdebinfo/
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -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
diff --git a/.idea/UserScript.iml b/.idea/UserScript.iml
new file mode 100644
index 0000000..6d70257
--- /dev/null
+++ b/.idea/UserScript.iml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..f1c67df
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..33e7d3d
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..c8397c9
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..d410da5
--- /dev/null
+++ b/CMakeLists.txt
@@ -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 $ "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)
diff --git a/include/UserScript.h b/include/UserScript.h
new file mode 100644
index 0000000..b6bdb7d
--- /dev/null
+++ b/include/UserScript.h
@@ -0,0 +1,59 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+
+namespace scripting {
+ struct null {};
+ struct array;
+
+ using script_value = std::variant;
+ struct script_variable {
+ std::string name;
+ };
+
+ struct code_location {
+ std::shared_ptr line_contents;
+ int32_t line_number;
+ int32_t column_number;
+ };
+
+ struct script_error {
+ std::shared_ptr location;
+ std::string message;
+ };
+
+ struct array {
+ std::vector value;
+ operator std::vector&() {
+ return value;
+ }
+ };
+
+ using argument = std::variant;
+
+ class UserScript;
+
+ struct function_impl {
+ virtual std::optional apply(UserScript* self, std::vector, std::optional&) = 0;
+ virtual ~function_impl() = default;
+ };
+
+ using function = std::unique_ptr;
+
+ class UserScript {
+ public:
+ virtual std::optional> 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> executeAtOnce(std::string code) = 0;
+ virtual std::vector prepare(std::string code) = 0;
+ virtual std::optional stepOnce() = 0;
+ virtual ~UserScript() = default;
+ };
+
+ std::unique_ptr prepare_interpreter(const std::string& code);
+}
\ No newline at end of file
diff --git a/include/UserScript/parser.h b/include/UserScript/parser.h
new file mode 100644
index 0000000..127f6fe
--- /dev/null
+++ b/include/UserScript/parser.h
@@ -0,0 +1,169 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+
+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 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 location;
+ operator_t op;
+ std::unique_ptr content;
+ };
+
+ struct binary_algebraic_expression {
+ std::shared_ptr location;
+ std::unique_ptr lhs;
+ operator_t op;
+ std::unique_ptr rhs;
+ };
+
+ struct command_expression {
+ std::shared_ptr location;
+ identifier name;
+ std::vector> arguments;
+ };
+
+ struct variable_expression {
+ std::shared_ptr location;
+ identifier name;
+ };
+
+ struct paren_expression {
+ std::shared_ptr location;
+ std::variant<
+ std::unique_ptr,
+ std::unique_ptr
+ > content;
+ };
+
+ struct literal_int_expression {
+ std::shared_ptr location;
+ int32_t value;
+ };
+
+ struct literal_string_expression {
+ std::shared_ptr location;
+ std::string value;
+ };
+
+ struct expression {
+ std::shared_ptr location;
+ std::variant<
+ std::unique_ptr,
+ std::unique_ptr,
+ std::unique_ptr,
+ std::unique_ptr,
+ std::unique_ptr,
+ std::unique_ptr
+ > contents;
+ };
+
+ struct statement;
+
+ struct block {
+ std::shared_ptr location;
+ std::vector contents;
+ };
+
+ struct conditional {
+ std::shared_ptr location;
+ std::unique_ptr condition;
+ std::unique_ptr on_condition;
+ std::unique_ptr otherwise;
+ };
+
+ struct while_loop {
+ std::shared_ptr location;
+ std::unique_ptr condition;
+ std::unique_ptr on_condition;
+ };
+
+ struct statement {
+ std::shared_ptr location;
+ std::variant<
+ std::unique_ptr,
+ std::unique_ptr,
+ std::unique_ptr
+ > contents;
+ };
+
+ struct token {
+ std::shared_ptr location;
+ std::variant value;
+ };
+
+ std::vector lex(const std::string& code, std::vector& errors);
+ scripting::ast::block parse(std::span code, std::vector& errors);
+ }
+}
\ No newline at end of file
diff --git a/script_exe/main.cpp b/script_exe/main.cpp
new file mode 100644
index 0000000..06d750d
--- /dev/null
+++ b/script_exe/main.cpp
@@ -0,0 +1,347 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "UserScript.h"
+
+void print_value(std::ostream& stream, const scripting::script_value& res) {
+ if(std::holds_alternative(res)) {
+ stream << "[";
+ auto max = std::get(res).value.size();
+ auto no_comma = max - 1;
+ for(size_t idx = 0; idx < max; ++idx) {
+ print_value(stream, std::get(res).value[idx]);
+ stream << (idx != no_comma ? ", " : "");
+ }
+ stream << "]";
+ } else if(std::holds_alternative(res)) {
+ stream << std::get(res);
+ } else if(std::holds_alternative(res)) {
+ stream << "null";
+ } else {
+ stream << std::get(res);
+ }
+}
+
+struct identity : public scripting::function_impl {
+ std::optional apply(scripting::UserScript* self,std::vector args, std::optional& errors) final {
+ if(args.size() != 1) {
+ errors = scripting::script_error{.message = "identity expects a single argument"};
+ } else {
+ if(std::holds_alternative(args.front())) {
+ return std::get(args.front());
+ } else {
+ return self->resolve(std::get(args.front()).name);
+ }
+ }
+ return scripting::script_value({});
+ }
+};
+
+struct print : public scripting::function_impl {
+ std::ostream& stream;
+
+ print(std::ostream& _stream) : stream(_stream) {}
+
+ std::optional apply(scripting::UserScript* self,std::vector args, std::optional& errors) final {
+
+ while(not args.empty()) {
+ auto& arg = args.back();
+ if(std::holds_alternative(arg)) {
+ print_value(stream, std::get(arg));
+ } else {
+ print_value(stream, self->resolve(std::get(arg).name));
+ }
+ args.pop_back();
+ }
+ return scripting::script_value({});
+ }
+};
+
+struct set : public scripting::function_impl {
+ std::optional apply(scripting::UserScript* self,std::vector args, std::optional& 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(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(arg)) {
+ self->setValue(get(var).name, std::get(arg));
+ } else {
+ self->setValue(get(var).name, self->resolve(std::get(arg).name));
+ }
+ if(auto v = self->getValue(get(var).name); v) {
+ return v.value();
+ } else {
+ return scripting::script_value{};
+ }
+ }
+};
+
+struct terminate : public scripting::function_impl {
+ std::optional apply(scripting::UserScript*,std::vector, std::optional&) 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());
+ engine->registerFunction("exit", std::make_unique());
+ engine->registerFunction("set", std::make_unique());
+
+ /***
+ * 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(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(per_exec).count() << "ns\n";
+ std::cout << "time per step = " << std::chrono::duration_cast(per_step).count() << "ns\n";
+ std::cout << "time per avg op = " << std::chrono::duration_cast(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());
+ engine->registerFunction("exit", std::make_unique());
+ engine->registerFunction("set", std::make_unique());
+
+ /***
+ * Same as above but for compilation times
+ *
+ * 2023-07-04 Archivist -> 386µs
+ */
+ engine->registerFunction("print", std::make_unique(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(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());
+ engine->registerFunction("exit", std::make_unique());
+ engine->registerFunction("set", std::make_unique());
+
+
+ std::stringstream str;
+ std::string_view filename_source = target;
+ std::string_view filename_output = expect;
+ engine->registerFunction("print", std::make_unique(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(res)) {
+ } else {
+ auto &errors = std::get>(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(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());
+ engine->registerFunction("exit", std::make_unique());
+ engine->registerFunction("set", std::make_unique());
+
+ engine->registerFunction("print", std::make_unique(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(res)) {
+ } else {
+ auto &errors = std::get>(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 args) {
+ std::vector batch;
+
+ auto engine = scripting::prepare_interpreter(std::string{});
+
+ engine->registerFunction("identity", std::make_unique());
+ engine->registerFunction("terminate", std::make_unique());
+ engine->registerFunction("set", std::make_unique());
+
+ engine->registerFunction("print", std::make_unique(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(res)) {
+ } else {
+ auto &errors = std::get>(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 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 args;
+ for(auto& arg : std::span(argv, argv+argc)) {
+ args.emplace_back(arg, arg+strlen(arg));
+ }
+ return cpp_main(args);
+}
\ No newline at end of file
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
new file mode 100644
index 0000000..8e9834c
--- /dev/null
+++ b/src/interpreter.cpp
@@ -0,0 +1,1186 @@
+#include
+#include