Skip to content

Commit

Permalink
feat(compiler,runtime): better error message when member not found us…
Browse files Browse the repository at this point in the history
…ing levenshtein distance
  • Loading branch information
JaDogg committed Apr 28, 2024
1 parent bebd2f9 commit c4c862e
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 11 deletions.
11 changes: 11 additions & 0 deletions compiler/src/compiler/def_class_visitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,14 @@ bool def_class_visitor::has_zero_arg_directive(directive_stmt *obj) {
}
return zero_arg_directive;
}
std::vector<std::string> def_class_visitor::get_all_names() {
std::vector<std::string> 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;
}
1 change: 1 addition & 0 deletions compiler/src/compiler/def_class_visitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> get_all_names();
std::vector<std::string> function_names_{};
std::vector<std::string> class_names_{};
std::vector<std::string> global_const_names_{};
Expand Down
45 changes: 42 additions & 3 deletions compiler/src/compiler/type_checker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> &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);
Expand Down Expand Up @@ -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;
Expand All @@ -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_) {
Expand All @@ -817,9 +837,24 @@ void type_checker::handle_dot_operator(expr *lhs_expr, token *dot,
return;
}
}
std::vector<std::string> 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) {
Expand Down Expand Up @@ -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<std::string> members{};
for (auto const &para : 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);
Expand Down
19 changes: 19 additions & 0 deletions compiler/src/utilities/cpp_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include <cerrno>
#include <fstream>
#include <iostream>
#include <numeric>
#include <sstream>
#include <streambuf>
#include <string>
Expand Down Expand Up @@ -213,5 +214,23 @@ namespace yaksha {
}
template<typename T>
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<std::size_t> 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
20 changes: 12 additions & 8 deletions compiler/tests/test_type_checker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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());
}
Expand All @@ -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));
Expand All @@ -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());
}
Expand Down Expand Up @@ -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"
Expand All @@ -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(
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit c4c862e

Please sign in to comment.