diff --git a/compiler/src/compiler/def_class_visitor.cpp b/compiler/src/compiler/def_class_visitor.cpp index 60c13f25..d68f9eb2 100644 --- a/compiler/src/compiler/def_class_visitor.cpp +++ b/compiler/src/compiler/def_class_visitor.cpp @@ -378,3 +378,14 @@ bool def_class_visitor::has_zero_arg_directive(directive_stmt *obj) { } return zero_arg_directive; } +std::vector def_class_visitor::get_all_names() { + std::vector all_names{}; + all_names.insert(all_names.end(), function_names_.begin(), + function_names_.end()); + all_names.insert(all_names.end(), class_names_.begin(), class_names_.end()); + all_names.insert(all_names.end(), global_const_names_.begin(), + global_const_names_.end()); + all_names.insert(all_names.end(), global_native_const_names_.begin(), + global_native_const_names_.end()); + return all_names; +} diff --git a/compiler/src/compiler/def_class_visitor.h b/compiler/src/compiler/def_class_visitor.h index f3faa1af..e251badc 100644 --- a/compiler/src/compiler/def_class_visitor.h +++ b/compiler/src/compiler/def_class_visitor.h @@ -107,6 +107,7 @@ namespace yaksha { void visit_enum_stmt(enum_stmt *obj) override; void visit_union_stmt(union_stmt *obj) override; void visit_directive_stmt(directive_stmt *obj) override; + std::vector get_all_names(); std::vector function_names_{}; std::vector class_names_{}; std::vector global_const_names_{}; diff --git a/compiler/src/compiler/type_checker.cpp b/compiler/src/compiler/type_checker.cpp index 2307a355..57491b18 100644 --- a/compiler/src/compiler/type_checker.cpp +++ b/compiler/src/compiler/type_checker.cpp @@ -752,6 +752,19 @@ void type_checker::visit_del_stmt(del_stmt *obj) { void type_checker::visit_get_expr(get_expr *obj) { handle_dot_operator(obj->lhs_, obj->dot_, obj->item_); } +std::string find_closest(const std::string &member, + const std::vector &members) { + std::string closest; + std::size_t closest_distance = 9999999; + for (const auto &member_name : members) { + std::size_t distance = ::levenshtein_distance(member, member_name); + if (distance < closest_distance) { + closest = member_name; + closest_distance = distance; + } + } + return closest; +} void type_checker::handle_dot_operator(expr *lhs_expr, token *dot, token *member_item) { lhs_expr->accept(this); @@ -791,7 +804,11 @@ void type_checker::handle_dot_operator(expr *lhs_expr, token *dot, obj.module_file_ = lhs.string_val_; obj.module_name_ = lhs.module_name_; } else { - error(dot, "Member not found"); + auto closest = find_closest(member_item->token_, + imported->data_->dsv_->get_all_names()); + error(dot, "Member not found. Perhaps '" + closest + + "' is what you " + "meant?"); } push(obj); return; @@ -803,10 +820,13 @@ void type_checker::handle_dot_operator(expr *lhs_expr, token *dot, return; } auto item = member_item->token_; + bool datatype_of_lhs_found = false; + std::string closest = ""; if (!lhs.datatype_->module_.empty()) { auto mod_file_info = cf_->get_or_null(lhs.datatype_->module_); if (mod_file_info != nullptr && mod_file_info->data_->dsv_->has_class(lhs.datatype_->type_)) { + datatype_of_lhs_found = true; auto class_ = mod_file_info->data_->dsv_->get_class(lhs.datatype_->type_); for (const auto &member : class_->members_) { if (item == member.name_->token_) { @@ -817,9 +837,24 @@ void type_checker::handle_dot_operator(expr *lhs_expr, token *dot, return; } } + std::vector members{}; + for (const auto &member : class_->members_) { + members.push_back(member.name_->token_); + } + closest = find_closest(item, members); + } + } + if (!datatype_of_lhs_found) { + error(dot, "Cannot find data type of LHS"); + } else { + if (closest.empty()) { + error(dot, "Member not found"); + } else { + error(dot, "Member not found. Perhaps '" + closest + + "' is what you " + "meant?"); } } - error(dot, "Cannot find data type of LHS"); push(ykobject(dt_pool_)); } void type_checker::visit_set_expr(set_expr *obj) { @@ -1144,13 +1179,17 @@ class_stmt *type_checker::find_class(token *tok, ykdatatype *data_type) { } void type_checker::validate_member(name_val &member, class_stmt *class_st) { ykdatatype *class_member_dt = nullptr; + std::vector members{}; for (auto const ¶ : class_st->members_) { if (para.name_->token_ == member.name_->token_) { class_member_dt = para.data_type_; } + members.push_back(para.name_->token_); } if (class_member_dt == nullptr) { - error(member.name_, "member not found in class/struct"); + std::string closest = find_closest(member.name_->token_, members); + error(member.name_, "member not found in class/struct. Perhaps '" + + closest + "' is what you meant?"); return; } member.value_->accept(this); diff --git a/compiler/src/utilities/cpp_util.h b/compiler/src/utilities/cpp_util.h index 77e265fc..ebed4aad 100644 --- a/compiler/src/utilities/cpp_util.h +++ b/compiler/src/utilities/cpp_util.h @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -213,5 +214,23 @@ namespace yaksha { } template static inline void intentionally_ignored(const T &) {} + // Reference: https://codereview.stackexchange.com/a/238646/47826 + static inline std::size_t levenshtein_distance(const std::string &string_a, + const std::string &string_b) { + const auto size_a = string_a.size(); + const auto size_b = string_b.size(); + std::vector distances(size_b + 1); + std::iota(distances.begin(), distances.end(), std::size_t{0}); + for (std::size_t i = 0; i < size_a; ++i) { + std::size_t previous_distance = 0; + for (std::size_t j = 0; j < size_b; ++j) { + distances[j + 1] = + std::min({std::exchange(previous_distance, distances[j + 1]) + + (string_a[i] == string_b[j] ? 0 : 1), + distances[j] + 1, distances[j + 1] + 1}); + } + } + return distances[size_b]; + } }// namespace yaksha #endif diff --git a/compiler/tests/test_type_checker.cpp b/compiler/tests/test_type_checker.cpp index b68fead6..d53da88c 100644 --- a/compiler/tests/test_type_checker.cpp +++ b/compiler/tests/test_type_checker.cpp @@ -67,7 +67,8 @@ static void test_typechecker_snippet(const std::string &S, xa += (S); xa += "\n" " return 0"; - auto result = mc.compile(xa, true, "dummy.yaka", "../libs", &cg); + std::filesystem::path code_file_path = std::filesystem::absolute(std::filesystem::path{"dummy.yaka"}); + auto result = mc.compile(xa, true, code_file_path.string(), "../libs", &cg); REQUIRE(result.failed_ == true); REQUIRE(mc.error_printer_.has_any_error()); REQUIRE(mc.error_printer_.has_error(E)); @@ -80,7 +81,8 @@ static void test_typechecker_snippet_ok(const std::string &S) { xa += (S); xa += "\n" " return 0"; - auto result = mc.compile(xa, true, "dummy.yaka", "../libs", &cg); + std::filesystem::path code_file_path = std::filesystem::absolute(std::filesystem::path{"dummy.yaka"}); + auto result = mc.compile(xa, true, code_file_path.string(), "../libs", &cg); REQUIRE(result.failed_ == false); REQUIRE(mc.error_printer_.has_no_errors()); } @@ -89,7 +91,8 @@ static void test_typechecker_snippet_full(const std::string &S, multifile_compiler mc{}; codegen_c cg{}; const std::string &xa = S; - auto result = mc.compile(xa, true, "dummy.yaka", "../libs", &cg); + std::filesystem::path code_file_path = std::filesystem::absolute(std::filesystem::path{"dummy.yaka"}); + auto result = mc.compile(xa, true, code_file_path.string(), "../libs", &cg); REQUIRE(result.failed_ == true); REQUIRE(mc.error_printer_.has_any_error()); REQUIRE(mc.error_printer_.has_error(E)); @@ -98,7 +101,8 @@ static void test_typechecker_snippet_full_ok(const std::string &S) { multifile_compiler mc{}; codegen_c cg{}; const std::string &xa = S; - auto result = mc.compile(xa, true, "dummy.yaka", "../libs", &cg); + std::filesystem::path code_file_path = std::filesystem::absolute(std::filesystem::path{"dummy.yaka"}); + auto result = mc.compile(xa, true, code_file_path.string(), "../libs", &cg); REQUIRE(result.failed_ == false); REQUIRE(mc.error_printer_.has_no_errors()); } @@ -617,9 +621,9 @@ TEST_CASE("type checker: dot operator from class") { TEST_CASE("type checker: member not found from imported module") { test_typechecker_snippet_full("import libs.c\n" "def main() -> int:\n" - " println(c.A)\n" + " println(c.Cos)\n" " return 0", - "Member not found"); + "Member not found. Perhaps 'cos' is what you meant?"); } TEST_CASE("type checker: non existent type access") { test_typechecker_snippet_full("def main() -> int:\n" @@ -635,7 +639,7 @@ TEST_CASE("type checker: non existent element access") { " c: A\n" " println(c.B)\n" " return 0", - "Cannot find data type of LHS"); + "Member not found. Perhaps 'b' is what you meant?"); } TEST_CASE("type checker: ccode statement used outside non native function") { test_typechecker_snippet_full( @@ -679,7 +683,7 @@ TEST_CASE("type checker: Invalid fields in struct") { "def main() -> int:\n" " a = P{k: 0}\n" " return 0\n", - "member not found in class/struct"); + "member not found in class/struct. Perhaps 'x' is what you meant?"); } TEST_CASE("type checker: Duplicate fields in {} init (struct)") { test_typechecker_snippet_full("struct P:\n"