diff options
| author | Bruce Hill <bruce@bruce-hill.com> | 2026-02-08 23:53:36 -0500 |
|---|---|---|
| committer | Bruce Hill <bruce@bruce-hill.com> | 2026-02-08 23:53:36 -0500 |
| commit | b6372ef6cc7b9727bc90946e91c85e2749dc372d (patch) | |
| tree | eef7a1265a3eb3bf25ae306f00abe01a82bd637d | |
| parent | 4e31786efcf32818a620cc87ffa80dadef90a466 (diff) | |
Add a "did you mean?" suggestion for invalid field/method names.
| -rw-r--r-- | src/compile/functions.c | 5 | ||||
| -rw-r--r-- | src/typecheck.c | 122 | ||||
| -rw-r--r-- | src/typecheck.h | 3 |
3 files changed, 123 insertions, 7 deletions
diff --git a/src/compile/functions.c b/src/compile/functions.c index f62e00f8..2db419d2 100644 --- a/src/compile/functions.c +++ b/src/compile/functions.c @@ -941,7 +941,10 @@ Text_t compile_method_call(env_t *env, ast_t *ast) { type_t *fn_t = get_method_type(env, methodcall->self, methodcall->name); arg_ast_t *args = new (arg_ast_t, .value = methodcall->self, .next = methodcall->args); binding_t *b = get_namespace_binding(env, methodcall->self, methodcall->name); - if (!b) code_err(ast, "No such method"); + if (!b) { + OptionalText_t suggestion = suggest_best_name(call->name, get_method_names(env, self_value_t)); + code_err(ast, "No such method!", suggestion); + } return Texts(b->code, "(", compile_arguments(env, ast, Match(fn_t, FunctionType)->args, args), ")"); } } diff --git a/src/typecheck.c b/src/typecheck.c index a073cb2e..8e82ee28 100644 --- a/src/typecheck.c +++ b/src/typecheck.c @@ -623,7 +623,10 @@ type_t *get_function_return_type(env_t *env, ast_t *ast) { type_t *get_method_type(env_t *env, ast_t *self, const char *name) { binding_t *b = get_namespace_binding(env, self, name); - if (!b || !b->type) code_err(self, "No such method: ", type_to_text(get_type(env, self)), ".", name, "(...)"); + if (!b || !b->type) { + OptionalText_t suggestion = suggest_best_name(name, get_method_names(env, get_type(env, self))); + code_err(self, "No such method: ", type_to_text(get_type(env, self)), ".", name, "(...)", suggestion); + } return b->type; } @@ -864,8 +867,11 @@ type_t *get_type(env_t *env, ast_t *ast) { return b->type; } type_t *field_t = get_field_type(fielded_t, access->field); - if (!field_t) - code_err(ast, type_to_text(fielded_t), " objects don't have a field called '", access->field, "'"); + if (!field_t) { + OptionalText_t suggestion = suggest_best_name(access->field, get_field_names(env, fielded_t)); + code_err(ast, type_to_text(fielded_t), " objects don't have a field called '", access->field, "'", + suggestion); + } return field_t; } case Index: { @@ -966,7 +972,9 @@ type_t *get_type(env_t *env, ast_t *ast) { else if (streq(call->name, "to")) return self_value_t; else if (streq(call->name, "unique")) return Type(TableType, .key_type = item_type, .value_type = PRESENT_TYPE); - else code_err(ast, "There is no '", call->name, "' method for lists"); + + OptionalText_t suggestion = suggest_best_name(call->name, get_method_names(env, self_value_t)); + code_err(ast, "There is no '", call->name, "' method for lists", suggestion); } case TableType: { DeclareMatch(table, self_value_t, TableType); @@ -982,7 +990,10 @@ type_t *get_type(env_t *env, ast_t *ast) { else if (streq(call->name, "intersection")) return self_value_t; else if (streq(call->name, "difference")) return self_value_t; else if (streq(call->name, "with")) return self_value_t; - code_err(ast, "There is no '", call->name, "' method for ", type_to_text(self_value_t), " tables"); + + OptionalText_t suggestion = suggest_best_name(call->name, get_method_names(env, self_value_t)); + code_err(ast, "There is no '", call->name, "' method for ", type_to_text(self_value_t), " tables", + suggestion); } default: { if (call->name[0] == '_') { @@ -994,7 +1005,10 @@ type_t *get_type(env_t *env, ast_t *ast) { if (field_type && field_type->tag == ClosureType) field_type = Match(field_type, ClosureType)->fn; if (field_type && field_type->tag == FunctionType) return Match(field_type, FunctionType)->ret; type_t *fn_type_t = get_method_type(env, call->self, call->name); - if (!fn_type_t) code_err(ast, "No such method!"); + if (!fn_type_t) { + OptionalText_t suggestion = suggest_best_name(call->name, get_method_names(env, self_value_t)); + code_err(ast, "No such method!", suggestion); + } if (fn_type_t->tag != FunctionType) code_err(ast, "This isn't a method, it's a ", type_to_text(fn_type_t)); DeclareMatch(fn_type, fn_type_t, FunctionType); return fn_type->ret; @@ -1738,3 +1752,99 @@ PUREFUNC bool can_compile_to_type(env_t *env, ast_t *ast, type_t *needed) { return can_promote(actual, non_optional_needed); } } + +OptionalText_t suggest_best_name(const char *wrong, List_t names) { + if (names.length == 0) return NONE_TEXT; + Text_t target = Text$from_str(wrong); + Text_t best = *(Text_t *)names.data; + Text_t lang = Text("C"); + double best_dist = Text$distance(target, best, lang); + for (int64_t i = 1; i < (int64_t)names.length; i++) { + Text_t text = *(Text_t *)(names.data + i * names.stride); + double dist = Text$distance(target, text, lang); + if (dist < best_dist) { + best = text; + best_dist = dist; + } + } + + // Too far away: + if (best_dist > 6.0 || best_dist > 0.75 * (double)target.length || best_dist > 0.75 * (double)best.length) + return NONE_TEXT; + return Texts("\nDid you mean '", best, "'?"); +} + +List_t get_field_names(env_t *env, type_t *t) { + t = value_type(t); + switch (t->tag) { + case PointerType: return get_field_names(env, Match(t, PointerType)->pointed); + case TextType: return List(Text("text"), Text("length")); + case StructType: { + DeclareMatch(struct_t, t, StructType); + List_t fields = EMPTY_LIST; + for (arg_t *field = struct_t->fields; field; field = field->next) { + Text_t field_text = Text$from_str(field->name); + List$insert(&fields, &field_text, I(0), sizeof(Text_t)); + } + return fields; + } + case EnumType: { + DeclareMatch(e, t, EnumType); + List_t tags = EMPTY_LIST; + for (tag_t *tag = e->tags; tag; tag = tag->next) { + Text_t tag_text = Text$from_str(tag->name); + List$insert(&tags, &tag_text, I(0), sizeof(Text_t)); + } + return tags; + } + case TableType: return List(Text("length"), Text("keys"), Text("values"), Text("fallback")); + case ListType: return List(Text("length")); + default: { + env_t *ns_env = get_namespace_by_type(env, t); + List_t fields = EMPTY_LIST; + for (int64_t i = 0; i < (int64_t)ns_env->locals->entries.length; i++) { + struct { + const char *key; + binding_t *value; + } *entry = (ns_env->locals->entries.data + i * ns_env->locals->entries.stride); + if (entry->value->type->tag == FunctionType || entry->value->type->tag == ClosureType) continue; + Text_t name = Text$from_str(entry->key); + List$insert(&fields, &name, I(0), sizeof(Text_t)); + } + return fields; + } + } +} + +List_t get_method_names(env_t *env, type_t *t) { + t = value_type(t); + switch (t->tag) { + case PointerType: return get_method_names(env, Match(t, PointerType)->pointed); + case ListType: { + return List(Text("binary_search"), Text("by"), Text("clear"), Text("counts"), Text("find"), Text("where"), + Text("from"), Text("has"), Text("heap_pop"), Text("heap_push"), Text("heapify"), Text("insert"), + Text("insert_all"), Text("pop"), Text("random"), Text("remove_at"), Text("remove_item"), + Text("reversed"), Text("sample"), Text("shuffle"), Text("shuffled"), Text("slice"), Text("sort"), + Text("sorted"), Text("to"), Text("unique")); + } + case TableType: { + return List(Text("clear"), Text("get"), Text("get_or_set"), Text("has"), Text("remove"), Text("set"), + Text("sorted"), Text("with_fallback"), Text("without"), Text("intersection"), Text("difference"), + Text("with")); + } + default: { + env_t *ns_env = get_namespace_by_type(env, t); + List_t methods = EMPTY_LIST; + for (int64_t i = 0; i < (int64_t)ns_env->locals->entries.length; i++) { + struct { + const char *key; + binding_t *value; + } *entry = (ns_env->locals->entries.data + i * ns_env->locals->entries.stride); + if (entry->value->type->tag != FunctionType && entry->value->type->tag != ClosureType) continue; + Text_t name = Text$from_str(entry->key); + List$insert(&methods, &name, I(0), sizeof(Text_t)); + } + return methods; + } + } +} diff --git a/src/typecheck.h b/src/typecheck.h index d64bb316..5a2a543b 100644 --- a/src/typecheck.h +++ b/src/typecheck.h @@ -27,6 +27,9 @@ type_t *parse_type_string(env_t *env, const char *str); type_t *get_method_type(env_t *env, ast_t *self, const char *name); PUREFUNC bool is_constant(env_t *env, ast_t *ast); PUREFUNC bool can_compile_to_type(env_t *env, ast_t *ast, type_t *needed); +OptionalText_t suggest_best_name(const char *wrong, List_t names); +List_t get_field_names(env_t *env, type_t *t); +List_t get_method_names(env_t *env, type_t *t); typedef struct { bool promotion : 1, underscores : 1; |
