Skip to content

Latest commit

 

History

History
190 lines (149 loc) · 7.53 KB

File metadata and controls

190 lines (149 loc) · 7.53 KB

mojo-diff

Text diffing in pure Mojo, mirroring Python's difflib. No Python dependencies, no FFI.

License: MIT Mojo Podcast X

mojo-diff: unified-diff text comparison in pure Mojo Demo: unified_diff output with @@ hunks, byte-matching Python difflib

As of mid-2026 the Mojo ecosystem has no line-diffing library. mojo-diff fills that gap: a pure-Mojo implementation of the Myers O(ND) greedy diff algorithm (Myers, 1986), exposed through the same opcode model and unified-diff output that Python's difflib produces. unified_diff is byte-for-byte compatible with difflib.unified_diff on the fixture corpus in test/data/, generated directly by Python's difflib.

Coming from Python

If you know Python's difflib, the core operations map like this:

Python (difflib) mojo-diff
difflib.unified_diff(a, b, fromfile, tofile) unified_diff(a, b, fromfile, tofile, n)
SequenceMatcher(None, a, b).ratio() ratio(splitlines_keepends(a), splitlines_keepends(b))
SequenceMatcher(None, a, b).get_opcodes() get_opcodes(splitlines_keepends(a), splitlines_keepends(b))

One shape difference: difflib.unified_diff takes lists of lines, whereas mojo-diff's unified_diff takes the two whole strings and splits them for you. ratio and get_opcodes operate on line lists — use splitlines_keepends(s) to produce them.

What it handles

  • get_opcodes(a, b), mirroring SequenceMatcher.get_opcodes(): edit opcodes turning a into b, tagged "equal" / "replace" / "delete" / "insert", same 5-tuple shape as difflib.
  • matching_blocks(a, b), mirroring get_matching_blocks(): sentinel-terminated matching runs.
  • ratio(a, b), mirroring SequenceMatcher.ratio(): similarity as 2*M/T.
  • unified_diff(a_text, b_text, from_file, to_file, context=3), mirroring difflib.unified_diff: standard unified-diff text, matching difflib's hunk grouping and @@ range math exactly.
  • splitlines_keepends(text), mirroring str.readlines(): split on \n, keeping terminators.
  • Compatibility details: lines keep trailing newlines like readlines()-fed difflib; a final line with no trailing newline is emitted without one and no GNU-diff \ No newline at end of file marker (difflib doesn't emit one either); identical inputs produce the empty string; empty ranges use difflib's start-1,0 form and single-line ranges omit the length.

What it deliberately does NOT do

  • Character-level or word-level diffing. Line-level only, in v0.1.
  • Match difflib's Ratcliff-Obershelp alignment exactly on repeated lines. Myers finds a minimal edit script; with heavily repeated lines that can differ from SequenceMatcher's alignment, even though both are valid diffs.
  • ndiff, context_diff, HtmlDiff, autojunk heuristics. Not implemented in v0.1.

Install

With pixi:

pixi install
pixi run test

Or with uv:

uv venv
uv pip install mojo
.venv/bin/mojo run -I src test/test_diff.mojo

Requires Mojo 1.0 or newer.

Usage

from diff import get_opcodes, unified_diff, ratio, matching_blocks, OpCode

def main() raises:
    var diff = unified_diff(
        open("a.txt", "r").read(), open("b.txt", "r").read(), "a.txt", "b.txt"
    )
    print(diff, end="")
--- a.txt
+++ b.txt
@@ -1,7 +1,9 @@
 def load(path):
+    if not path:
+        return 0
     data = open(path).read()
-    rows = data.split(chr(10))
+    lines = data.split(chr(10))
     total = 0
-    for r in rows:
-        total += len(r)
+    for line in lines:
+        total += len(line)
     return total

OpCode is {tag, a_start, a_end, b_start, b_end}, the same shape as difflib's opcodes.

Tests

pixi run test

34 tests cover opcode generation, matching blocks, ratio calculation, and splitlines_keepends, plus 11/11 unified-diff fixtures matching Python's difflib byte-for-byte (identical inputs, single-line files, multi-hunk diffs at multiple context widths, content-to-empty and empty-to-content, and missing-trailing-newline cases). A performance test diffs 5,000 lines in roughly 0.9ms. Fixtures and their expected outputs are (re)generated with python3 test/data/generate_fixtures.py.

test/fuzz_runner.mojo exercises the diff engine against corrupted or random input to confirm it never crashes.

Part of a pure-Mojo library suite

Eleven pure-Mojo libraries that mirror familiar Python stdlib and PyPI APIs, filling gaps in the native Mojo ecosystem:

  • mojo-xml — general-purpose XML parsing, an ElementTree-shaped DOM (Python's xml.etree.ElementTree)
  • mojo-feed — RSS, Atom, and JSON Feed parsing (Python's feedparser)
  • mojo-captions — SRT and WebVTT subtitle/transcript parsing (no Python stdlib parallel)
  • mojo-html — HTML parsing and article extraction (Python's readability)
  • mojo-markdown — CommonMark markdown parsing (Python's markdown)
  • mojo-unicodedata — Unicode normalization and case folding (Python's unicodedata)
  • mojo-template — a Jinja-flavored template engine (Python's jinja2)
  • mojo-tar — tar archive reading and writing (Python's tarfile)
  • mojo-redis — a Redis client (Python's redis-py)
  • mojo-url — URL parsing and encoding (Python's urllib.parse)

Contributing

Issues and PRs welcome, especially cases where output diverges from Python's difflib (attach the two inputs) and repeated-line scenarios where Myers and Ratcliff-Obershelp disagree. Run pixi run test before sending a PR, and regenerate fixtures with python3 test/data/generate_fixtures.py if you add test data.

About

Built by Conor Bronsdon — host of Chain of Thought, a podcast about AI agents, infrastructure, and engineering. Find me on X or LinkedIn.


Disclaimer

This is an independent personal project, not affiliated with, sponsored by, or endorsed by any company. All views expressed are my own.

License

Licensed under the MIT License.