Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Goldfish Scheme uses subcommands for different operations:
| `repl` | Enter interactive REPL mode |
| `run TARGET` | Run main function from target |
| `test` | Run tests |
| `fix PATH` | Format Scheme code |
| `fix PATH` | Check unmatched parentheses in Scheme code |
| `FILE` | Load and evaluate Scheme file directly |

### Display Help
Expand Down
2 changes: 1 addition & 1 deletion README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ brew uninstall goldfish
| `repl` | 进入交互式 REPL 模式 |
| `run TARGET` | 从目标运行 main 函数 |
| `test` | 运行测试 |
| `fix PATH` | 格式化 Scheme 代码 |
| `fix PATH` | 检查 Scheme 代码中的括号不匹配 |
| `FILE` | 直接加载并求值 Scheme 文件 |

### 显示帮助
Expand Down
133 changes: 133 additions & 0 deletions devel/200_44.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# [200_43]

## 任务相关的代码文件
- src/goldfish.hpp
- tools/goldfix/liii/goldfix.scm
- tools/goldfix/main.scm
- tests/goldfish/liii/goldfix-test.scm
- devel/200_43.md

## 如何测试
```bash
# 构建
xmake b goldfish

# 运行 goldfix 单元测试
bin/gf -I tools/goldfix tests/goldfish/liii/goldfix-test.scm

# 手动验证 fix 只输出括号问题统计
tmpdir=$(mktemp -d)
printf '(define (f x)\n (+ x 1)\n' > "$tmpdir/missing.scm"
printf '(define (f x))\n (+ x 1))\n' > "$tmpdir/extra.scm"
printf '(define (f x)\n (+ x 1))\n' > "$tmpdir/ok.scm"
bin/gf fix "$tmpdir"

# 验证 goldfix 目录已经缩减为最小集合
find tools/goldfix -maxdepth 3 -type f | sort
```

## 2026-03-27 将 `fix` 收敛为仅检查括号数量

### What
本次修改将 Goldfish 的 `fix` 功能彻底收敛为“括号问题检查器”,只保留两项输出:

1. `extra-right-parens`:多出来的右括号 `)` 数量
2. `missing-right-parens`:缺失的右括号 `)` 数量

现在的 `gf fix PATH` 不再做以下事情:

- 不再格式化 Scheme 代码
- 不再修复括号结构
- 不再插入或重写 right-tag
- 不再回写文件
- 不再支持 `--dry-run`

### Why
当前需求很明确:`fix` 只需要回答“多了几个括号、少了几个括号”,其余自动修复和格式化能力都属于多余功能。

继续保留旧的修复流水线会带来两个问题:

1. 用户看到 `fix` 命令名时,很容易误以为它仍然会改写文件
2. 仓库里保留一整套已经不再对外提供的修复逻辑,会提高维护成本,也容易让后续开发误判当前功能边界

因此这次不仅收敛了命令行为,也一并删除了已经不再使用的旧 `goldfix-*` 模块。

### How
主要实现分为三部分:

1. 在 `tools/goldfix/liii/goldfix.scm` 中重写 `(liii goldfix)`,改为最小自包含实现
2. 在 `src/goldfish.hpp` 中将 `gf fix PATH` 的 C++ 入口改成纯检查模式
3. 删除旧的 `tools/goldfix/liii/goldfix-*` 辅助模块,只保留当前真正需要的入口文件

其中,新的 `count-content-paren-issues` 采用逐字符扫描,而不是“按行汇总左右括号数量”的方式。
这样可以正确处理:

- `())(` 这种总数相同但顺序错误的输入
- 字符串中的括号
- 行注释中的括号
- 块注释中的括号
- 字符字面量中的括号

### 结果结构
`(liii goldfix)` 现在导出的核心接口是:

```scheme
(count-content-paren-issues content)
```

返回值为:

```scheme
(extra-right-parens . missing-right-parens)
```

例如:

```scheme
(count-content-paren-issues "())(")
;; => (1 . 1)
```

### CLI 行为变化
现在:

```bash
bin/gf fix PATH
```

只会检查 `PATH`:

- 如果是文件,就输出该文件的括号问题统计
- 如果是目录,就递归检查其中所有 `.scm` 文件,并汇总存在括号问题的文件数量

退出码约定也同步调整为:

- 没有括号问题时返回 `0`
- 存在括号问题时返回 `1`
- 运行失败时返回非零错误码

### 清理结果
本次清理后,`tools/goldfix` 目录只保留:

```text
tools/goldfix/
main.scm
liii/
goldfix.scm
```

之前用于格式化、环境扫描、right-tag 处理、括号修复的旧模块都已经删除。

### 测试覆盖
本次新增或保留的测试覆盖以下场景:

1. 空字符串
2. 括号完全匹配
3. 缺失右括号
4. 多余右括号
5. 顺序敏感场景,例如 `())(` 和 `))((`
6. 字符串中的括号应被忽略
7. 行注释中的括号应被忽略
8. 块注释中的括号应被忽略
9. 字符字面量中的括号应被忽略
10. 多行输入的统计
138 changes: 47 additions & 91 deletions src/goldfish.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3362,9 +3362,8 @@ display_help () {
cout << " version Display version" << endl;
cout << " eval CODE Evaluate Scheme code" << endl;
cout << " load FILE Load Scheme code from FILE, then enter REPL" << endl;
cout << " fix [options] PATH Format PATH (PATH can be a .scm file or directory)" << endl;
cout << " Options:" << endl;
cout << " --dry-run Print formatted result to stdout" << endl;
cout << " fix PATH Check PATH for unmatched parentheses" << endl;
cout << " PATH can be a .scm file or directory" << endl;
cout << " test [options] Run tests (all *-test.scm files under tests/)" << endl;
cout << " Options:" << endl;
cout << " --only PATTERN Run tests matching PATTERN" << endl;
Expand Down Expand Up @@ -3422,16 +3421,10 @@ goldfish_eval_code (s7_scheme* sc, string code) {

struct GoldfixCliOptions {
bool enabled= false;
bool dry_run= false;
string path;
string error;
};

static bool
string_starts_with (const string& value, const string& prefix) {
return value.rfind (prefix, 0) == 0;
}

static GoldfixCliOptions
parse_goldfix_cli_options (int argc, char** argv) {
GoldfixCliOptions opts;
Expand All @@ -3457,15 +3450,6 @@ parse_goldfix_cli_options (int argc, char** argv) {
for (int i= fix_index + 1; i < argc; ++i) {
string arg= argv[i];

if (arg == "--dry-run") {
if (opts.dry_run) {
opts.error= "Error: '--dry-run' can only be specified once.";
return opts;
}
opts.dry_run= true;
continue;
}

// Check for options that start with '-' but are not recognized
if (arg.length () > 0 && arg[0] == '-') {
// This is an unrecognized option, will be caught by invalid flag check
Expand All @@ -3487,22 +3471,6 @@ parse_goldfix_cli_options (int argc, char** argv) {
return opts;
}

static bool
is_goldfix_option_flag (const string& flag) {
return flag == "fix" || flag == "--dry-run" || flag == "dry-run";
}

static vector<string>
filter_invalid_options_for_goldfix (const vector<string>& flags) {
vector<string> filtered;
for (const auto& flag : flags) {
if (!is_goldfix_option_flag (flag)) {
filtered.push_back (flag);
}
}
return filtered;
}

static string
find_goldfix_tool_root (const char* gf_lib) {
std::error_code ec;
Expand Down Expand Up @@ -3566,26 +3534,13 @@ read_text_file_exact (const fs::path& path) {
return buffer.str ();
}

static void
write_text_file_exact (const fs::path& path, const string& content) {
std::ofstream output (path, std::ios::binary | std::ios::trunc);
if (!output.is_open ()) {
throw std::runtime_error ("Failed to open file for writing: " + path.string ());
}

output.write (content.data (), static_cast<std::streamsize> (content.size ()));
if (!output) {
throw std::runtime_error ("Failed to write file: " + path.string ());
}
}

static bool
is_scheme_source_file (const fs::path& path) {
return path.has_extension () && path.extension () == ".scm";
}

static vector<fs::path>
collect_goldfix_targets (const fs::path& input_path, bool dry_run) {
collect_goldfix_targets (const fs::path& input_path) {
std::error_code ec;
if (!fs::exists (input_path, ec)) {
throw std::runtime_error ("Path does not exist: " + input_path.string ());
Expand All @@ -3595,10 +3550,6 @@ collect_goldfix_targets (const fs::path& input_path, bool dry_run) {
return {input_path};
}

if (dry_run) {
throw std::runtime_error ("'fix --dry-run' only supports files.");
}

if (!fs::is_directory (input_path, ec)) {
throw std::runtime_error ("Unsupported path: " + input_path.string ());
}
Expand All @@ -3620,7 +3571,7 @@ collect_goldfix_targets (const fs::path& input_path, bool dry_run) {
}

static s7_pointer
require_goldfix_fix_content (s7_scheme* sc, const char* gf_lib) {
require_goldfix_count_content_paren_issues (s7_scheme* sc, const char* gf_lib) {
string tool_root= find_goldfix_tool_root (gf_lib);
if (tool_root.empty ()) {
throw std::runtime_error ("Goldfix module directory does not exist.");
Expand All @@ -3633,21 +3584,37 @@ require_goldfix_fix_content (s7_scheme* sc, const char* gf_lib) {
throw std::runtime_error ("Failed to import (liii goldfix).");
}

s7_pointer fix_content= s7_name_to_value (sc, "fix-content");
if ((!fix_content) || (!s7_is_procedure (fix_content))) {
throw std::runtime_error ("Failed to resolve fix-content from (liii goldfix).");
s7_pointer count_content_paren_issues= s7_name_to_value (sc, "count-content-paren-issues");
if ((!count_content_paren_issues) || (!s7_is_procedure (count_content_paren_issues))) {
throw std::runtime_error ("Failed to resolve count-content-paren-issues from (liii goldfix).");
}

return fix_content;
return count_content_paren_issues;
}

static string
goldfix_fix_content (s7_scheme* sc, s7_pointer fix_content, const string& content) {
s7_pointer result= s7_call (sc, fix_content, s7_list (sc, 1, s7_make_string (sc, content.c_str ())));
if (!result || !s7_is_string (result)) {
throw std::runtime_error ("(liii goldfix) fix-content did not return a string.");
struct GoldfixParenIssues {
long long extra_right_parens = 0;
long long missing_right_parens= 0;
};

static GoldfixParenIssues
goldfix_count_content_paren_issues (s7_scheme* sc, s7_pointer count_content_paren_issues, const string& content) {
s7_pointer result=
s7_call (sc, count_content_paren_issues, s7_list (sc, 1, s7_make_string (sc, content.c_str ())));
if ((!result) || (!s7_is_pair (result))) {
throw std::runtime_error ("(liii goldfix) count-content-paren-issues did not return a pair.");
}
return string (s7_string (result));

s7_pointer extra = s7_car (result);
s7_pointer missing= s7_cdr (result);
if ((!s7_is_integer (extra)) || (!s7_is_integer (missing))) {
throw std::runtime_error ("(liii goldfix) count-content-paren-issues returned a malformed result.");
}

GoldfixParenIssues issues;
issues.extra_right_parens = static_cast<long long> (s7_integer (extra));
issues.missing_right_parens = static_cast<long long> (s7_integer (missing));
return issues;
}

static string
Expand All @@ -3660,42 +3627,30 @@ goldfix_progress_prefix (std::size_t index, std::size_t total) {
static int
goldfish_run_fix_mode (s7_scheme* sc, const char* gf_lib, const GoldfixCliOptions& opts) {
try {
s7_pointer fix_content= require_goldfix_fix_content (sc, gf_lib);
vector<fs::path> files = collect_goldfix_targets (fs::path (opts.path), opts.dry_run);
std::ostream& status_out= std::cerr;

if (opts.dry_run) {
if (files.empty ()) {
throw std::runtime_error ("No input file provided for 'fix --dry-run'.");
}
const fs::path& file = files.front ();
string prefix= goldfix_progress_prefix (1, 1);
status_out << prefix << " Processing " << file.string () << std::endl;
cout << goldfix_fix_content (sc, fix_content, read_text_file_exact (file));
status_out << prefix << " Dry-run complete " << file.string () << std::endl;
return current_scheme_error_output (sc).empty () ? 0 : -1;
}
s7_pointer count_content_paren_issues= require_goldfix_count_content_paren_issues (sc, gf_lib);
vector<fs::path> files = collect_goldfix_targets (fs::path (opts.path));
std::ostream& status_out = std::cerr;
std::size_t issue_file_count = 0;

std::size_t changed_count= 0;
for (std::size_t i= 0; i < files.size (); ++i) {
const fs::path& file = files[i];
string prefix= goldfix_progress_prefix (i + 1, files.size ());
status_out << prefix << " Processing " << file.string () << std::endl;
status_out << prefix << " Checking " << file.string () << std::endl;

try {
string original= read_text_file_exact (file);
string fixed = goldfix_fix_content (sc, fix_content, original);
GoldfixParenIssues issues=
goldfix_count_content_paren_issues (sc, count_content_paren_issues, read_text_file_exact (file));
if (!current_scheme_error_output (sc).empty ()) {
status_out << prefix << " Failed " << file.string () << std::endl;
return -1;
}
if (fixed != original) {
write_text_file_exact (file, fixed);
++changed_count;
status_out << prefix << " Updated " << file.string () << std::endl;
}
else {
status_out << prefix << " Unchanged " << file.string () << std::endl;

if ((issues.extra_right_parens == 0) && (issues.missing_right_parens == 0)) {
status_out << prefix << " OK " << file.string () << std::endl;
} else {
++issue_file_count;
status_out << prefix << " Mismatch " << file.string () << ": extra-right-parens=" << issues.extra_right_parens
<< ", missing-right-parens=" << issues.missing_right_parens << std::endl;
}
}
catch (const std::exception& err) {
Expand All @@ -3705,9 +3660,10 @@ goldfish_run_fix_mode (s7_scheme* sc, const char* gf_lib, const GoldfixCliOption
}

if (fs::is_directory (fs::path (opts.path))) {
status_out << "Processed " << files.size () << " .scm file(s), updated " << changed_count << std::endl;
status_out << "Processed " << files.size () << " .scm file(s), " << issue_file_count
<< " file(s) with paren mismatches" << std::endl;
}
return 0;
return (issue_file_count == 0) ? 0 : 1;
}
catch (const std::exception& err) {
cerr << err.what () << endl;
Expand Down
Loading
Loading