diff --git a/devel/200_58.md b/devel/200_58.md new file mode 100644 index 00000000..81da12e9 --- /dev/null +++ b/devel/200_58.md @@ -0,0 +1,176 @@ +# [200_58] 缺失函数提示增强 + +## 如何测试 + +### 1. 函数存在,但对应库没有导入 +```bash +./bin/gf eval '(path-join "a" "b")' +``` + +预期会输出类似: + +```text +unbound-variable + +;unbound variable path-join in (path-join "a" "b") +(path-join "a" "b") + +Hint: function `path-join` exists in library `(liii path)`. +Please import that library first: `(import (liii path))`. +``` + +### 2. 函数在多个库中都有实现 +```bash +./bin/gf eval '(remove 1)' +``` + +预期会输出类似: + +```text +Hint: function `remove` exists in multiple visible libraries: + (liii list) + (liii os) + (srfi srfi-1) +Try one of these commands to decide which library to use: + gf doc liii/list "remove" + gf doc liii/os "remove" + gf doc srfi/srfi-1 "remove" +``` + +### 3. 函数不存在 +```bash +./bin/gf eval '(foo 1)' +``` + +预期会输出类似: + +```text +Hint: try `gf doc "foo"` . +`gf doc` may show similarly named functions when there is no exact match. +If it finds nothing similar, try searching the codebase with `git grep "foo"`, implement that function yourself, or stop using it. +``` + +这里的提示说明了两种情况: + +- `gf doc "foo"` 可能会返回与 `foo` 命名相近的函数 +- 如果连命名相近的函数也找不到,就建议使用 `git grep` 等工具继续查找,或者自行实现该函数,或者放弃使用 + +### 4. REPL 中也应当出现同样的提示 +```bash +printf '(path-join "a" "b")\n,q\n' | ./bin/gf repl +``` + +预期会输出类似: + +```text +;unbound variable path-join in (path-join "a" "b") +(path-join "a" "b") + +Hint: function `path-join` exists in library `(liii path)`. +Please import that library first: `(import (liii path))`. +``` + +### 5. 嵌套表达式中的缺失函数也应当提示 +```bash +./bin/gf eval '((display (path-read-text "/tmp/demo")) 0)' +``` + +预期会输出类似: + +```text +;unbound variable path-read-text ... +... +Hint: function `path-read-text` exists in library `(liii path)`. +Please import that library first: `(import (liii path))`. +``` + +这一步的意义是确认提示逻辑不只适用于最外层直接调用 `(foo ...)`,也能处理更大的表达式内部出现的未绑定函数。 + + + +### 6. 相关测试 +```bash +xmake build goldfish +./bin/gf test doc-hint-test.scm +./bin/gf test function-libraries-test.scm +./bin/gf test tests/liii/goldsource/source-command-test.scm +./bin/gf test tests/goldfish/scheme/ +``` + +## 说明 + +本次改动的目标,是让 Goldfish 在执行一段代码时,如果碰到“函数名未绑定”,能够更准确地区分下面几种情况: + +- 函数其实存在,只是还没有导入对应的库 +- 函数在多个库里都存在,用户需要先判断自己想用哪个库 +- 当前可见的 `*load-path*` 中根本找不到这个函数 + +为此,本次新增了一条独立于 `gf doc` 实现的函数查找逻辑。运行时不会去依赖 `golddoc` 或 `tests/function-library-index.json`,而是直接扫描当前 `*load-path*` 下的 Scheme 库源码文件,读取 `define-library` 和 `export` 信息,判断某个函数名被哪些库导出。 + +这条逻辑对外暴露为: + +```scheme +(g_function-libraries "string-split") +``` + +返回值是一个列表,例如: + +```scheme +((liii string)) +``` + +如果一个函数在多个库中导出,则会返回多个库名;如果没有找到,则返回空列表。 + +在报错提示层面,本次统一了多条执行链的行为: + +- `gf eval` +- `gf repl` +- `gf load` +- `gf test` +- `gf doc` +- `gf source` + +现在这些路径在处理 Scheme 报错时,都会先尝试识别 `unbound variable ...` 是否对应“缺失函数调用”,然后追加更合适的 hint。 + +具体行为如下: + +### 1. 只在一个库中找到 + +提示用户该函数存在于哪个库,并直接给出建议的 `import` 语句。 + +### 2. 在多个库中找到 + +提示该函数存在于多个可见库中,并给出多条: + +```bash +gf doc 库 函数 +``` + +形式的命令,让用户逐个查看文档和用例,自己判断该选哪个库。 + +### 3. 完全找不到 + +提示用户先调用: + +```bash +gf doc 目标函数 +``` + +并明确说明: + +- `gf doc` 有可能返回命名相近的函数 +- 如果没有任何命名相近的函数,就建议使用 `git grep` 等工具搜索代码库,或者自己实现该函数,或者放弃使用 + +## 额外修复 + +在这次整理过程中,还顺手修复了 `gf source` 的一个问题。 + +`tools/goldsource/liii/goldsource-cli.scm` 内部直接调用了 `path-read-text`,但之前没有导入 `(liii path)`,导致: + +```bash +./bin/gf source liii/path +``` + +会错误地报出 `path-read-text` 未绑定。现在已经补上该导入,`gf source` 会正常输出目标库的源码。 + +## 2026/04/03 缺失函数提示增强,并补齐 REPL 与 source 场景 diff --git a/src/goldfish.hpp b/src/goldfish.hpp index c117294c..34320f65 100644 --- a/src/goldfish.hpp +++ b/src/goldfish.hpp @@ -95,6 +95,15 @@ using nlohmann::json; inline void glue_define (s7_scheme* sc, const char* name, const char* desc, s7_function f, s7_int required, s7_int optional); +static s7_pointer +f_function_libraries (s7_scheme* sc, s7_pointer args); + +static bool +split_library_query (const string& query, string& group, string& library); + +static vector +find_function_libraries_in_load_path (s7_scheme* sc, const string& function_name); + static const char* NJSON_HANDLE_TAG = "njson-handle"; struct NjsonState { std::thread::id owner_thread_id; @@ -2114,16 +2123,23 @@ inline void glue_goldfish (s7_scheme* sc) { s7_pointer cur_env= s7_curlet (sc); - const char* s_version = "version"; - const char* d_version = "(version) => string"; - const char* s_delete_file= "g_delete-file"; - const char* d_delete_file= "(g_delete-file string) => boolean"; + const char* s_version = "version"; + const char* d_version = "(version) => string"; + const char* s_delete_file = "g_delete-file"; + const char* d_delete_file = "(g_delete-file string) => boolean"; + const char* s_function_libraries= "g_function-libraries"; + const char* d_function_libraries= + "(g_function-libraries function-name) => list, returns visible library names such as '((liii string)) that " + "export function-name in the current *load-path*"; s7_define (sc, cur_env, s7_make_symbol (sc, s_version), s7_make_typed_function (sc, s_version, f_version, 0, 0, false, d_version, NULL)); s7_define (sc, cur_env, s7_make_symbol (sc, s_delete_file), s7_make_typed_function (sc, s_delete_file, f_delete_file, 1, 0, false, d_delete_file, NULL)); + + s7_define (sc, cur_env, s7_make_symbol (sc, s_function_libraries), + s7_make_typed_function (sc, s_function_libraries, f_function_libraries, 1, 0, false, d_function_libraries, NULL)); } // old `f_current_second` TODO: use std::chrono::tai_clock::now() when using C++ 20 @@ -3463,7 +3479,68 @@ goldfish_extract_scheme_path_from_error (const string& errmsg) { } static string -goldfish_extract_unbound_function_name_from_error (const string& errmsg) { +goldfish_extract_error_expression (const string& errmsg, size_t search_start) { + const string infix= " in "; + size_t start= errmsg.find (infix, search_start); + if (start == string::npos) { + return ""; + } + + start += infix.size (); + size_t end= errmsg.find ('\n', start); + if (end == string::npos) { + end= errmsg.size (); + } + + return errmsg.substr (start, end - start); +} + +static bool +goldfish_form_contains_called_symbol (s7_scheme* sc, s7_pointer form, const string& function_name) { + if (s7_is_pair (form)) { + s7_pointer operator_form= s7_car (form); + if (s7_is_symbol (operator_form) && (function_name == s7_symbol_name (operator_form))) { + return true; + } + + for (s7_pointer iter= form; s7_is_pair (iter); iter= s7_cdr (iter)) { + if (goldfish_form_contains_called_symbol (sc, s7_car (iter), function_name)) { + return true; + } + } + + s7_pointer tail= form; + while (s7_is_pair (tail)) { + tail= s7_cdr (tail); + } + if ((!s7_is_null (sc, tail)) && goldfish_form_contains_called_symbol (sc, tail, function_name)) { + return true; + } + } + + return false; +} + +static bool +goldfish_error_expression_contains_function_call (s7_scheme* sc, const string& expression, const string& function_name) { + if (expression.empty ()) { + return false; + } + + s7_pointer port = s7_open_input_string (sc, expression.c_str ()); + s7_pointer eof_object= s7_eof_object (sc); + s7_pointer form = s7_read (sc, port); + s7_close_input_port (sc, port); + + if ((form == eof_object) || (!form)) { + return expression.find ("(" + function_name) != string::npos; + } + + return goldfish_form_contains_called_symbol (sc, form, function_name); +} + +static string +goldfish_extract_unbound_function_name_from_error (s7_scheme* sc, const string& errmsg) { const string prefix= "unbound variable "; size_t start = errmsg.find (prefix); if (start == string::npos) { @@ -3485,7 +3562,12 @@ goldfish_extract_unbound_function_name_from_error (const string& errmsg) { } string function_name= errmsg.substr (start, end - start); - if (errmsg.find ("in (" + function_name, end) == string::npos) { + if (errmsg.find ("in (" + function_name, end) != string::npos) { + return function_name; + } + + string error_expression= goldfish_extract_error_expression (errmsg, end); + if (!goldfish_error_expression_contains_function_call (sc, error_expression, function_name)) { return ""; } @@ -3519,12 +3601,63 @@ goldfish_format_scheme_error_message (const char* errmsg) { } static string -goldfish_append_doc_hint_if_needed (const string& errmsg) { +goldfish_shell_double_quote (const string& value) { + string quoted= "\""; + for (char ch : value) { + switch (ch) { + case '\\': + quoted += "\\\\"; + break; + case '"': + quoted += "\\\""; + break; + case '$': + quoted += "\\$"; + break; + case '`': + quoted += "\\`"; + break; + default: + quoted += ch; + break; + } + } + quoted += "\""; + return quoted; +} + +static string +goldfish_library_display_name (const string& library_query) { + string group; + string library; + if (!split_library_query (library_query, group, library)) { + return library_query; + } + return "(" + group + " " + library + ")"; +} + +static string +goldfish_library_import_form (const string& library_query) { + string group; + string library; + if (!split_library_query (library_query, group, library)) { + return ""; + } + return "(import (" + group + " " + library + "))"; +} + +static string +goldfish_library_doc_command (const string& library_query, const string& function_name) { + return goldfish_cli_program_name () + " doc " + library_query + " " + goldfish_shell_double_quote (function_name); +} + +static string +goldfish_append_doc_hint_if_needed (s7_scheme* sc, const string& errmsg) { if (errmsg.find ("Hint: try `") != string::npos) { return errmsg; } - string function_name= goldfish_extract_unbound_function_name_from_error (errmsg); + string function_name= goldfish_extract_unbound_function_name_from_error (sc, errmsg); if (function_name.empty ()) { return errmsg; } @@ -3533,15 +3666,73 @@ goldfish_append_doc_hint_if_needed (const string& errmsg) { if ((!formatted.empty ()) && (formatted.back () != '\n')) { formatted += '\n'; } - formatted += "Hint: try `" + goldfish_cli_program_name () + " doc \"" + function_name + - "\"` to look up related documentation.\n"; + + vector library_queries; + try { + library_queries= find_function_libraries_in_load_path (sc, function_name); + } + catch (const std::exception&) { + library_queries.clear (); + } + + if (library_queries.empty ()) { + formatted += "Hint: try `" + goldfish_cli_program_name () + " doc " + goldfish_shell_double_quote (function_name) + + "`\n"; + formatted += "`" + goldfish_cli_program_name () + " doc` may show similarly named functions when there is no exact match.\n"; + formatted += "If it finds nothing similar, try searching the codebase with `git grep " + + goldfish_shell_double_quote (function_name) + + "`, implement that function yourself, or stop using it.\n"; + return formatted; + } + + if (library_queries.size () == 1) { + string import_form= goldfish_library_import_form (library_queries.front ()); + formatted += "Hint: function `" + function_name + "` exists in library `" + + goldfish_library_display_name (library_queries.front ()) + "`.\n"; + if (!import_form.empty ()) { + formatted += "Please import that library first: `" + import_form + "`.\n"; + } + return formatted; + } + + formatted += "Hint: function `" + function_name + "` exists in multiple visible libraries:\n"; + for (const auto& library_query : library_queries) { + formatted += " " + goldfish_library_display_name (library_query) + "\n"; + } + formatted += "Try one of these commands to decide which library to use:\n"; + for (const auto& library_query : library_queries) { + formatted += " " + goldfish_library_doc_command (library_query, function_name) + "\n"; + } return formatted; } static void -goldfish_print_scheme_error_message (const char* errmsg) { +goldfish_render_scheme_error_message (s7_scheme* sc, const char* errmsg, string& rendered) { + rendered= goldfish_append_doc_hint_if_needed (sc, goldfish_format_scheme_error_message (errmsg)); + if ((!rendered.empty ()) && (rendered.back () != '\n')) { + rendered += '\n'; + } +} + +static void +goldfish_print_scheme_error_message (s7_scheme* sc, const char* errmsg) { + if ((errmsg) && (*errmsg)) { + string rendered; + goldfish_render_scheme_error_message (sc, errmsg, rendered); + cout << rendered; + } +} + +static void +goldfish_print_prefixed_scheme_error_message (s7_scheme* sc, const string& prefix, const char* errmsg) { if ((errmsg) && (*errmsg)) { - cout << goldfish_append_doc_hint_if_needed (goldfish_format_scheme_error_message (errmsg)); + string rendered; + goldfish_render_scheme_error_message (sc, errmsg, rendered); + cerr << prefix; + if ((!prefix.empty ()) && (prefix.back () != '\n')) { + cerr << '\n'; + } + cerr << rendered; } } @@ -4011,7 +4202,9 @@ ic_goldfish_eval (s7_scheme* sc, const char* code) { const char* errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); if (errmsg && *errmsg) { - ic_printf ("[error]%s[/]\n", errmsg); // 美化输出 + string rendered; + goldfish_render_scheme_error_message (sc, errmsg, rendered); + ic_printf ("[error]%s[/]", rendered.c_str ()); } if (result) { history_values.push_back (result); @@ -4562,6 +4755,258 @@ append_load_path_entries (s7_scheme* sc, const vector& append_dirs) { } } +static bool +append_unique_exact_string (vector& items, const string& value) { + if (value.empty ()) return false; + if (std::find (items.begin (), items.end (), value) != items.end ()) return false; + items.push_back (value); + return true; +} + +static bool +string_is_decimal_integer (const string& value) { + if (value.empty ()) return false; + + size_t index= 0; + if (value[0] == '+' || value[0] == '-') { + index= 1; + } + if (index >= value.size ()) return false; + + return std::all_of (value.begin () + static_cast (index), value.end (), + [] (unsigned char ch) { return std::isdigit (ch) != 0; }); +} + +static s7_pointer +make_library_name_part (s7_scheme* sc, const string& part) { + if (string_is_decimal_integer (part)) { + try { + return s7_make_integer (sc, std::stoll (part)); + } + catch (const std::exception&) { + } + } + return s7_make_symbol (sc, part.c_str ()); +} + +static bool +split_library_query (const string& query, string& group, string& library) { + size_t slash_pos= query.find ('/'); + if (slash_pos == string::npos || slash_pos == 0 || slash_pos == query.size () - 1) { + return false; + } + if (query.find ('/', slash_pos + 1) != string::npos) { + return false; + } + group = query.substr (0, slash_pos); + library= query.substr (slash_pos + 1); + return true; +} + +static bool +is_named_symbol (s7_pointer value, const char* name) { + return s7_is_symbol (value) && (strcmp (s7_symbol_name (value), name) == 0); +} + +static bool +library_name_part_to_string (s7_pointer value, string& out) { + if (s7_is_symbol (value)) { + out= s7_symbol_name (value); + return true; + } + if (s7_is_integer (value)) { + out= std::to_string (s7_integer (value)); + return true; + } + return false; +} + +static bool +extract_library_name_from_form (s7_scheme* sc, s7_pointer library_name_form, string& group, string& library) { + if ((!s7_is_list (sc, library_name_form)) || (s7_list_length (sc, library_name_form) != 2)) { + return false; + } + + return library_name_part_to_string (s7_car (library_name_form), group) && + library_name_part_to_string (s7_cadr (library_name_form), library); +} + +static bool +export_spec_name_matches (s7_scheme* sc, s7_pointer export_spec, const string& function_name) { + if (s7_is_symbol (export_spec)) { + return function_name == s7_symbol_name (export_spec); + } + + if (s7_is_list (sc, export_spec) && (s7_list_length (sc, export_spec) == 3) && is_named_symbol (s7_car (export_spec), "rename")) { + string renamed_export; + if (library_name_part_to_string (s7_caddr (export_spec), renamed_export)) { + return function_name == renamed_export; + } + } + + return false; +} + +static bool +define_library_form_exports_function (s7_scheme* sc, s7_pointer form, const string& function_name, string& group, string& library) { + if ((!s7_is_list (sc, form)) || s7_is_null (sc, form) || (!is_named_symbol (s7_car (form), "define-library"))) { + return false; + } + + string form_group; + string form_library; + if (!extract_library_name_from_form (sc, s7_cadr (form), form_group, form_library)) { + return false; + } + + for (s7_pointer declarations= s7_cddr (form); s7_is_pair (declarations); declarations= s7_cdr (declarations)) { + s7_pointer declaration= s7_car (declarations); + if ((!s7_is_list (sc, declaration)) || s7_is_null (sc, declaration) || (!is_named_symbol (s7_car (declaration), "export"))) { + continue; + } + + for (s7_pointer export_specs= s7_cdr (declaration); s7_is_pair (export_specs); export_specs= s7_cdr (export_specs)) { + if (export_spec_name_matches (sc, s7_car (export_specs), function_name)) { + group = form_group; + library= form_library; + return true; + } + } + } + + return false; +} + +static bool +source_file_exports_function (s7_scheme* sc, const fs::path& source_file, const string& function_name, string& group, string& library) { + string source_text= read_text_file_exact (source_file); + s7_pointer port = s7_open_input_string (sc, source_text.c_str ()); + s7_pointer eof_object = s7_eof_object (sc); + + while (true) { + s7_pointer form= s7_read (sc, port); + if (form == eof_object) break; + if (define_library_form_exports_function (sc, form, function_name, group, library)) { + s7_close_input_port (sc, port); + return true; + } + } + + s7_close_input_port (sc, port); + return false; +} + +static s7_pointer +make_library_name_list (s7_scheme* sc, const string& group, const string& library) { + return s7_list (sc, 2, make_library_name_part (sc, group), make_library_name_part (sc, library)); +} + +static vector +sorted_child_directories (const fs::path& root) { + vector directories; + std::error_code ec; + + for (fs::directory_iterator it (root, fs::directory_options::skip_permission_denied, ec), end; it != end; it.increment (ec)) { + if (ec) { + ec.clear (); + continue; + } + if (it->is_directory (ec)) { + directories.push_back (it->path ()); + } + ec.clear (); + } + + std::sort (directories.begin (), directories.end (), + [] (const fs::path& lhs, const fs::path& rhs) { return lhs.string () < rhs.string (); }); + return directories; +} + +static vector +sorted_scheme_source_files (const fs::path& dir) { + vector files; + std::error_code ec; + + for (fs::directory_iterator it (dir, fs::directory_options::skip_permission_denied, ec), end; it != end; it.increment (ec)) { + if (ec) { + ec.clear (); + continue; + } + if (it->is_regular_file (ec) && (it->path ().extension () == ".scm")) { + files.push_back (it->path ()); + } + ec.clear (); + } + + std::sort (files.begin (), files.end (), [] (const fs::path& lhs, const fs::path& rhs) { return lhs.string () < rhs.string (); }); + return files; +} + +static vector +find_function_libraries_in_load_path (s7_scheme* sc, const string& function_name) { + vector library_queries; + std::error_code ec; + + for (const auto& load_root_string : current_load_path_entries (sc)) { + fs::path load_root (load_root_string); + if ((!fs::exists (load_root, ec)) || (!fs::is_directory (load_root, ec))) { + ec.clear (); + continue; + } + ec.clear (); + + for (const auto& group_dir : sorted_child_directories (load_root)) { + for (const auto& source_file : sorted_scheme_source_files (group_dir)) { + string group; + string library; + if (source_file_exports_function (sc, source_file, function_name, group, library)) { + append_unique_exact_string (library_queries, group + "/" + library); + } + } + } + } + + std::sort (library_queries.begin (), library_queries.end ()); + return library_queries; +} + +static s7_pointer +make_library_name_list_list (s7_scheme* sc, const vector& library_queries) { + s7_pointer result= s7_nil (sc); + for (auto it= library_queries.rbegin (); it != library_queries.rend (); ++it) { + string group; + string library; + if (!split_library_query (*it, group, library)) continue; + result= s7_cons (sc, make_library_name_list (sc, group, library), result); + } + return result; +} + +static s7_pointer +f_function_libraries (s7_scheme* sc, s7_pointer args) { + s7_pointer function_name_arg= s7_car (args); + if (!s7_is_string (function_name_arg)) { + return s7_error (sc, s7_make_symbol (sc, "type-error"), + s7_list (sc, 2, s7_make_string (sc, "g_function-libraries: function-name must be string?"), + function_name_arg)); + } + + string function_name= s7_string (function_name_arg); + vector visible_library_queries; + + try { + visible_library_queries= find_function_libraries_in_load_path (sc, function_name); + } + catch (const std::exception& ex) { + return s7_error (sc, s7_make_symbol (sc, "read-error"), + s7_list (sc, 2, + s7_make_string (sc, (string ("g_function-libraries: failed to inspect libraries: ") + ex.what ()).c_str ()), + function_name_arg)); + } + + return make_library_name_list_list (sc, visible_library_queries); +} + static StartupCliOptions parse_startup_cli_options (int argc, char** argv) { StartupCliOptions opts; @@ -4685,7 +5130,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { if (goldfix_opts.enabled) { int fix_ret= goldfish_run_fix_mode (sc, gf_lib, goldfix_opts); errmsg = s7_get_output_string (sc, s7_current_error_port (sc)); - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4744,7 +5189,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } goldfish_eval_code (sc, code); errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4786,7 +5231,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { goldfish_eval_file (sc, file, true); errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); if ((errmsg) && (*errmsg)) { - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4796,8 +5241,9 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { #ifdef GOLDFISH_WITH_REPL errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); if ((errmsg) && (*errmsg)) { - string formatted_errmsg= goldfish_format_scheme_error_message (errmsg); - ic_printf ("[red]%s[/]\n", formatted_errmsg.c_str ()); + string rendered; + goldfish_render_scheme_error_message (sc, errmsg, rendered); + ic_printf ("[red]%s[/]", rendered.c_str ()); } s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); @@ -4819,8 +5265,9 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { #ifdef GOLDFISH_WITH_REPL errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); if ((errmsg) && (*errmsg)) { - string formatted_errmsg= goldfish_format_scheme_error_message (errmsg); - ic_printf ("[red]%s[/]\n", formatted_errmsg.c_str ()); + string rendered; + goldfish_render_scheme_error_message (sc, errmsg, rendered); + ic_printf ("[red]%s[/]", rendered.c_str ()); } s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); @@ -4861,7 +5308,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } errmsg = s7_get_output_string (sc, s7_current_error_port (sc)); if ((errmsg) && (*errmsg)) { - cerr << "Error importing (liii goldtest): " << errmsg << endl; + goldfish_print_prefixed_scheme_error_message (sc, "Error importing (liii goldtest):", errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4879,7 +5326,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } s7_pointer result = s7_call (sc, main_func, s7_nil (sc)); errmsg = s7_get_output_string (sc, s7_current_error_port (sc)); - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4911,7 +5358,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } errmsg = s7_get_output_string (sc, s7_current_error_port (sc)); if ((errmsg) && (*errmsg)) { - cerr << "Error importing (liii golddoc): " << errmsg << endl; + goldfish_print_prefixed_scheme_error_message (sc, "Error importing (liii golddoc):", errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4928,7 +5375,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } s7_pointer result = s7_call (sc, main_func, s7_nil (sc)); errmsg = s7_get_output_string (sc, s7_current_error_port (sc)); - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4960,7 +5407,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } errmsg = s7_get_output_string (sc, s7_current_error_port (sc)); if ((errmsg) && (*errmsg)) { - cerr << "Error importing (liii goldsource): " << errmsg << endl; + goldfish_print_prefixed_scheme_error_message (sc, "Error importing (liii goldsource):", errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -4977,7 +5424,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } s7_pointer result = s7_call (sc, main_func, s7_nil (sc)); errmsg = s7_get_output_string (sc, s7_current_error_port (sc)); - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -5037,7 +5484,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); if ((errmsg) && (*errmsg)) { - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -5058,7 +5505,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { s7_call (sc, main_func, s7_nil (sc)); errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); @@ -5071,7 +5518,7 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { if (fs::exists (command, ec) && fs::is_regular_file (command, ec)) { goldfish_eval_file (sc, command, true); errmsg= s7_get_output_string (sc, s7_current_error_port (sc)); - goldfish_print_scheme_error_message (errmsg); + goldfish_print_scheme_error_message (sc, errmsg); s7_close_output_port (sc, s7_current_error_port (sc)); s7_set_current_error_port (sc, old_port); if (gc_loc != -1) s7_gc_unprotect_at (sc, gc_loc); diff --git a/tests/goldfish/scheme/doc-hint-test.scm b/tests/goldfish/scheme/doc-hint-test.scm index 5ec4dfc3..a81c1299 100644 --- a/tests/goldfish/scheme/doc-hint-test.scm +++ b/tests/goldfish/scheme/doc-hint-test.scm @@ -28,29 +28,128 @@ ) ;define (when (not (os-windows?)) - (let ((output-path (path-join (path-temp-dir) - (string-append "gf-doc-hint-" - (number->string (getpid)) - ".log")))) - (path-unlink output-path #t) + (let ((unknown-output-path (path-join (path-temp-dir) + (string-append "gf-doc-hint-unknown-" + (number->string (getpid)) + ".log"))) + (single-library-output-path (path-join (path-temp-dir) + (string-append "gf-doc-hint-import-" + (number->string (getpid)) + ".log"))) + (multiple-library-output-path (path-join (path-temp-dir) + (string-append "gf-doc-hint-multi-" + (number->string (getpid)) + ".log"))) + (nested-library-output-path (path-join (path-temp-dir) + (string-append "gf-doc-hint-nested-" + (number->string (getpid)) + ".log"))) + (repl-output-path (path-join (path-temp-dir) + (string-append "gf-doc-hint-repl-" + (number->string (getpid)) + ".log")))) + (path-unlink unknown-output-path #t) + (path-unlink single-library-output-path #t) + (path-unlink multiple-library-output-path #t) + (path-unlink nested-library-output-path #t) + (path-unlink repl-output-path #t) (dynamic-wind (lambda () #f) (lambda () (check (not (= (run-shell-command (string-append (executable) " eval \\\"(foo 1)\\\" > " - (path->string output-path) + (path->string unknown-output-path) " 2>&1")) 0)) => #t) - (let ((output (path-read-text output-path))) - (check-true (string-contains? output "unbound variable foo in (foo 1)")) - (check-true (string-contains? output - "Hint: try `gf doc \"foo\"` to look up related documentation.")) + (check (not (= (run-shell-command + (string-append (executable) + " eval \\\"(path-join \\\\\\\"a\\\\\\\" \\\\\\\"b\\\\\\\")\\\" > " + (path->string single-library-output-path) + " 2>&1")) + 0)) + => #t) + (check (not (= (run-shell-command + (string-append (executable) + " eval \\\"(remove 1)\\\" > " + (path->string multiple-library-output-path) + " 2>&1")) + 0)) + => #t) + (check (not (= (run-shell-command + (string-append (executable) + " eval \\\"((display (path-read-text \\\\\\\"/tmp/demo\\\\\\\")) 0)\\\" > " + (path->string nested-library-output-path) + " 2>&1")) + 0)) + => #t) + (check (= (run-shell-command + (string-append "printf '(path-join \\\"a\\\" \\\"b\\\")\\n,q\\n' | " + (executable) + " repl > " + (path->string repl-output-path) + " 2>&1")) + 0) + => #t) + (let ((unknown-output (path-read-text unknown-output-path)) + (single-library-output (path-read-text single-library-output-path)) + (multiple-library-output (path-read-text multiple-library-output-path)) + (nested-library-output (path-read-text nested-library-output-path)) + (repl-output (path-read-text repl-output-path))) + (check-true (string-contains? unknown-output "unbound variable foo in (foo 1)")) + (check-true (string-contains? unknown-output + "Hint: try `gf doc \"foo\"`")) + (check-true (string-contains? unknown-output + "`gf doc` may show similarly named functions when there is no exact match.")) + (check-true (string-contains? unknown-output + "If it finds nothing similar, try searching the codebase with `git grep \"foo\"`, implement that function yourself, or stop using it.")) + + (check-true (string-contains? single-library-output + "unbound variable path-join in (path-join \"a\" \"b\")")) + (check-true (string-contains? single-library-output + "Hint: function `path-join` exists in library `(liii path)`.")) + (check-true (string-contains? single-library-output + "Please import that library first: `(import (liii path))`.")) + + (check-true (string-contains? multiple-library-output + "unbound variable remove in (remove 1)")) + (check-true (string-contains? multiple-library-output + "Hint: function `remove` exists in multiple visible libraries:")) + (check-true (string-contains? multiple-library-output + " (liii list)")) + (check-true (string-contains? multiple-library-output + " (liii os)")) + (check-true (string-contains? multiple-library-output + "Try one of these commands to decide which library to use:")) + (check-true (string-contains? multiple-library-output + "gf doc liii/list \"remove\"")) + (check-true (string-contains? multiple-library-output + "gf doc liii/os \"remove\"")) + + (check-true (string-contains? nested-library-output + "unbound variable path-read-text")) + (check-true (string-contains? nested-library-output + "((display (path-read-text \"/tmp/demo\")) 0)")) + (check-true (string-contains? nested-library-output + "Hint: function `path-read-text` exists in library `(liii path)`.")) + (check-true (string-contains? nested-library-output + "Please import that library first: `(import (liii path))`.")) + + (check-true (string-contains? repl-output + "unbound variable path-join in (path-join \"a\" \"b\")")) + (check-true (string-contains? repl-output + "Hint: function `path-join` exists in library `(liii path)`.")) + (check-true (string-contains? repl-output + "Please import that library first: `(import (liii path))`.")) ) ;let ) ;lambda (lambda () - (path-unlink output-path #t) + (path-unlink unknown-output-path #t) + (path-unlink single-library-output-path #t) + (path-unlink multiple-library-output-path #t) + (path-unlink nested-library-output-path #t) + (path-unlink repl-output-path #t) ) ;lambda ) ;dynamic-wind ) ;let diff --git a/tests/goldfish/scheme/function-libraries-test.scm b/tests/goldfish/scheme/function-libraries-test.scm new file mode 100644 index 00000000..79d5b1b1 --- /dev/null +++ b/tests/goldfish/scheme/function-libraries-test.scm @@ -0,0 +1,118 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii check) + (liii os) + (liii path) + (liii string) +) ;import + +(check-set-mode! 'report-failed) + +(define (cleanup-function-libraries-fixture base-root) + (let ((load-root (path-join base-root "goldfish")) + (hidden-root (path-join base-root "hidden-goldfish"))) + (path-unlink (path-join load-root "liii" "alpha.scm") #t) + (path-unlink (path-join load-root "custom" "beta.scm") #t) + (path-unlink (path-join load-root "srfi" "1.scm") #t) + (path-unlink (path-join hidden-root "liii" "missing.scm") #t) + (if (path-dir? (path-join load-root "liii")) + (path-rmdir (path-join load-root "liii")) + #f + ) ;if + (if (path-dir? (path-join load-root "custom")) + (path-rmdir (path-join load-root "custom")) + #f + ) ;if + (if (path-dir? (path-join load-root "srfi")) + (path-rmdir (path-join load-root "srfi")) + #f + ) ;if + (if (path-dir? (path-join hidden-root "liii")) + (path-rmdir (path-join hidden-root "liii")) + #f + ) ;if + (if (path-dir? hidden-root) + (path-rmdir hidden-root) + #f + ) ;if + (if (path-dir? load-root) + (path-rmdir load-root) + #f + ) ;if + (if (path-dir? base-root) + (path-rmdir base-root) + #f + ) ;if + ) ;let +) ;define + +(let* ((base-root (path-join (path-temp-dir) + (string-append "goldfish-function-libraries-" + (number->string (getpid))))) + (load-root (path-join base-root "goldfish")) + (hidden-root (path-join base-root "hidden-goldfish")) + (liii-root (path-join load-root "liii")) + (custom-root (path-join load-root "custom")) + (srfi-root (path-join load-root "srfi")) + (hidden-liii-root (path-join hidden-root "liii")) + (old-load-path *load-path*)) + (cleanup-function-libraries-fixture base-root) + (mkdir (path->string base-root)) + (mkdir (path->string load-root)) + (mkdir (path->string hidden-root)) + (mkdir (path->string liii-root)) + (mkdir (path->string custom-root)) + (mkdir (path->string srfi-root)) + (mkdir (path->string hidden-liii-root)) + (path-write-text + (path-join liii-root "alpha.scm") + "(define-library (liii alpha)\n (export unique-func shared-func duplicate-func)\n (import (scheme base))\n (begin))\n" + ) ;path-write-text + (path-write-text + (path-join custom-root "beta.scm") + "(define-library (custom beta)\n (export shared-func (rename beta-hidden renamed-func))\n (import (scheme base))\n (begin (define beta-hidden 1))\n)\n" + ) ;path-write-text + (path-write-text + (path-join srfi-root "1.scm") + "(define-library (srfi 1)\n (export fold)\n (import (scheme base))\n (begin))\n" + ) ;path-write-text + (path-write-text + (path-join hidden-liii-root "missing.scm") + "(define-library (liii missing)\n (export invisible-func)\n (import (scheme base))\n (begin))\n" + ) ;path-write-text + (dynamic-wind + (lambda () + (set! *load-path* (list (path->string load-root))) + ) ;lambda + (lambda () + (check (g_function-libraries "unique-func") => '((liii alpha))) + (check (g_function-libraries "shared-func") => '((custom beta) (liii alpha))) + (check (g_function-libraries "fold") => '((srfi 1))) + (check (g_function-libraries "renamed-func") => '((custom beta))) + (check (g_function-libraries "invisible-func") => '()) + (check (g_function-libraries "missing-func") => '()) + (check (g_function-libraries "duplicate-func") => '((liii alpha))) + (check-catch 'type-error (g_function-libraries 1)) + ) ;lambda + (lambda () + (set! *load-path* old-load-path) + (cleanup-function-libraries-fixture base-root) + ) ;lambda + ) ;dynamic-wind +) ;let* + +(check-report)