Browse Source

Initial commit after splitting

crazy_things
Ludovic 'Archivist' Lagouardette 10 months ago
parent
commit
ac7cd9cc0d
25 changed files with 3276 additions and 0 deletions
  1. +4
    -0
      .gitignore
  2. +8
    -0
      .idea/.gitignore
  3. +2
    -0
      .idea/UserScript.iml
  4. +4
    -0
      .idea/misc.xml
  5. +8
    -0
      .idea/modules.xml
  6. +6
    -0
      .idea/vcs.xml
  7. +48
    -0
      CMakeLists.txt
  8. +59
    -0
      include/UserScript.h
  9. +169
    -0
      include/UserScript/parser.h
  10. +347
    -0
      script_exe/main.cpp
  11. +1186
    -0
      src/interpreter.cpp
  12. +1051
    -0
      src/lex_parse.cpp
  13. +118
    -0
      tests/lexer_test.cpp
  14. +168
    -0
      tests/parser_test.cpp
  15. +13
    -0
      tests/scripts/001.results
  16. +18
    -0
      tests/scripts/001.script
  17. +1
    -0
      tests/scripts/002.results
  18. +17
    -0
      tests/scripts/002.script
  19. +10
    -0
      tests/scripts/003.results
  20. +5
    -0
      tests/scripts/003.script
  21. +4
    -0
      tests/scripts/004.results
  22. +5
    -0
      tests/scripts/004.script
  23. +4
    -0
      tests/scripts/005.results
  24. +5
    -0
      tests/scripts/005.script
  25. +16
    -0
      tests/scripts/testfile.test

+ 4
- 0
.gitignore View File

@ -45,3 +45,7 @@ compile_commands.json
CTestTestfile.cmake
_deps
cmake-build-debug/
cmake-build-release/
cmake-build-minsizerel/
cmake-build-relwithdebinfo/

+ 8
- 0
.idea/.gitignore View File

@ -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

+ 2
- 0
.idea/UserScript.iml View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module classpath="CMake" type="CPP_MODULE" version="4" />

+ 4
- 0
.idea/misc.xml View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
</project>

+ 8
- 0
.idea/modules.xml View File

@ -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>

+ 6
- 0
.idea/vcs.xml View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

+ 48
- 0
CMakeLists.txt View File

@ -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)

+ 59
- 0
include/UserScript.h View File

@ -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);
}

+ 169
- 0
include/UserScript/parser.h View File

@ -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);
}
}

+ 347
- 0
script_exe/main.cpp View File

@ -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);
}

+ 1186
- 0
src/interpreter.cpp
File diff suppressed because it is too large
View File


+ 1051
- 0
src/lex_parse.cpp
File diff suppressed because it is too large
View File


+ 118
- 0
tests/lexer_test.cpp View File

@ -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));
}
}
}

+ 168
- 0
tests/parser_test.cpp View File

@ -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<>();
}

+ 13
- 0
tests/scripts/001.results View File

@ -0,0 +1,13 @@
8
12
12
14
14
10
2
2
0
7
0
3
true

+ 18
- 0
tests/scripts/001.script View File

@ -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"

+ 1
- 0
tests/scripts/002.results View File

@ -0,0 +1 @@
1

+ 17
- 0
tests/scripts/002.script View File

@ -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"

+ 10
- 0
tests/scripts/003.results View File

@ -0,0 +1,10 @@
0
1
2
3
4
5
6
7
8
9

+ 5
- 0
tests/scripts/003.script View File

@ -0,0 +1,5 @@
/set counter 0
while(counter < 10)
/print counter "\n"
/set counter (counter+1)
endwhile

+ 4
- 0
tests/scripts/004.results View File

@ -0,0 +1,4 @@
Unexpected statement in block
at line 5:1
endif
^

+ 5
- 0
tests/scripts/004.script View File

@ -0,0 +1,5 @@
/set counter 0
while(counter < 10)
/print counter "\n"
/set counter (counter+1)
endif

+ 4
- 0
tests/scripts/005.results View File

@ -0,0 +1,4 @@
Unexpected expression content
at line 5:1
endwhile
^

+ 5
- 0
tests/scripts/005.script View File

@ -0,0 +1,5 @@
/set counter 0
if(counter < 10)
/print counter "\n"
/set counter (counter+1)
endwhile

+ 16
- 0
tests/scripts/testfile.test View File

@ -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

Loading…
Cancel
Save