Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

v0 source pure

Package myersdiff computes the shortest edit script between two sequences of lines, using Myers' greedy algorithm: th...

Readme View source

gno.land/p/moul/x/daily/myersdiff/v0

The shortest edit script between two documents, Myers' algorithm: Lines, Slices, Budget, Hunks, Unified, UnifiedScript, and Script.Apply.

 1import "gno.land/p/moul/x/daily/myersdiff/v0"
 2
 3s := myersdiff.Lines("a\nb\nc", "a\nx\nc")
 4s.Edits        // [ a] [-b] [+x] [ c]
 5s.Added()      // 1
 6s.Minimal      // true
 7s.Apply([]string{"a", "b", "c"})     // ["a", "x", "c"], true
 8s.Apply([]string{"q", "b", "c"})     // ["a", "x", "c"], false: not this document
 9
10myersdiff.Unified(old, new, 2)       // @@ -1,4 +1,5 @@ and its lines

A realm that stores text and lets it be edited ends up holding two versions of it and no way to say what moved between them. Storing the diff instead of the second snapshot is the usual answer, and showing a reader what a proposal changes is the other. Both need the same thing first: which lines were added, which were removed, and in what order. Everything else is formatting.

The search is bounded by a budget, not by the input size, deliberately. Myers costs O((n+m)*d) where d is the number of differing lines, so two similar documents are cheap at any size and two unrelated ones are quadratic. On chain the caller picks both documents, so d is attacker-controlled and the input size alone bounds nothing useful. DefaultBudget caps the rounds at 256; past it the search gives up and returns a script that deletes everything and inserts everything, with Minimal set to false. MaxBudget (1,024) is the ceiling a caller cannot raise past, because the round count is what the search allocates against: one frontier snapshot is kept per round so the script can be walked back out of them.

The threshold is a line count, not a file size, and it is easy to read the wrong way. The default gives up past 256 changed lines at any file size: two 4,000-line documents differing in 10 lines are diffed minimally, two 300-line documents sharing nothing are not. The cost is gas, not only time, and a search that runs to the ceiling against two MaxLines documents was measured in the billions against a 3,000,000,000 block limit during this package's review. A caller diffing text that may be unrelated should pass a small budget and take the fallback, rather than pay for the full search in front of it.

Three behaviours worth knowing before you use it:

  • A non-minimal script is still a correct script. The budget changes how short the answer is, never whether it is right: Apply replays the fallback into the same document it replays the minimal one into, and reports true for both. Check Minimal when you are about to store a diff as the canonical record of a change, and ignore it when you are rendering one for a human.
  • Reads are total, construction panics. Lines, Unified and Budget panic above MaxLines (4,096 a side), and nothing else in the package panics at all: Hunks with a negative context, Apply against a document the script does not describe, a Hunk whose side is empty are all defined. Lines counts newlines before it splits, so the cap is enforced before the slice for an oversized document is allocated, not after. Construction is a transaction, where aborting is the useful answer. Rendering is a page, and a realm cannot be redeployed on the same path, so a Render that panics is broken permanently.
  • Apply verifies, it does not merely replay. Every Equal and Delete edit has to match the line it consumes and the script has to consume the whole document, so a script built against a different document comes back with false. Without that check the round trip proves only that a script agrees with itself, which is a test that cannot fail.
  • A moved line is a delete and an insert, not a move. Myers compares sequences and has no concept of identity, so reordering three lines costs the edits needed to explain the new order. A realm that wants move detection has to run it as a second pass over this script, pairing equal texts across the two sides.

Hunks(s, context) groups the script the way a review tool prints it, merging two changes into one hunk when their context would overlap. The @@ header follows diff(1) exactly, including the convention a pure insertion needs: a side contributing no lines reports the line before the hunk and a length of zero, so @@ -0,0 +1,2 @@ is what patch(1) expects to read back.

Unified emits the hunk bodies and nothing else. The --- and +++ file headers name files, which a realm does not have, so a caller that wants them adds them.

Line-oriented only. A word diff or a character diff is the same algorithm over a different sequence, and Slices is exported so a caller can supply one; the splitting and the rendering would be different and are not here.

Live demo: r/moul/x/daily/myersdiffdemo.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package myersdiff computes the shortest edit script between two sequences of lines, using Myers' greedy algorithm: the same one git, diff and every review tool run to turn two documents into a list of additions and deletions.

A realm that stores text and lets it be edited has two versions of it and no way to show what moved between them. Storing a diff instead of a snapshot is the usual answer, and this is the half that produces it.

The cost of the search is bounded by a budget, not by the input size alone: see Budget for why a minimal diff is sometimes the wrong answer on chain.

Live demo: r/moul/x/daily/myersdiffdemo(/r/moul/x/daily/myersdiffdemo/v0).

Constants 4

const DefaultBudget

1const DefaultBudget = 256
source

DefaultBudget caps the number of rounds the search may run before it gives up and returns a non-minimal script. Myers' algorithm costs O((n+m)*d) where d is the number of differing lines, so two similar documents are cheap at any size and two unrelated ones are quadratic. On chain the caller picks the input, so the cost has to be bounded by something the caller does not control.

The threshold this sets is worth stating plainly, because it is not a size: the default gives up past 256 changed lines, at any file size. Two 4,000-line documents differing in 10 lines are diffed minimally; two 300-line documents sharing nothing are not. A caller whose documents are large and similar is well inside it; a caller diffing unrelated text is always outside it and should pass a small budget rather than pay for the search before the fallback.

The cost is in gas, not just in time: a search that runs to the default ceiling against two MaxLines documents was measured in the billions of gas against a 3,000,000,000 block limit during the review of this package, before the frontier was resized to the round ceiling. Treat a full-ceiling search as something to avoid rather than something to budget for.

const MaxBudget

1const MaxBudget = 1024
source

MaxBudget is the hard ceiling on the rounds any caller can ask for, whatever budget it passes.

It exists because the round count, not the input size, is what the search allocates against: one snapshot of the frontier is kept per round so the script can be walked back out of them. Without the ceiling, two documents at MaxLines and a large budget would permit 8,192 snapshots, and the budget parameter would hand a caller the memory bill as a dial. The useful direction for a caller is downwards.

const MaxLines

1const MaxLines = 4096
source

MaxLines caps each side of the comparison. The algorithm allocates two slices proportional to n+m before it compares anything, so the size is checked before the work starts rather than discovered during it.

const Delete, Equal, Insert

1const (
2	// Delete marks a line present in a and absent from b.
3	Delete Op = -1
4	// Equal marks a line present in both, unchanged.
5	Equal Op = 0
6	// Insert marks a line absent from a and present in b.
7	Insert Op = 1
8)
source

Functions 6

func Unified

1func Unified(a, b string, context int) string
source

Unified renders the body of a unified diff: the @@ headers and their lines, and nothing else. The --- and +++ file headers name files, which a realm does not have, so they are the caller's to add.

func UnifiedScript

1func UnifiedScript(s Script, context int) string
source

UnifiedScript renders an already-computed script, for a caller that wants the counts or the Minimal flag as well as the text.

func Hunks

1func Hunks(s Script, context int) []Hunk
source

Hunks groups a script into the runs a unified diff prints, with context unchanged lines around each change. A negative context is treated as zero; a script with no changes produces no hunks.

func Budget

1func Budget(a, b []string, budget int) Script
source

Budget diffs two line sequences, stopping the search after at most budget rounds. A budget of zero or less means DefaultBudget; a budget larger than len(a)+len(b) is capped at it, since no minimal script is longer than that.

It panics when either side exceeds MaxLines. Construction is a transaction, where aborting is the useful answer; reading is where totality matters, and every method on the resulting Script is total.

func Lines

1func Lines(a, b string) Script
source

Lines splits a and b on "\n" and diffs them with DefaultBudget.

A trailing newline produces a trailing empty line on that side, which is what makes "a\n" and "a" different documents here, as they are to diff(1).

func Slices

1func Slices(a, b []string) Script
source

Slices diffs two line sequences with DefaultBudget.

Types 4

type Edit

struct
1type Edit struct {
2	Op   Op
3	Line string
4}
source

Edit is one line of the edit script.

type Hunk

struct
1type Hunk struct {
2	AStart int
3	ALines int
4	BStart int
5	BLines int
6	Edits  []Edit
7}
source

Hunk is a contiguous run of the script worth printing: every change, plus up to context unchanged lines on each side of it.

AStart and BStart are 1-based line numbers. When a side contributes no lines at all, its start is the number of the line before the hunk and its length is zero, which is what diff(1) prints for a pure insertion into an empty file.

Methods on Hunk

func Header

method on Hunk
1func (h Hunk) Header() string
source

Header renders the @@ line of a unified diff.

type Op

ident
1type Op int8
source

Op is what happened to one line.

Methods on Op

func String

method on Op
1func (o Op) String() string
source

String renders the op as the single character a unified diff uses.

type Script

struct
1type Script struct {
2	Edits   []Edit
3	Minimal bool
4}
source

Script is a complete edit script turning a into b.

Minimal is false when the search hit its budget and the script fell back to deleting everything and inserting everything. That script is still correct: applying it to a yields b. It is simply not the shortest one.

Methods on Script

func Added

method on Script
1func (s Script) Added() int
source

Added counts inserted lines.

func Apply

method on Script
1func (s Script) Apply(a []string) ([]string, bool)
source

Apply replays the script against a and returns the lines it produces, which is b when the script came from Slices(a, b).

The second return reports whether a is the document the script describes: every Equal and Delete edit has to match the line it consumes, and the script has to consume all of a. It is false for a script built against a different document, which is the check that makes a stored diff verifiable rather than merely replayable. The lines are returned either way, because they are what the script says and a caller deciding what to do about a mismatch usually wants to see both.

func Deleted

method on Script
1func (s Script) Deleted() int
source

Deleted counts deleted lines.

func Unchanged

method on Script
1func (s Script) Unchanged() int
source

Unchanged counts lines carried over untouched.

Dependencies

Imports 2

/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/myersdiff/v0

Imported by indexer

Source Files 4