Skip to content

fix(rust): resolve use paths inside the importing crate - #2943

Merged
DeusData merged 2 commits into
mainfrom
fix/rust-workspace-relative-imports
Nov 10, 2026
Merged

DeusData merged 2 commits into
mainfrom
fix/rust-workspace-relative-imports

Conversation

@DeusData

@DeusData DeusData commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Problem

In a Cargo workspace, Rust use declarations were bound to the wrong crate, and imports of external crates were bound to unrelated project symbols:

  1. A relative import such as use crate::util::helper; or use super::proto::encode; in crate beta bound alpha's util::helper / net::proto::encode whenever both crates define the item, simply because alpha sorts first. On jdx/mise (22 packages) 720 Rust IMPORTS crossed into another workspace crate this way on main (1,768 with fix(rust): expand grouped use imports #2932), and 259 of 927 import_map CALLS followed them (528 of 2,303 with fix(rust): expand grouped use imports #2932).
  2. An import whose item is no graph node bound a same-named project symbol: use serde_json::json; bound the project's own json function (and every json! call through import_map), use meili_snap::snapshot; (a macro of a member crate) bound FileStore::snapshot, use std::env; in a build script bound a benchmark fixture's env, and a path segment above the item (use crate::util::Gone;, no Gone in the crate) bound a function named util. 1,123 such IMPORTS on mise main, 513 on meilisearch.

Root cause

resolve_import_strategies (src/pipeline/pass_pkgmap.c) has no Rust branch:

  • Strategy 1 (cbm_pipeline_resolve_module) never matches a :: path, so every Rust import falls through to the name guesses.
  • Strategy 3, the symbol-name fallback, looks the last path segment and then every enclosing segment up in the whole project (cbm_gbuf_find_by_name) and keeps the lexicographically smallest qualified name. Nothing restricts it to the importing crate, nothing tells an external crate from a workspace member, and an enclosing segment (a module name) may bind a function.
  • Strategy 4 strips crate:: / self:: / super:: and resolves the rest from the repository root rather than the crate's module root, ignoring where self / super point.

Both pipeline drivers build the CALLS import map from these IMPORTS edges (cbm_pxc_build_import_map), so import_map CALLS inherited every wrong target.

Fix

For a Rust file under a Cargo.toml (the nearest one is its crate), an import is classified by its first path segment and resolved inside exactly one crate:

  • crate:: is the importing crate's module root: src/ (or tests/, benches/, examples/; a build script is its own root);
  • self:: / super:: are relative to the importing file's module path (a/b.rs and a/b/mod.rs are a::b);
  • a child module of the importing module is that module (2018 paths: mod foo; plus use foo::Bar;);
  • a workspace member named by its crate name (from the existing package map; a package foo-bar is the crate foo_bar) is that member;
  • anything else (std, core, alloc, a dependency) binds no project symbol.

Inside that crate: the item at the module the path names wins; else the module the full path names (use crate::cli;, globs); else a same-named item in a submodule of that module (a re-export from below); else the type an associated item or enum variant belongs to (a segment above the leaf names a module or a type, never a function). Nothing else is guessed. Ties break on module-path fit, then qualified name, so the result is deterministic. Files with no Cargo.toml above them keep the generic strategies unchanged; other languages are untouched.

No new tool, flag or environment variable; no new allocation sites (memory-core count unchanged); Strategy 4's prefix loop moved into a helper (resolve_module_prefixes) that the crate tree reuses, with its old behaviour kept.

Results

Release builds, full index, default workers unless noted. Corpora: jdx/mise v2026.11.2 (43ee02c5b497, 22 packages) and meilisearch 979167f13b5f (27 packages). "main+#2932" is a local merge of #2932's head 0df1074 used only for measurement; this branch is based on main.

jdx/mise

metric main main+#2932 main+fix main+#2932+fix
Rust IMPORTS .rs -> .rs into another crate 1,790 4,622 146 351
.. via crate/self/super only (always wrong) 720 1,768 0 0
IMPORTS bound although every naming use is an external crate 1,123 3,167 0 0
IMPORTS into a crate the caller does not depend on 920 1,402 0 0
import_map CALLS 927 2,303 1,040 2,606
.. into another crate via crate/self/super 259 528 0 0
.. callee named only by an external-crate use 60 284 0 0
.. into a crate the caller does not depend on 12 111 0 0
suffix_match CALLS into a crate the caller does not depend on 6,049 6,112 5,697 5,654
all Rust CALLS into a crate the caller does not depend on 7,406 7,559 7,042 6,988
Rust CALLS total 88,718 89,255 89,434 89,588
index wall (daemon process: start, index, stop), 2 interleaved runs 5.81 / 5.78 s 5.78 / 5.80 s 5.79 / 5.82 s 5.79 / 5.80 s
index worker peak RSS, same 2 runs 763 / 760 MB 767 / 765 MB 760 / 762 MB 759 / 759 MB

meilisearch

metric main main+#2932 main+fix main+#2932+fix
Rust IMPORTS .rs -> .rs into another crate 986 2,670 264 764
.. via crate/self/super only (always wrong) 146 677 0 0
IMPORTS bound although every naming use is an external crate 513 829 0 2 *
IMPORTS into a crate the caller does not depend on 859 1,542 8 * 18 *
import_map CALLS 791 2,427 549 1,749
.. callee named only by an external-crate use 107 150 0 0
.. into a crate the caller does not depend on 108 189 0 2 *
all Rust CALLS into a crate the caller does not depend on 4,203 4,214 3,882 3,734
index wall (daemon process: start, index, stop), 2 interleaved runs 1.75 / 1.76 s 1.74 / 1.74 s 1.75 / 1.74 s 1.74 / 1.74 s
index worker peak RSS, same 2 runs 261 / 263 MB 265 / 264 MB 265 / 261 MB 264 / 261 MB

* classifier residue, checked by hand: imports of r#match (a raw-identifier child module) and member imports through [dependencies.<name>] tables or a re-exported member crate, which the census script does not parse; the resolver binds them correctly.

"Into a crate the caller does not depend on" uses each crate's Cargo.toml dependency tables: such an edge is wrong by construction.

Judged sample. 54 changed edges (36 on mise, 18 on meilisearch), drawn with a fixed seed (20261109) across every class of change (IMPORTS retargeted / removed / added, import_map CALLS retargeted / removed / added, suffix_match retargets by crate membership, new suffix_match edges), each judged against the source: 28 better, 13 wrong before and after (mostly std methods such as as_str, parent, Default::default), 12 worse, 1 unclear. Every IMPORTS and import_map change in the sample was better except the recall losses below. The worse ones are 6 suffix_match retargets and 2 new suffix_match edges (suffix_match ranks by import reachability, which moves with the corrected imports; its share of impossible edges still falls, see the tables) and 4 recall losses (next section). A further seeded check of the 822 import_map CALLS that main+#2932+fix drops relative to main+#2932 on meilisearch found 6 of 6 correct removals (all snapshot! bound to FileStore::snapshot; 538 of the 822 are that macro).

Determinism. Two runs of each fixed binary produce byte-identical graphs (whole-graph canonical dump) on both corpora; IMPORTS are identical between CBM_WORKERS=1 and the default. The sequential and parallel drivers already differ in Rust CALLS on main (mise: 1,070 / 944 edges only in one of them); with the fix 6 import_map edges join that existing gap on mise (correct calls through a now-resolved module import, ini_merge::merge(), that only the sequential driver uses).

Control. helm 9b88efd6 (Go): the whole graph is byte-identical across all four binaries.

Known limits (follow-ups, not in this change)

  • An item a crate re-exports from another crate is not followed: mise's root crate has about 70 facade modules (src/file.rs: pub use mise_util::file::*;), so use crate::file::read no longer binds anything instead of binding mise_util::file::read by name. At most 156 of the 720 relative cross-crate IMPORTS on mise main were right this way (0 of 146 on meilisearch). Following pub use <member>::...::* would recover them.
  • Modules placed by #[path = "..."] and items inside inline mod x { ... } blocks are found only through the file path.
  • A macro invocation can still reach a same-named function through the registry's unique_name fallback, which is not an import binding: on meilisearch 680 snapshot! calls land on FileStore::snapshot that way on main and with this change alike (the 69 import_map ones are gone).

Tests

  • pipeline_rust_workspace_relative_import_stays_in_crate (+ _parallel, which forces the parallel driver): a two-crate workspace where crate::util::helper and super::proto::encode must bind their own crate although the sibling crate has the same paths, serde_json::json / json! and a build script's use std::env must bind nothing, crate::util::Gone must not bind a function util, and alpha_core::util::helper (package alpha-core) must still bind into alpha. Both fail on main.
  • Narrow ASan/UBSan suites, all green: extraction (531), extraction_imports (23), pipeline (456), rust_lsp (522), complexity (9), edge_imports (86), edge_structural (35), parallel (90).

Refs #2922, #2932

In a Cargo workspace a relative `use` bound a sibling crate's item. The
import resolver has no Rust branch, so `crate::util::helper` fell through
to the project-wide symbol-name fallback (Strategy 3), which matches the
last path segment, then every enclosing segment, against every same-named
definition in the project and keeps the lexicographically smallest QN:
whichever crate sorts first. The same fallback bound imports of external
crates to project symbols by name (`use serde_json::json` -> the project's
own `json` function, and every `json!` call through import_map) and let a
module segment bind a function (`crate::util::Gone` -> fn `util`), while
Strategy 4 resolved `crate::`, `self::` and `super::` from the repository
root instead of the crate's module root.

A Rust import of a file under a Cargo.toml is now classified by its first
path segment and resolved inside exactly one crate:
- `crate::` is the importing crate's module root: the nearest Cargo.toml's
  src/ (or tests/, benches/, examples/; a build script is its own root);
- `self::` / `super::` are relative to the importing file's module path;
- a child module of the importing module is that module (2018 paths);
- a workspace member's crate name (a package `foo-bar` is `foo_bar`) is
  that member;
- anything else (std, core, a dependency) binds no project symbol.
Inside the crate the item at the module the path names wins, else the
module the full path names, else a same-named item in a submodule of that
module (a re-export from below), else the type an associated item or enum
variant belongs to. Nothing else is guessed, ties break on module-path fit
then QN, and files with no Cargo.toml above them keep the generic
strategies. The CALLS import map is built from these IMPORTS edges in both
drivers, so import_map CALLS follow.

On jdx/mise this removes all 720 relative IMPORTS that crossed into
another workspace crate, all 1,123 IMPORTS bound through an external-crate
path and all 259 import_map CALLS that followed them; IMPORTS and
import_map CALLS into a crate the caller does not depend on drop from
920 and 12 to 0. Wall time and index worker peak RSS are unchanged; a
non-Rust control graph is byte-identical.

Strategy 4's module-prefix loop moves into resolve_module_prefixes so the
crate tree can reuse its lookup (plus the mod.rs form); Strategy 4's own
behaviour is unchanged.

Refs #2922, #2932

Signed-off-by: Martin Vogel <[email protected]>
@DeusData
DeusData merged commit edfb824 into main Nov 10, 2026
27 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