Learn/Semantic · 7 min · updated 2026-08-03

Semantic diff for Rust: traits, impls and what a text diff misses

Rust makes text diffs particularly misleading: impl blocks move, trait bounds change meaning without changing shape, and macros hide the real edit. What a structural diff sees instead.

Rust code is unusually badly served by line-based diffs, because so much of its meaning lives in places a line diff cannot see. A trait bound tightens and one line changes, while every caller in the crate is now affected. An impl block moves between files and the diff reports a hundred lines of churn with no change in behaviour.

Four Rust-specific cases

ChangeWhat the text diff showsWhat it means
impl block moved to another moduleLarge delete and addNothing changed for callers, unless visibility changed with it
Trait bound added to a genericOne lineEvery instantiation must now satisfy it
Lifetime elided or namedA few charactersBorrow relationships changed, and downstream code may stop compiling
A function made pub(crate)One wordThe public API shrank

What a structural diff reads

Parsing Rust with tree-sitter gives named nodes for functions, structs, enums, traits, impls and macro invocations. That is enough to identify a function by its owning impl and its signature rather than by its position, which is what makes moves and renames legible. It also makes the dependency question answerable: which functions call this one, which types implement this trait, what does the change reach.

There is a limit worth stating plainly. A syntax tree is not type inference. Tree-sitter knows a call is a call; it does not resolve which impl a method call selects. For questions that need real type resolution, the answer is rust-analyzer or the compiler, and any tool claiming otherwise is overselling. What a syntax-level index gives you is speed, tolerance of code that does not compile, and coverage across languages, which is the right trade for review and impact analysis.

Macros

Macro-generated code is the honest weak spot. A change inside a macro_rules! body can alter every expansion, and the expansions do not exist in the source tree. A structural diff will report the macro definition as changed, which is correct, and cannot enumerate the affected call sites the way it can for a plain function. Treat a macro edit as a wide change by default.

In practice

The Aura engine is written in Rust and indexes Rust the same way it indexes everything else: parse, hash each node, store the graph, compare. On a large crate the first index takes seconds and subsequent edits are incremental. The output is a change list by symbol, with the callers of each changed symbol, which is the shape a reviewer wants when a session has rewritten half a module.

See also: semantic diff compared with git diff and tree-sitter grammar packages for Rust and TypeScript.

Questions

What is a semantic diff for Rust?

A comparison that reads Rust structure — items, impl blocks, trait bounds, generics — instead of lines. It matters more in Rust than most languages because meaning lives in places a text diff renders as noise, such as a bound moving from a signature into a where clause.

Why is git diff misleading on Rust code?

Impl blocks move without changing behaviour, so a reorganised file reads as a rewrite. Trait bounds change meaning without changing much text. And macros expand into the code that actually runs, which the diff never shows you at all.

How do you diff Rust code by structure?

Parse both revisions with a Rust grammar, match items by path and signature rather than position, and compare the matched pairs. The result reports which functions and impls changed, and stays quiet about the ones that only moved.