diff --git a/include/Ark/TypeChecker.hpp b/include/Ark/TypeChecker.hpp index dcb582e4..5b59f1cb 100644 --- a/include/Ark/TypeChecker.hpp +++ b/include/Ark/TypeChecker.hpp @@ -14,6 +14,7 @@ #include #include +#include #include @@ -62,15 +63,15 @@ namespace Ark::types */ struct ARK_API Typedef { - std::string_view name; + std::string name; std::vector types; bool variadic; - Typedef(const std::string_view& type_name, const ValueType type, const bool is_variadic = false) : + Typedef(const std::string& type_name, const ValueType type, const bool is_variadic = false) : name(type_name), types { type }, variadic(is_variadic) {} - Typedef(const std::string_view& type_name, const std::vector& type_list, const bool is_variadic = false) : + Typedef(const std::string& type_name, const std::vector& type_list, const bool is_variadic = false) : name(type_name), types(type_list), variadic(is_variadic) {} }; @@ -90,8 +91,15 @@ namespace Ark::types * @param funcname ArkScript name of the function * @param contracts types contracts the function can follow * @param args provided argument list + * @param os output stream, default to cout + * @param colorize enable output colorizing */ - ARK_API void generateError [[noreturn]] (const std::string_view& funcname, const std::vector& contracts, const std::vector& args); + ARK_API void generateError [[noreturn]] ( + const std::string_view& funcname, + const std::vector& contracts, + const std::vector& args, + std::ostream& os = std::cout, + bool colorize = true); } #endif diff --git a/lib/fmt b/lib/fmt index 7f769552..5f6fb96d 160000 --- a/lib/fmt +++ b/lib/fmt @@ -1 +1 @@ -Subproject commit 7f7695524a4bc3c9b7883afcc86ba61421989b7f +Subproject commit 5f6fb96df1637c3b4a2a068ab03e156ed8c3ea39 diff --git a/src/arkreactor/TypeChecker.cpp b/src/arkreactor/TypeChecker.cpp index 8a6f8a41..d177b571 100644 --- a/src/arkreactor/TypeChecker.cpp +++ b/src/arkreactor/TypeChecker.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include @@ -25,20 +27,25 @@ namespace Ark::types return acc; } - void displayContract(const Contract& contract, const std::vector& args) + void displayContract(const Contract& contract, const std::vector& args, std::ostream& os, const bool colorize) { - auto displayArg = [](const Typedef& td, bool correct) { + auto displayArg = [colorize, &os](const Typedef& td, const bool correct) { const std::string arg_str = typeListToString(td.types); - fmt::print( - " -> {}{} ({}) ", - td.variadic ? "variadic " : "", - fmt::styled( - td.name, - correct - ? fmt::fg(fmt::color::green) - : fmt::fg(fmt::color::magenta)), - arg_str); + fmt::dynamic_format_arg_store store; + store.push_back(td.variadic ? "variadic " : ""); + if (colorize) + store.push_back( + fmt::styled( + td.name, + correct + ? fmt::fg(fmt::color::green) + : fmt::fg(fmt::color::magenta))); + else + store.push_back(td.name); + store.push_back(arg_str); + + fmt::vprint(os, " -> {}{} ({})", store); }; for (std::size_t i = 0, end = contract.arguments.size(); i < end; ++i) @@ -57,36 +64,61 @@ namespace Ark::types if (bad_type) { - displayArg(td, false); - fmt::print("{} argument{} do not match", fmt::styled(bad_type, fmt::fg(fmt::color::red)), bad_type > 1 ? "s" : ""); + displayArg(td, /* correct= */ false); + + fmt::dynamic_format_arg_store store; + if (colorize) + store.push_back(fmt::styled(bad_type, fmt::fg(fmt::color::red))); + else + store.push_back(bad_type); + store.push_back(bad_type > 1 ? "s" : ""); + + fmt::vprint(os, " {} argument{} do not match", store); } else - displayArg(td, true); + displayArg(td, /* correct= */ true); } else { // provided argument but wrong type if (i < args.size() && td.types[0] != ValueType::Any && std::ranges::find(td.types, args[i].valueType()) == td.types.end()) { - displayArg(td, false); - fmt::print("was of type {}", fmt::styled(types_to_str[static_cast(args[i].valueType())], fmt::fg(fmt::color::red))); + displayArg(td, /* correct= */ false); + const auto type = types_to_str[static_cast(args[i].valueType())]; + + fmt::dynamic_format_arg_store store; + if (colorize) + store.push_back(fmt::styled(type, fmt::fg(fmt::color::red))); + else + store.push_back(type); + fmt::vprint(os, " was of type {}", store); } // non-provided argument else if (i >= args.size()) { - displayArg(td, false); - fmt::print(fmt::fg(fmt::color::red), "was not provided"); + displayArg(td, /* correct= */ false); + if (colorize) + fmt::print(os, "{}", fmt::styled(" was not provided", fmt::fg(fmt::color::red))); + else + fmt::print(os, " was not provided"); } else - displayArg(td, true); + displayArg(td, /* correct= */ true); } - fmt::print("\n"); + fmt::print(os, "\n"); } } - [[noreturn]] void generateError(const std::string_view& funcname, const std::vector& contracts, const std::vector& args) + [[noreturn]] void generateError(const std::string_view& funcname, const std::vector& contracts, const std::vector& args, std::ostream& os, bool colorize) { - fmt::print("Function {} expected ", fmt::styled(funcname, fmt::fg(fmt::color::blue))); + { + fmt::dynamic_format_arg_store store; + if (colorize) + store.push_back(fmt::styled(funcname, fmt::fg(fmt::color::blue))); + else + store.push_back(funcname); + fmt::vprint(os, "Function {} expected ", store); + } std::vector sanitizedArgs; std::ranges::copy_if(args, std::back_inserter(sanitizedArgs), [](const Value& value) -> bool { @@ -107,34 +139,53 @@ namespace Ark::types if (min_argc != max_argc) { - fmt::print( - "between {} argument{} and {} argument{}", - fmt::styled(min_argc, fmt::fg(fmt::color::yellow)), - min_argc > 1 ? "s" : "", - fmt::styled(max_argc, fmt::fg(fmt::color::yellow)), - max_argc > 1 ? "s" : ""); + fmt::dynamic_format_arg_store store; + if (colorize) + store.push_back(fmt::styled(min_argc, fmt::fg(fmt::color::yellow))); + else + store.push_back(min_argc); + store.push_back(min_argc > 1 ? "s" : ""); + if (colorize) + store.push_back(fmt::styled(max_argc, fmt::fg(fmt::color::yellow))); + else + store.push_back(max_argc); + store.push_back(max_argc > 1 ? "s" : ""); + + fmt::vprint(os, "between {} argument{} and {} argument{}", store); if (sanitizedArgs.size() < min_argc || sanitizedArgs.size() > max_argc) correct_argcount = false; } else { - fmt::print("{} argument{}", fmt::styled(min_argc, fmt::fg(fmt::color::yellow)), min_argc > 1 ? "s" : ""); + fmt::dynamic_format_arg_store store; + if (colorize) + store.push_back(fmt::styled(min_argc, fmt::fg(fmt::color::yellow))); + else + store.push_back(min_argc); + store.push_back(min_argc > 1 ? "s" : ""); + + fmt::vprint(os, "{} argument{}", store); if (sanitizedArgs.size() != min_argc) correct_argcount = false; } if (!correct_argcount) - fmt::print(" but got {}", fmt::styled(sanitizedArgs.size(), fmt::fg(fmt::color::red))); + { + if (colorize) + fmt::print(os, " but got {}", fmt::styled(sanitizedArgs.size(), fmt::fg(fmt::color::red))); + else + fmt::print(os, " but got {}", sanitizedArgs.size()); + } - fmt::print("\n"); + fmt::print(os, "\n"); - displayContract(contracts[0], sanitizedArgs); + displayContract(contracts[0], sanitizedArgs, os, colorize); for (std::size_t i = 1, end = contracts.size(); i < end; ++i) { - std::cout << "Alternative " << (i + 1) << ":\n"; - displayContract(contracts[i], sanitizedArgs); + fmt::print(os, "Alternative {}:\n", i + 1); + displayContract(contracts[i], sanitizedArgs, os, colorize); } throw TypeError(""); diff --git a/tests/unittests/Suites/TypeCheckerSuite.cpp b/tests/unittests/Suites/TypeCheckerSuite.cpp new file mode 100644 index 00000000..e80a1193 --- /dev/null +++ b/tests/unittests/Suites/TypeCheckerSuite.cpp @@ -0,0 +1,186 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +using namespace boost; + +struct Input +{ + std::string func; + std::size_t expected_arg_count {}; + Ark::types::Contract expected_arg_types; + std::vector given_args; + + bool initialized = false; +}; + +Input parse_input(const std::string& path) +{ + using namespace ut; + + const auto content = Ark::Utils::readFile(path); + const auto lines = Ark::Utils::splitString(content, '\n'); + expect(lines.size() > 1) << fmt::format("not enough data to construct test input {}\n", path) << fatal; + + // parsing "# name,count" + // ^^^^ + const auto funcname = lines[0].substr( + lines[0].find_first_not_of("# "), + lines[0].find_first_of(',') - lines[0].find_first_not_of("# ")); + expect(!funcname.empty()) << fmt::format("function name empty in test input {}\n", path) << fatal; + + // parsing "# name,count" + // ^^^^^ + const auto s_arg_count = lines[0].substr(lines[0].find_first_of(',') + 1); + expect(s_arg_count.size() == 1 && s_arg_count[0] >= '0' && s_arg_count[0] <= '9') + << fmt::format("expected argc should be 0-9 in test input {}\n", path) + << fatal; + const auto arg_count = static_cast(s_arg_count[0] - '0'); + expect(lines.size() >= 2 + arg_count) + << fmt::format("not enough defined arguments in test input {}\n", path) + << fatal; + + // parsing following lines defining argument types "# name:ArkType" + Ark::types::Contract args; + auto parse_def = [&path, &args](std::size_t i, const std::string& def, bool is_sum_type = false) { + const auto arg_name = def.substr(0, def.find_first_of(':')); + const auto type_name = def.substr(def.find_first_of(':') + 1); + + if (auto it = std::ranges::find(Ark::types_to_str, type_name); it != Ark::types_to_str.end()) + { + auto type = static_cast(std::distance(Ark::types_to_str.begin(), it)); + if (!is_sum_type || args.arguments.empty() || args.arguments.back().types.empty()) + args.arguments.emplace_back(arg_name, type); + else + args.arguments.back().types.emplace_back(type); + } + else + expect(false) + << fmt::format("[line {}] couldn't find the corresponding Ark type for {} in test input {}\n", i + 1, type_name, path) + << fatal; + }; + for (std::size_t i = 0; i < arg_count; ++i) + { + const auto& definition = lines[1 + i].substr(lines[1 + i].find_first_not_of("# ")); + const auto args_with_name = Ark::Utils::splitString(definition, ','); + + if (args_with_name.size() == 1) + { + const auto& def = args_with_name.front(); + parse_def(i, def); + } + else + { + // argument can have multiple types + for (auto&& def : args_with_name) + parse_def(i, def, /* is_sum_type= */ true); + } + } + + // parsing given argument list "# ArkType,ArkType,..." + const auto tmp = lines[1 + arg_count].find_first_not_of("# "); + const auto args_def = Ark::Utils::splitString(lines[1 + arg_count].substr(tmp), ','); + std::vector given_args; + for (const auto& arg_type : args_def) + { + if (auto it = std::ranges::find(Ark::types_to_str, arg_type); it != Ark::types_to_str.end()) + { + auto type = static_cast(std::distance(Ark::types_to_str.begin(), it)); + switch (type) + { + case Ark::ValueType::List: + given_args.emplace_back(std::vector { Ark::Value(1) }); + break; + + case Ark::ValueType::Number: + given_args.emplace_back(1.0); + break; + + case Ark::ValueType::String: + given_args.emplace_back("hello"); + break; + + case Ark::ValueType::PageAddr: + given_args.emplace_back(static_cast(12)); + break; + + case Ark::ValueType::CProc: + given_args.emplace_back([](std::vector&, Ark::VM*) -> Ark::Value { + return Ark::Value(Ark::ValueType::Nil); + }); + break; + + case Ark::ValueType::Nil: + [[fallthrough]]; + case Ark::ValueType::True: + [[fallthrough]]; + case Ark::ValueType::False: + given_args.emplace_back(type); + break; + + case Ark::ValueType::Closure: + // unsupported + [[fallthrough]]; + case Ark::ValueType::User: + // unsupported + [[fallthrough]]; + case Ark::ValueType::Undefined: + [[fallthrough]]; + case Ark::ValueType::Reference: + [[fallthrough]]; + case Ark::ValueType::InstPtr: + [[fallthrough]]; + case Ark::ValueType::Any: + break; + } + } + else + expect(false) + << fmt::format("[line {}] couldn't find the corresponding Ark type for {} in test input {}\n", 1 + arg_count, arg_type, path) + << fatal; + } + + return Input { + .func = funcname, + .expected_arg_count = arg_count, + .expected_arg_types = args, + .given_args = given_args, + .initialized = true + }; +} + +ut::suite<"TypeChecker"> type_checker_suite = [] { + using namespace ut; + + iter_test_files("TypeCheckerSuite", [&](TestData&& data) { + const Input input = parse_input(data.path); + expect(fatal(input.initialized)) << "invalid test input: " << data.stem; + + should("generate error message " + data.stem) = [input, data] { + std::stringstream stream; + try + { + Ark::types::generateError( + input.func, + { input.expected_arg_types }, + input.given_args, + stream, + /* colorize= */ false); + expect(fatal(false)) << "generateError should throw an Ark::TypeError"; + } + catch (const Ark::TypeError&) + { + auto result = stream.str(); + rtrim(ltrim(result)); + expect_or_diff(data.expected, result); + } + }; + }); +}; diff --git a/tests/unittests/resources/TypeCheckerSuite/not_enough_args.ark b/tests/unittests/resources/TypeCheckerSuite/not_enough_args.ark new file mode 100644 index 00000000..6fb5adce --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/not_enough_args.ark @@ -0,0 +1,4 @@ +# f,2 +# n:Number +# m:Number +# Number \ No newline at end of file diff --git a/tests/unittests/resources/TypeCheckerSuite/not_enough_args.expected b/tests/unittests/resources/TypeCheckerSuite/not_enough_args.expected new file mode 100644 index 00000000..c316ce41 --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/not_enough_args.expected @@ -0,0 +1,3 @@ +Function f expected 2 arguments but got 1 + -> n (Number) + -> m (Number) was not provided diff --git a/tests/unittests/resources/TypeCheckerSuite/num.ark b/tests/unittests/resources/TypeCheckerSuite/num.ark new file mode 100644 index 00000000..a22c78b4 --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/num.ark @@ -0,0 +1,3 @@ +# f,1 +# n:Number +# String \ No newline at end of file diff --git a/tests/unittests/resources/TypeCheckerSuite/num.expected b/tests/unittests/resources/TypeCheckerSuite/num.expected new file mode 100644 index 00000000..a6bfe8c2 --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/num.expected @@ -0,0 +1,2 @@ +Function f expected 1 argument + -> n (Number) was of type String diff --git a/tests/unittests/resources/TypeCheckerSuite/sum_type.ark b/tests/unittests/resources/TypeCheckerSuite/sum_type.ark new file mode 100644 index 00000000..002eefb8 --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/sum_type.ark @@ -0,0 +1,3 @@ +# f,1 +# n:Number,_:String +# Bool \ No newline at end of file diff --git a/tests/unittests/resources/TypeCheckerSuite/sum_type.expected b/tests/unittests/resources/TypeCheckerSuite/sum_type.expected new file mode 100644 index 00000000..14c76ac6 --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/sum_type.expected @@ -0,0 +1,2 @@ +Function f expected 1 argument + -> n (Number, String) was of type Bool diff --git a/tests/unittests/resources/TypeCheckerSuite/too_many_args.ark b/tests/unittests/resources/TypeCheckerSuite/too_many_args.ark new file mode 100644 index 00000000..7ee5d595 --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/too_many_args.ark @@ -0,0 +1,3 @@ +# f,1 +# n:Number +# Number,String \ No newline at end of file diff --git a/tests/unittests/resources/TypeCheckerSuite/too_many_args.expected b/tests/unittests/resources/TypeCheckerSuite/too_many_args.expected new file mode 100644 index 00000000..2fa2df84 --- /dev/null +++ b/tests/unittests/resources/TypeCheckerSuite/too_many_args.expected @@ -0,0 +1,2 @@ +Function f expected 1 argument but got 2 + -> n (Number)