moon-diff

    A text diff & patch library for MoonBit: LCS & Myers diff, 5 diff algorithms, unified/git patch apply, 3-way merge, semantic JSON diff, and multi-file tree diff — zero-dependency.

    diff
    patch
    myers
    lcs
    text
    unified
    merge
    json
    three-way
    Download zip
    Author
    Version
    0.2.2
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    27

    #moon-diff

    CI License mooncakes

    A text diff & patch library for MoonBit, written for the MoonBit 国产基础软件生态开源大赛 (MGPIC 2026).

    moon-diff computes the difference between two sequences and renders / applies standard unified diffs (the diff -u format used by Git, patch, etc.). It is generic over the element type, so it works on lines, tokens, AST nodes, or any Array[T]. On top of the core diff engine it also offers 5 diff algorithms, a 3-way merge, a semantic JSON diff, and a multi-file tree diff — all with zero external dependencies.

    #Features

    • Five diff algorithms — classic LCS (diff), Myers' O(ND) minimal edit script (myers_diff), Patience diff (patience_diff), Histogram diff (histogram_diff), and a linear-space Hirschberg algorithm (diff_linear) that uses only O(|a|+|b|) memory.
    • Unified diff renderingto_unified emits GNU diff -u style output with context lines and @@ headers, compatible with standard patch tooling.
    • Patch applicationapply_unified reads a unified diff back and reconstructs the new text, so diffs are fully reversible. apply_unified_fuzzy tolerates offset / fuzz like patch/git apply.
    • Reverse apply (patch -R)apply_unified_reverse applies a unified diff backwards (new → old) and reverse_unified flips a patch's +/- signs.
    • Git-style & binary diffgit_diff_text emits Git's diff --git / index <sha> headers, git_blob_hash computes the Git blob SHA-1, and binary_diff emits the Binary files ... differ format.
    • Verified SHA-1sha1_hex is a from-scratch, fully tested implementation.
    • Token & character-level diffdiff_tokens (word granularity, whitespace preserved) and diff_chars (single-character), plus word_diff / word_diff_html for inline highlighting.
    • Unicode-aware tokenisertokenize_unicode / diff_tokens_unicode split text into per-CJK-character and per-word tokens (ASCII whitespace preserved), so Chinese / Japanese / Korean diffs are compared character-by-character instead of as one opaque blob. word_diff_unicode / word_diff_html_unicode give inline highlighting for such text.
    • Similarity ratioratio(a, b) returns a Double in [0, 1] (LCS-based, like Python's difflib.ratio) for ranking / near-duplicate detection — works on CJK text too.
    • 3-way mergemerge3 implements the classic diff3 region strategy with conflict markers and git merge -X ours/theirs style resolvers.
    • Semantic JSON diffjson_diff_text parses two JSON documents and emits an RFC 6902 JSON Patch (object order-independent).
    • JSON Patch applyapply_json_patch / apply_json_patch_text apply an RFC 6902 patch (add / remove / replace; objects & arrays; the - end-of-array token and JSON-Pointer ~ escape handling) back to a document, closing the round-trip so a patch is fully reversible.
    • Multi-file tree diffdiff_trees / render_tree_patch / apply_tree_patch produce and consume Git-style multi-file patches with rename detection.
    • git diff --stat summaryto_unified_stat renders a file | N +- change histogram for any Change list, scaled to fit on one line (like git diff --stat).
    • Ignore whitespace / casediff_lines_ignore compares lines ignoring trailing/internal whitespace (--ignore-whitespace) and/or letter case (--ignore-case), while still rendering and patching the real, un-normalized content.
    • Prefix/suffix pruningdiff_algorithm trims the common leading/trailing lines before dispatching, the standard GNU diff / git optimization that makes large inputs much faster without changing the result.
    • Generic & zero-dependency — works on any T with Eq (and Show for rendering).
      • Command-line toolsrc/cli is a runnable front-end (diff / patch / merge / json / jsonapply / ratio / algo / selftest) that doubles as the project's runnable example.

    #Install

    moon add yuzhiblue/moon-diff

    Then import it in your package:

    import yuzhiblue/moon-diff/diff

    #Quick start

    import yuzhiblue/moon-diff/diff

    // Diff two sequences (LCS based)
    let a = ["apple", "banana", "cherry"]
    let b = ["apple", "blueberry", "cherry"]
    let changes = diff(a, b)
    // changes = [Equal("apple"), Delete("banana"), Insert("blueberry"), Equal("cherry")]

    // Reconstruct either side from the change list
    to_new(changes) // ["apple", "blueberry", "cherry"]
    to_old(changes) // ["apple", "banana", "cherry"]

    // Diff two texts line-by-line, then render a unified diff
    let old_text = "line1\nline2\nline3\nline4\nline5"
    let new_text = "line1\nLINE2\nline3\nline4\nline5x"
    let patch = to_unified(diff_lines(old_text, new_text), "old.txt", "new.txt", 3)
    println(patch)

    // Apply the patch back — fully reversible
    let result = apply_unified(old_text, patch)
    // result == new_text

    #Choosing a diff algorithm

    import yuzhiblue/moon-diff/diff

    let a = ["the", "quick", "brown", "fox", "jumps"]
    let b = ["the", "slow", "brown", "fox", "sleeps"]
    // all five produce an edit script whose to_new / to_old reconstruct b / a
    let ch = diff_algorithm(Patience, a, b)
    to_new(ch) // == b

    #3-way merge

    import yuzhiblue/moon-diff/diff

    let (merged, n) = merge3_count("1\n2\n3", "1\n2\nX\n3", "1\n2\nY\n3")
    // merged contains diff3 conflict markers; n == 1 (one unresolved region)

    #Semantic JSON diff (RFC 6902)

    import yuzhiblue/moon-diff/diff

    let patch = json_diff_text("{\"a\":1,\"b\":2}", "{\"a\":1,\"b\":3}")
    // patch == [{"op":"replace","path":"/b","value":3}]

    #Multi-file tree diff

    import yuzhiblue/moon-diff/diff

    let old_fs = [("a.txt", "1\n2\n3"), ("b.txt", "x\ny")]
    let new_fs = [("a.txt", "1\n2\n3\n4"), ("c.txt", "hello")]
    let patch = render_tree_patch(old_fs, new_fs, 50)
    let result = apply_tree_patch(old_fs, patch) // Ok(new tree)

    #Command-line tool

    The src/cli package is a runnable example. Because the Core SDK ships no filesystem package, multi-line inputs use the escape sequences \n (newline) and \t (tab):

    moon run cli -- diff "line1\nline2" "line1\nLINE2" moon run cli -- diff "line1\nline2" "line1\nLINE2" --stat # git diff --stat style summary moon run cli -- diff "a b" "A B" --ignore-whitespace --ignore-case # ignore ws/case moon run cli -- patch "line1\nline2" "$(cat patch.txt)" # patch text as one arg moon run cli -- merge "base" "ours" "theirs" --ours moon run cli -- json '{"a":1}' '{"a":2}' moon run cli -- jsonapply '{"a":1}' '[{"op":"replace","path":"/a","value":2}]' # apply a JSON Patch moon run cli -- ratio "我爱北京" "我爱南京" # similarity in [0,1] moon run cli -- algo "old text" "new text" # edit distance of all 5 algorithms moon run cli -- selftest # internal consistency checks

    #API reference

    FunctionSignatureDescription
    difffn[T: Eq] diff(Array[T], Array[T]) -> Array[Change[T]]LCS-based difference
    myers_difffn[T: Eq] myers_diff(Array[T], Array[T]) -> Array[Change[T]]Myers O(ND) minimal diff
    to_newfn[T] to_new(Array[Change[T]]) -> Array[T]reconstruct the new sequence
    to_oldfn[T] to_old(Array[Change[T]]) -> Array[T]reconstruct the old sequence
    diff_linesfn diff_lines(String, String) -> Array[Change[String]]diff two texts by line
    diff_lines_ignorefn diff_lines_ignore(String, String, Bool, Bool) -> Array[Change[String]]line diff ignoring whitespace/case (real content preserved)
    diff_charsfn diff_chars(String, String) -> Array[Change[String]]character-level diff
    diff_tokensfn diff_tokens(String, String) -> Array[Change[String]]word/token-level diff (whitespace preserved)
    tokenizefn tokenize(String) -> Array[String]split into alternating word / whitespace runs
    tokenize_unicodefn tokenize_unicode(String) -> Array[String]Unicode-aware split (per-CJK-char, per-word, per-punct, whitespace runs)
    diff_tokens_unicodefn diff_tokens_unicode(String, String) -> Array[Change[String]]Unicode-token diff (Chinese/Japanese/Korean friendly)
    word_diff_unicodefn word_diff_unicode(String, String) -> Stringintra-line highlight with [-x-]{+y+} (Unicode-aware)
    word_diff_html_unicodefn word_diff_html_unicode(String, String) -> (String, String)<del>/<ins> HTML highlighting (Unicode-aware)
    ratiofn ratio(String, String) -> Doublesimilarity in [0,1] (LCS of code points, like difflib.ratio)
    changes_to_stringfn changes_to_string(Array[Change[String]]) -> Stringreconstruct a String from a change list
    to_unifiedfn[T: Show] to_unified(Array[Change[T]], String, String, Int) -> Stringrender a unified diff (context = lines of context)
    to_unified_statfn to_unified_stat(Array[Change[String]], String, Int) -> Stringgit diff --stat style file | N +- summary
    apply_unifiedfn apply_unified(String, String) -> Stringapply a unified diff to the old text
    apply_unified_fuzzyfn apply_unified_fuzzy(String, String, Int, Int) -> Result[String, String]apply with offset/fuzz tolerance
    apply_unified_reversefn apply_unified_reverse(String, String) -> Stringapply a unified diff backwards (patch -R)
    reverse_unifiedfn reverse_unified(String) -> Stringflip a patch's +/- signs
    git_diff_textfn git_diff_text(String, String, String, String, Int) -> StringGit-style diff --git / index headers
    git_blob_hashfn git_blob_hash(String) -> StringGit blob SHA-1 of a string
    binary_difffn binary_diff(String, String, String) -> StringBinary files ... differ format
    sha1_hexfn sha1_hex(String) -> Stringfrom-scratch SHA-1 (hex)
    word_difffn word_diff(String, String) -> Stringintra-line highlight ([-x-]{+y+})
    word_diff_htmlfn word_diff_html(String, String) -> (String, String)<del>/<ins> HTML highlighting
    diff_algorithmfn diff_algorithm(DiffAlgorithm, Array[T], Array[T]) -> Array[Change[T]]dispatch to any algorithm
    patience_difffn patience_diff(Array[T], Array[T]) -> Array[Change[T]]Patience diff
    histogram_difffn histogram_diff(Array[T], Array[T]) -> Array[Change[T]]Histogram diff
    diff_linearfn diff_linear(Array[T], Array[T]) -> Array[Change[T]]linear-space Hirschberg diff
    merge3fn merge3(Array[String], Array[String], Array[String]) -> MergeResult3-way merge (diff3)
    merge3_textfn merge3_text(String, String, String) -> String3-way merge returning text
    merge3_countfn merge3_count(String, String, String) -> (String, Int)merge + conflict count
    merge3_resolve_oursfn merge3_resolve_ours(MergeResult) -> Array[String]resolve conflicts with ours
    merge3_resolve_theirsfn merge3_resolve_theirs(MergeResult) -> Array[String]resolve conflicts with theirs
    parse_jsonfn parse_json(String) -> Result[Json, String]parse a JSON document
    json_equalfn json_equal(Json, Json) -> Booldeep structural equality
    json_to_stringfn json_to_string(Json) -> Stringserialise JSON to text
    json_difffn json_diff(Json, Json, String) -> Array[JsonPatchOp]RFC 6902 diff
    json_patch_to_stringfn json_patch_to_string(Array[JsonPatchOp]) -> Stringrender a JSON Patch document
    json_diff_textfn json_diff_text(String, String) -> Result[String, String]JSON Patch of A → B
    apply_json_patchfn apply_json_patch(Json, Array[JsonPatchOp]) -> Result[Json, String]apply an RFC 6902 patch to a Json value
    apply_json_patch_textfn apply_json_patch_text(String, String) -> Result[String, String]apply an RFC 6902 patch (doc + patch as text)
    diff_treesfn diff_trees(Array[(String,String)], Array[(String,String)], Int) -> TreeDiffdiff two file trees
    render_tree_patchfn render_tree_patch(Array[(String,String)], Array[(String,String)], Int) -> StringGit multi-file patch
    apply_tree_patchfn apply_tree_patch(Array[(String,String)], String) -> Result[Array[(String,String)], String]apply a multi-file patch
    tree_diff_summaryfn tree_diff_summary(TreeDiff) -> StringN files changed, ... summary
    unified_to_htmlfn unified_to_html(String, String, Int) -> Stringrender a unified diff as an HTML <table>
    diff_html_pagefn diff_html_page(String, String, Int, String) -> Stringfull HTML page (inline CSS) for a diff

    Change[T] is Equal(T) | Delete(T) | Insert(T).

    #How it works

    • diff builds an LCS DP table dp[i][j] and backtracks it to recover Equal / Delete / Insert operations in forward order.
    • myers_diff runs the classic Myers algorithm with the V array + trace, then backtracks the trace to recover a minimal edit script.
    • patience_diff / histogram_diff pick unique/frequent anchor lines and recurse into the gaps for more "human" alignments.
    • diff_linear (Hirschberg) splits the problem in the middle and recurses, using only O(|a|+|b|) memory regardless of input size.
    • diff_algorithm trims the common prefix and suffix of the two inputs before dispatching, so the underlying algorithm only runs on the (usually smaller) middle — a standard GNU diff / git optimization that preserves results while drastically cutting work on large inputs.
    • merge3 decomposes each branch into base-line-aligned replacement blocks, then merges per region: identical → keep, one-sided change → take it, both-different → conflict markers.
    • json_diff walks two JSON values and emits RFC 6902 add/remove/replace ops; object members are compared by key (order-independent), arrays positionally.
    • to_unified / apply_unified walk the change list and emit / replay @@ ... @@ hunks; sha1_hex (in git.mbt) is a from-scratch SHA-1 used for Git blob hashes.

    #Complexity

    AlgorithmTimeSpace
    diff (LCS)O(n·m)O(n·m)
    myers_diffO((n+m)·D)O((n+m)·D)
    patience_diff / histogram_diffO(n·m) (amortised)O(n·m)
    diff_linear (Hirschberg)O(n·m)O(n+m)
    to_unified / apply_unifiedO(n)O(n)

    where n, m are the sequence lengths and D is the edit distance.

    #Build & test

    moon build # build all packages (default + bench + cli) moon test # run the test suite (50+ cases) moon run cli -- selftest # run the CLI's internal consistency checks moon run --release src/bench # run the benchmark harness (MoonBit side)

    CI runs moon build and moon test on every push/PR via .github/workflows/ci.yml.

    #Project layout

    moon.mod.json src/diff/ types.mbt # Change enum: Equal / Delete / Insert core.mbt # lcs_table, diff, myers_diff, to_new / to_old, diff_lines, diff_lines_ignore, diff_chars, diff_tokens, tokenize, word_diff, word_diff_html, tokenize_unicode, diff_tokens_unicode, word_diff_unicode, word_diff_html_unicode, ratio, codepoints unified.mbt # to_unified, to_unified_stat, apply_unified, apply_unified_fuzzy, apply_unified_reverse, reverse_unified git.mbt # sha1_hex, git_blob_hash, git_diff_text, binary_diff merge.mbt # merge3, merge3_text, merge3_count, resolve_ours / resolve_theirs algorithms.mbt # DiffAlgorithm, diff_algorithm, patience_diff, histogram_diff, diff_linear semantic.mbt # JSON parser, json_equal, json_to_string, json_diff (RFC 6902), json_patch_to_string, json_diff_text, apply_json_patch / apply_json_patch_text dir.mbt # diff_trees, render_tree_patch, apply_tree_patch, tree_diff_summary diff_test.mbt # test blocks (run with `moon test`) src/cli/ main.mbt # command-line front-end (diff / patch / merge / json / jsonapply / ratio / algo / selftest) moon.pkg.json src/bench/ bench.mbt # benchmark harness (RESULT-line protocol) moon.pkg.json docs/ benchmark.py # MoonBit vs Python difflib driver bench_results.md loc.py # effective-line-of-code counter (excludes comments / blanks)

    #Roadmap

    #License

    Apache License 2.0 — see LICENSE.