Skip to content

refactor(discover): port the discover module to Zig - #107

Merged
ata-sesli merged 3 commits into
mainfrom
refactor/discover-zig
Nov 3, 2026
Merged

ata-sesli merged 3 commits into
mainfrom
refactor/discover-zig

Conversation

@ata-sesli

Copy link
Copy Markdown
Member

Summary

This moves the file-discovery module (src/discover/) from C to Zig, following the same approach as the earlier git, simhash and watcher port: the C headers (discover.h, userconfig.h) stay the interface, and the Zig files export the same cbm_* functions. No caller changes. After this PR, src/discover/ has no .c files.

Discovery should behave exactly as before: the same files are found, the same directories are skipped, and .gitignore, .git/info/exclude, .cbmignore and the global excludes file are applied the same way.

Commits

Each commit builds and passes the full test suite on its own.

  1. refactor(foundation): move discover's stat and realpath helpers to compat_fs (C only). Moves the platform-specific file helpers out of discover.c unchanged, as cbm_stat_utf8, cbm_stat_no_follow and cbm_canonical_path in foundation/compat_fs.{h,c}. On Windows they use _wstat64, skip reparse points and call _fullpath; on POSIX they use stat/lstat/realpath. This keeps the Windows #ifdef code in C, so the Zig walk calls one helper on every platform.

  2. refactor(discover): port gitignore, userconfig and language to Zig. Replaces gitignore.c, userconfig.c and language.c with .zig counterparts.

    • The language tables keep their entry order, because lookups stop at the first match.
    • userconfig.zig parses JSON through the same yyjson calls as before, so duplicate keys and parse errors are handled the same way.
    • scripts/generate-lang-code.py now prints Zig table rows.
  3. refactor(discover): port the directory walk to Zig. Replaces discover.c with discover.zig. The port covers:

    • the skip lists and the ignore-file chain;
    • cache-directory pruning and the 512-frame walk stack;
    • resolving the git common dir, the enclosing repository and ancestor ignore files;
    • tracked-file lookup;
    • the CBM_DISCOVER_TEST_FAIL_PHASE/_NTH fault-injection hooks.

    It keeps the C version's fixed buffers and truncation behaviour. Arrays and strings returned to C use the C allocator, so cbm_discover_free and cbm_discover_free_excluded work unchanged.

Build wiring:

  • build.zig and Makefile.cbm drop the C sources and list the Zig files in CBM_ZIG_SRCS.
  • src/cbm_zig.zig runs checkExports on the new modules, so a signature that drifts from its C header fails the build.
  • scripts/license-policy.json and CONTRIBUTING.md point at the new file names.

Differences from the C code

  • cbm_gitignore_match_result is no longer exported. No header declared it; discover.c declared it locally. discover.zig now calls the matcher through a Zig import.
  • cbm_gitignore_merge_dup_hook_for_test is kept as an exported variable. It is a test hook.
  • excluded_strdup fault path: when that injected fault fires, the Zig code frees the copied string. The C code leaked it. This only runs under fault injection.
  • Cache-path respelling: the C code guarded it with a non-Windows #ifdef. The Zig code keeps the same guard as if (!is_windows).

Verification

  • Full test suite with CBM_TEST_TIERS=all (all language tiers on), macOS arm64: 6,447 passed on main (7484e7e) and 6,447 passed after each of the three commits.
  • Old vs new CLI. main and this branch each indexed the same repositories, each into its own cache. The index_repository response (status, excluded dirs and counts), the sorted File paths and the node counts per label were identical in every case. The repositories were:
    • a scratch fixture that covers:

      • nested .gitignore files, including re-included files;
      • .git/info/exclude and .cbmignore;
      • core.excludesFile from ~/.gitconfig and from the XDG config dir;
      • a tracked file inside an ignored dir;
      • a linked worktree;
      • a subfolder without its own .git (exercising enclosing-repository lookup);
      • a symlinked repository root;
      • a plain folder with no git;
      • a cache dir inside the repository, reached both directly and through the symlink.

      It was indexed in full, fast and moderate modes.

    • this repository's source tree and a larger external repository, each in full and fast modes.

  • Table parity. A script extracted all 14 tables from main's C sources and from the Zig sources (extensions, filenames, compound extensions, language names, tiers, user-config language names, skip dirs, suffixes, filenames, patterns, ignored JSON files) and compared them in order. All 14 were identical.
  • Discover-phase time (warm runs): this repository took 7 ms before and 5 ms after. The larger repository took 16–18 ms both before and after.
  • Cross-compile of libcbm_zig.a succeeded for x86_64 and aarch64 Linux (gnu), x86_64 Windows (gnu) and aarch64 macOS, and the discover exports were present in each archive.
  • Header drift check: I temporarily changed a parameter type in discover.h. The build failed with cbm_discover_free: parameter 1 is c_int in Zig but c_long in its C header. The change was then reverted.
  • Lint: clang-format-diff (clang-format 22.1.8) is clean on the changed C lines. zig fmt --check, git diff --check and tests/test_github_workflows.sh all pass.

Limits

  • Windows was only cross-compiled. It was not run, because PR CI runs Linux amd64 only.
  • Stale comments: a few comments still mention src/discover/discover.c. They are in tests/repro/*.c, src/pipeline/pass_pkgmap.c and one line of discover.h. They are left as they are to keep this diff focused on the port.

…mpat_fs

discover.c carried its own Windows code: a wide-char stat, a stat that
skips reparse points, and _fullpath in place of realpath. They move
unchanged into compat_fs as cbm_stat_utf8, cbm_stat_no_follow and
cbm_canonical_path, so discover.c no longer needs win_utf8.h or
platform #ifdefs for them.

This prepares the Zig port of discover: the Zig code can call one
function on every platform, and the Windows code stays in C.

Signed-off-by: ata-sesli <atasesli@protonmail.com>
gitignore.zig, userconfig.zig and language.zig replace their C files and
implement the same discover.h and userconfig.h declarations; the
comptime export check compares each with its header.

- gitignore: glob matching, parsing and the atomic merge are unchanged.
  cbm_gitignore_match_result stays a non-header export for discover.c,
  and the merge test hook stays an exported variable.
- userconfig: extra_extensions parsing still goes through yyjson with
  the same flags, so duplicate keys and the project-over-global dedup
  behave as before.
- language: the extension, filename, compound-extension, interpreter
  and name tables keep their entries and order; the tier switch,
  shebang probe and .m/.cls/.frm disambiguation are ported as written.

generate-lang-code.py now prints language.zig table rows.

Signed-off-by: ata-sesli <atasesli@protonmail.com>
discover.zig replaces discover.c, so src/discover/ has no C left. It
implements the walk part of discover.h with the same exports, checked
against the header at comptime.

Ported as written: the skip, suffix, filename, fast-pattern and
ignored-JSON lists (entry for entry); core.excludesFile and XDG lookup;
the gitignore link chain with nested files, info/exclude and the
enclosing-repository ancestors; tracked-file sets from git ls-files;
cache-tree pruning; the growable walk stack; the degraded flag; and the
CBM_DISCOVER_TEST_FAIL_PHASE/_NTH fault injection. Paths keep the fixed
4 KiB buffers with snprintf truncation, and the file and excluded arrays
stay on the C allocator for cbm_discover_free and
cbm_discover_free_excluded.

One deviation: when the excluded_strdup fault fires, the copied string
is now freed instead of leaked.

cbm_gitignore_match_result was exported only for discover.c; it is now
the plain Zig function gitignore.matchResult. The license-gate ignore
entry for the license-filename list moves to discover.zig.

Signed-off-by: ata-sesli <atasesli@protonmail.com>
@ata-sesli
ata-sesli merged commit ea95a61 into main Nov 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant