Skip to content

feat(diff): context/normal output formats, Myers algorithm as new default - #216

Merged
helly25 merged 2 commits into
mainfrom
diff-formats-myers
Jul 4, 2026
Merged

feat(diff): context/normal output formats, Myers algorithm as new default#216
helly25 merged 2 commits into
mainfrom
diff-formats-myers

Conversation

@helly25

@helly25 helly25 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three related changes to mbo/diff, developed and verified together:

1. Output formats: unified | context | normal

  • New DiffOptions::output_format / --format flag / format attr on the bazel diff_test rule.
  • Context format (diff -c) and normal format (plain diff) reproduce GNU diff byte for byte (verified against the system diff on 18 case families incl. empty files, no-newline markers, multi-hunk inputs) and apply cleanly with patch(1).
  • Rendering was extracted out of internal/chunk.cc into internal/output.{h,cc} (one emitter per format); Chunk now only accumulates and filters, and file headers are format aware (***/--- for context, none for normal).
  • Bug fix: unified chunk headers for empty ranges (pure insertions/deletions, visible with --context=0) now reference the line preceding the gap (@@ -2,0 +3 @@ instead of @@ -3,0 +3 @@), matching GNU diff. Previously patch applied such hunks one line too late. Found by round-tripping every format through patch.

2. Myers diff algorithm — the new default

  • impl/diff_myers.*: linear-space middle-snake divide and conquer ("An O(ND) Difference Algorithm and Its Variations", the algorithm behind GNU diff and git) over interned line tokens, so every ignore/strip/replace option applies unchanged. Produces minimal diffs; past a cost cap of max(64, √(L+R)) it takes a git-style furthest-reaching split to bound pathological inputs.

  • Replays through the existing ChunkedDiff push interface: chunking, context handling, filters and all three output formats are shared.

  • Benchmark (bazel run -c opt //mbo/diff:diff_benchmark, 10k-line inputs):

    case myers naive
    equal 3µs 3µs
    50 scattered edits 1.0ms 2.3ms
    100-line block moved 1.0ms 2.0ms
    disjoint (2k lines) 0.7ms 15.7s
  • The old engine is renamed to what it is: naive (DiffNaive, greedy closest-match resync, not minimal). The flag/attr value unified remains a deprecated alias that now selects myers — matching its historic "like diff -u" promise — and enforces --format=unified (CLI check + bzl fail()).

3. Feature/algorithm support matrix

  • New diff_test_formats_test macro crosses output formats (each format checked against its own golden) with algorithms; golden coverage now spans {myers,naive} × {unified,context,normal} plus direct × all formats.
  • AlgorithmFeatureMatrix unit test proves all comparison options (ignore_case, ignore_*_space, ignore_blank_lines, ignore_matching_lines, strip_comments, regex_replace_*) behave identically under naive, myers and direct.
  • Documented corner case (pinned by test): ignore_case + a case-sensitive ignore_matching_lines expression diverges between naive and myers; write such expressions as (?i)....
  • max_diff_chunk_length only applies to naive (documented; myers uses its internal cost cap).

Test plan

  • bazel test //... — 91 tests pass (40 in //mbo/diff/..., incl. 15 new golden targets and the new unit tests).
  • MyersRoundTrip property test: apply(lhs, diff) == rhs over a deterministic corpus incl. a disjoint pair that exercises the cost-cap fallback.
  • Manual: byte-comparison against system diff/diff -c/diff -U0 and patch round-trips for all three formats × {naive, myers} on 11 input families; hang-regression bisect for the fallback path (fixed grid-edge clamping in the furthest-reaching recurrence).

@helly25
helly25 requested a review from Fab-Cat July 3, 2026 22:47
@helly25
helly25 force-pushed the diff-formats-myers branch from 135c848 to e8d6ebb Compare July 3, 2026 22:59
…ault

Formats (DiffOptions::output_format, --format, diff_test format attr):
- unified (default), context (diff -c) and normal (plain diff) output,
  byte-identical to GNU diff and verified to apply cleanly with patch(1).
- Rendering extracted from internal/chunk.cc into internal/output.{h,cc};
  Chunk now only accumulates and filters, file headers are format aware.
- Fixed unified empty-range chunk headers (pure insertions/deletions with
  --context=0) to reference the line preceding the gap like GNU diff;
  previously patch applied such hunks one line too late.

Myers algorithm (impl/diff_myers.*, now the default):
- Linear-space middle-snake divide and conquer over interned line tokens
  (all ignore/strip/replace options apply); minimal diffs; git-style
  furthest-reaching split past a cost cap of max(64, sqrt(L+R)).
- Replays through the ChunkedDiff push interface, so chunking, filters and
  all output formats are shared.
- >2x faster on scattered edits, disjoint 2k-line inputs drop from ~16s to
  <1ms (//mbo/diff:diff_benchmark).

Algorithm naming:
- The old default engine is renamed unified -> naive (what it is: greedy
  closest-match resynchronization, not minimal): DiffNaive, kNaive,
  impl/diff_naive.*.
- The flag value 'unified' stays as a deprecated alias that now selects
  myers (matching its "like diff -u" promise) and enforces unified format
  (CLI check and bzl macro fail()).

Feature/algorithm matrix:
- diff_test_formats_test bzl macro crosses output formats (each with its
  own golden) with algorithms; golden coverage for
  {myers,naive}x{unified,context,normal} and direct x all formats.
- AlgorithmFeatureMatrix unit test: all comparison options behave
  identically under naive, myers and direct.
- Documented corner case: ignore_case with a case-sensitive
  ignore_matching_lines expression diverges between naive and myers;
  write such expressions as (?i)....
- max_diff_chunk_length only applies to naive (myers uses its cost cap).
@helly25
helly25 force-pushed the diff-formats-myers branch from e8d6ebb to 00561c2 Compare July 3, 2026 23:03
pre-commit and trunk cover disjoint formatters (pre-commit: clang-format,
shfmt; trunk: prettier, buildifier, ...), so markdown/yaml/bzl edits could
pass the local hooks and then fail the trunk CI job. Add a local trunk-fmt
hook that invokes the trunk CLI on the staged files, keeping
.trunk/trunk.yaml the single source of truth for formatter versions.

pre-commit remains the sole owner of the git hooks (trunk's git-hook
actions stay disabled); the hook is skipped in CI (SKIP env plus ci.skip)
where the trunk job runs the full check. Requires the trunk launcher
locally (brew install trunk-io).
@helly25
helly25 enabled auto-merge (squash) July 3, 2026 23:12
@helly25
helly25 merged commit 2291155 into main Jul 4, 2026
40 checks passed
@helly25
helly25 deleted the diff-formats-myers branch July 4, 2026 00:54
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.

2 participants