From 7e7a8e5647ae32b1735ee81712279800a1092f89 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Thu, 29 Jan 2026 17:59:36 +0100 Subject: [PATCH 1/2] feat(diagnostics): ARK-321, allow generating diagnostics from string instead of files --- include/Ark/Error/Diagnostics.hpp | 3 ++ include/Ark/Error/PrettyPrinting.hpp | 3 +- include/Ark/VM/VM.hpp | 2 +- lib/std | 2 +- src/arkreactor/Compiler/Welder.cpp | 5 ++- src/arkreactor/Error/Diagnostics.cpp | 25 ++++++++++++-- src/arkreactor/Error/PrettyPrinting.cpp | 5 +-- src/arkreactor/VM/Debugger.cpp | 3 +- src/arkreactor/VM/VM.cpp | 3 +- tests/unittests/Suites/EmbeddingSuite.cpp | 41 +++++++++++++++++++---- 10 files changed, 75 insertions(+), 17 deletions(-) diff --git a/include/Ark/Error/Diagnostics.hpp b/include/Ark/Error/Diagnostics.hpp index cd7e1c41..41084eff 100644 --- a/include/Ark/Error/Diagnostics.hpp +++ b/include/Ark/Error/Diagnostics.hpp @@ -26,6 +26,7 @@ namespace Ark::Diagnostics std::string filename; ///< Complete path to the file where the error is internal::FilePos start; std::optional end; + std::optional maybe_content; [[nodiscard]] bool wholeLineIsError() const { @@ -63,6 +64,8 @@ namespace Ark::Diagnostics */ std::string makeContextWithNode(const std::string& message, const internal::Node& node); + ARK_API void generateWithCode(const CodeError& e, const std::string& code, std::ostream& os = std::cout, bool colorize = true); + /** * @brief Generate a diagnostic from an error and print it to the standard output * diff --git a/include/Ark/Error/PrettyPrinting.hpp b/include/Ark/Error/PrettyPrinting.hpp index 23a2f111..3c99f187 100644 --- a/include/Ark/Error/PrettyPrinting.hpp +++ b/include/Ark/Error/PrettyPrinting.hpp @@ -68,8 +68,9 @@ namespace Ark::Diagnostics * @param target_line line of the error (0-indexed) * @param end_target_line optional end line for the error (0-indexed) * @param colorize if we should colorize the output or not + * @param maybe_content optional file content, if it was originally a string (via State.doString) */ - Printer(const std::string& filename, std::size_t target_line, std::optional end_target_line, bool colorize); + Printer(const std::string& filename, std::size_t target_line, std::optional end_target_line, bool colorize, const std::optional& maybe_content = std::nullopt); /** * @brief Slice the source code to get code between two cursors diff --git a/include/Ark/VM/VM.hpp b/include/Ark/VM/VM.hpp index b779cb91..2672ad56 100644 --- a/include/Ark/VM/VM.hpp +++ b/include/Ark/VM/VM.hpp @@ -167,7 +167,7 @@ namespace Ark */ [[noreturn]] static void throwVMError(internal::ErrorKind kind, const std::string& message); - inline const bytecode_t& bytecode() const + [[nodiscard]] inline const bytecode_t& bytecode() const { return m_state.m_bytecode; } diff --git a/lib/std b/lib/std index 87a9c371..c6100e52 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 87a9c371b39390036d6165646edd244f367ba922 +Subproject commit c6100e52e154f0d475002366b9e46dd3c043ea3b diff --git a/src/arkreactor/Compiler/Welder.cpp b/src/arkreactor/Compiler/Welder.cpp index e595efcb..31c7da36 100644 --- a/src/arkreactor/Compiler/Welder.cpp +++ b/src/arkreactor/Compiler/Welder.cpp @@ -204,7 +204,10 @@ namespace Ark if ((m_features & FeatureTestFailOnException) > 0) throw; - Diagnostics::generate(e); + if (filename != ARK_NO_NAME_FILE) + Diagnostics::generate(e); + else + Diagnostics::generateWithCode(e, code); return false; } } diff --git a/src/arkreactor/Error/Diagnostics.cpp b/src/arkreactor/Error/Diagnostics.cpp index 2f47b13f..621dc85e 100644 --- a/src/arkreactor/Error/Diagnostics.cpp +++ b/src/arkreactor/Error/Diagnostics.cpp @@ -50,7 +50,7 @@ namespace Ark::Diagnostics { assert(!(maybe_context && loc.wholeLineIsError()) && "Can not create error context when a context is given AND the whole line has to be underlined"); - Printer source_printer(loc.filename, loc.start.line, loc.maybeEndLine(), colorize); + Printer source_printer(loc.filename, loc.start.line, loc.maybeEndLine(), colorize, loc.maybe_content); if (!source_printer.hasContent()) { showFileLocation(os, loc); @@ -193,7 +193,8 @@ namespace Ark::Diagnostics void helper(std::ostream& os, const std::string& message, const bool colorize, const std::string& filename, const internal::FileSpan& at, - const std::optional& maybe_context = std::nullopt) + const std::optional& maybe_context = std::nullopt, + const std::optional& maybe_file_content = std::nullopt) { std::string uniformised_filename; std::ranges::replace_copy(filename, std::back_inserter(uniformised_filename), '\\', '/'); @@ -201,7 +202,8 @@ namespace Ark::Diagnostics ErrorLocation { .filename = uniformised_filename, .start = at.start, - .end = at.end }, + .end = at.end, + .maybe_content = maybe_file_content }, os, maybe_context, colorize); for (const auto& text : Utils::splitString(message, '\n')) @@ -222,6 +224,23 @@ namespace Ark::Diagnostics return ss.str(); } + void generateWithCode(const CodeError& e, const std::string& code, std::ostream& os, bool colorize) + { +#ifdef ARK_BUILD_EXE + if (const char* nocolor = std::getenv("NOCOLOR"); nocolor != nullptr) + colorize = false; +#endif + + helper( + os, + e.what(), + colorize, + e.context.filename, + e.context.at, + e.additional_context, + code); + } + void generate(const CodeError& e, std::ostream& os, bool colorize) { #ifdef ARK_BUILD_EXE diff --git a/src/arkreactor/Error/PrettyPrinting.cpp b/src/arkreactor/Error/PrettyPrinting.cpp index c6cfd1b6..fd5e75bb 100644 --- a/src/arkreactor/Error/PrettyPrinting.cpp +++ b/src/arkreactor/Error/PrettyPrinting.cpp @@ -77,10 +77,11 @@ namespace Ark::Diagnostics Printer::Printer( const std::string& filename, const std::size_t target_line, - const std::optional end_target_line, const bool colorize) : + const std::optional end_target_line, const bool colorize, + const std::optional& maybe_content) : m_should_colorize(colorize) { - const std::string code = filename == ARK_NO_NAME_FILE ? "" : Utils::readFile(filename); + const std::string code = filename == ARK_NO_NAME_FILE ? maybe_content.value_or("") : Utils::readFile(filename); m_source = Utils::splitString(code, '\n'); m_window = Window(target_line, end_target_line.value_or(target_line), m_source.size()); diff --git a/src/arkreactor/VM/Debugger.cpp b/src/arkreactor/VM/Debugger.cpp index 06fde1ef..3b078168 100644 --- a/src/arkreactor/VM/Debugger.cpp +++ b/src/arkreactor/VM/Debugger.cpp @@ -133,7 +133,8 @@ namespace Ark::internal Diagnostics::ErrorLocation { .filename = filename, .start = FilePos { .line = maybe_source_loc->line, .column = 0 }, - .end = std::nullopt }, + .end = std::nullopt, + .maybe_content = std::nullopt }, m_os, /* maybe_context= */ std::nullopt, /* colorize= */ m_colorize); diff --git a/src/arkreactor/VM/VM.cpp b/src/arkreactor/VM/VM.cpp index 130f45fc..cd202371 100644 --- a/src/arkreactor/VM/VM.cpp +++ b/src/arkreactor/VM/VM.cpp @@ -2366,7 +2366,8 @@ namespace Ark Diagnostics::ErrorLocation { .filename = filename, .start = FilePos { .line = maybe_location->line, .column = 0 }, - .end = std::nullopt }, + .end = std::nullopt, + .maybe_content = std::nullopt }, os, /* maybe_context= */ std::nullopt, /* colorize= */ colorize); diff --git a/tests/unittests/Suites/EmbeddingSuite.cpp b/tests/unittests/Suites/EmbeddingSuite.cpp index 7e3064e9..a0d676a3 100644 --- a/tests/unittests/Suites/EmbeddingSuite.cpp +++ b/tests/unittests/Suites/EmbeddingSuite.cpp @@ -4,11 +4,13 @@ #include #include #include +#include #include using namespace boost; using namespace Ark::literals; +// cppcheck-suppress [constParameterCallback, constParameterReference] Ark::Value my_function(std::vector& args, Ark::VM* vm [[maybe_unused]]) { // checking argument number @@ -187,6 +189,7 @@ ut::suite<"Embedding"> embedding_suite = [] { Ark::State state; int capture = 42; + // cppcheck-suppress constParameterReference state.loadFunction("my_function", [=](std::vector& args, [[maybe_unused]] Ark::VM* /*vm*/) { int solution = 0; for (const Ark::Value& value : args) @@ -215,7 +218,8 @@ ut::suite<"Embedding"> embedding_suite = [] { "[load cpp function with captured reference]"_test = [] { Ark::State state; - std::string name = ""; + std::string name; + // cppcheck-suppress constParameterReference state.loadFunction("my_function", [&name](std::vector& args, [[maybe_unused]] Ark::VM* /*vm*/) { for (const Ark::Value& value : args) { @@ -243,6 +247,7 @@ ut::suite<"Embedding"> embedding_suite = [] { "[load cpp function and call it from arkscript]"_test = [] { Ark::State state; state.loadFunction("my_function", my_function); + // cppcheck-suppress constParameterReference state.loadFunction("foo", [](std::vector& args, Ark::VM* /*vm*/) { return Ark::Value(static_cast(args.size())); }); @@ -305,6 +310,33 @@ ut::suite<"Embedding"> embedding_suite = [] { }; }; + "[fail to compile embedded code]"_test = [] { + constexpr uint16_t features = Ark::DefaultFeatures | Ark::FeatureTestFailOnException; + Ark::State state({ ARK_TESTS_ROOT "lib" }); + const std::string code = "(import std.Sys) (let foo sys:args) (let b bar)"; + const std::string expected = R"( 1 | (import std.Sys) (let foo sys:args) (let b bar) + | ^~~ + Unbound variable error "bar" (variable is used but not defined))"; + + should("compile the string with an error") = [&] { + try + { + const bool ok = mut(state).doString(code, features); + expect(!ok) << fatal; // we shouldn't be here, the compilation has to fail + } + catch (const Ark::CodeError& e) + { + std::stringstream stream; + Ark::Diagnostics::generateWithCode(e, code, stream, /* colorize= */ false); + std::string diag = stream.str(); + diag.erase(std::ranges::remove(diag, '\r').begin(), diag.end()); + Ark::Utils::rtrim(diag); + + expectOrDiff(expected, diag); + } + }; + }; + "[retrieve sys:args in embedded code]"_test = [] { Ark::State state({ ARK_TESTS_ROOT "lib" }); @@ -313,10 +345,8 @@ ut::suite<"Embedding"> embedding_suite = [] { }; Ark::VM vm(state); - double timestamp = 0.0; should("return exit code 0") = [&] { expect(mut(vm).run() == 0_i); - timestamp = vm["t"].number(); }; should("have symbol foo registered") = [&] { @@ -335,10 +365,8 @@ ut::suite<"Embedding"> embedding_suite = [] { }; Ark::VM vm(state); - double timestamp = 0.0; should("return exit code 0") = [&] { expect(mut(vm).run() == 0_i); - timestamp = vm["t"].number(); }; should("have symbol foo registered") = [&] { @@ -353,6 +381,7 @@ ut::suite<"Embedding"> embedding_suite = [] { "[load usertype and cpp lambdas and call them from arkscript]"_test = [] { Ark::State state; + // cppcheck-suppress constParameterReference state.loadFunction("getBreakfast", [](std::vector& n [[maybe_unused]], Ark::VM* vm [[maybe_unused]]) -> Ark::Value { // we need to send the address of the object, which will be cast // to void* internally @@ -366,7 +395,7 @@ ut::suite<"Embedding"> embedding_suite = [] { state.loadFunction("useBreakfast", [](std::vector& n, Ark::VM* vm [[maybe_unused]]) -> Ark::Value { if (n[0].valueType() == Ark::ValueType::User && n[0].usertype().is()) { - auto& bf = n[0].usertypeRef().as(); + const auto& bf = n[0].usertypeRef().as(); if (bf == Breakfast::Pizza) return Ark::Value(1); return Ark::Value(2); From 263052f3fa7b5c74a56ad5d65f1acb15e35687ef Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Thu, 29 Jan 2026 18:26:16 +0100 Subject: [PATCH 2/2] chore: update credits --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b98ab92..8d1a11bc 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ Huge thanks to those people for their donations to support the project: ## Credits -This project was inspired by [game programing patterns](http://gameprogrammingpatterns.com/bytecode.html) and [ofan lisp.cpp](https://gist.github.com/ofan/721464) +This project was inspired by [game programing patterns](http://gameprogrammingpatterns.com/bytecode.html) and [anthay/Lisp90](https://github.com/anthay/Lisp90) ## Copyright and Licence information