github.com/gopherd/gonum@v0.0.4/graph/formats/rdf/iso_canonical.go (about)

     1  // Copyright ©2020 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package rdf
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"hash"
    12  	"sort"
    13  )
    14  
    15  // See "Canonical Forms for Isomorphic and Equivalent RDF Graphs: Algorithms
    16  // for Leaning and Labelling Blank Nodes" by Aiden Hogan for description of
    17  // the algorithm, https://doi.org/10.1145/3068333 and available free from
    18  // the author's web page http://aidanhogan.com/docs/rdf-canonicalisation.pdf.
    19  //
    20  // Aspects of implementation from discussion in v1.0 of the readme of the PoC
    21  // at https://doi.org/10.5281/zenodo.3154322
    22  
    23  // Isomorphic returns whether the RDF graph datasets a and b are isomorphic,
    24  // where there is a bijective mapping between blank nodes in a and b using
    25  // the given hash function. If decomp is true, the graphs are decomposed
    26  // before canonicalization.
    27  func Isomorphic(a, b []*Statement, decomp bool, h hash.Hash) bool {
    28  	if len(a) != len(b) {
    29  		return false
    30  	}
    31  
    32  	zero := make([]byte, h.Size())
    33  	ah, _ := IsoCanonicalHashes(a, decomp, true, h, zero)
    34  	bh, _ := IsoCanonicalHashes(b, decomp, true, h, zero)
    35  	if len(ah) != len(bh) {
    36  		return false
    37  	}
    38  
    39  	work := make([][]byte, 2*len(ah))
    40  	lexicalHashes(work[:len(ah)], ah)
    41  	lexicalHashes(work[len(ah):], bh)
    42  	for i := range work[:len(ah)] {
    43  		if !bytes.Equal(work[i], work[i+len(ah)]) {
    44  			return false
    45  		}
    46  	}
    47  	return true
    48  }
    49  
    50  func lexicalHashes(dst [][]byte, hashes map[string][]byte) {
    51  	i := 0
    52  	for _, s := range hashes {
    53  		dst[i] = s
    54  		i++
    55  	}
    56  	sort.Sort(lexical(dst))
    57  }
    58  
    59  // IsoCanonicalHashes returns a mapping between the nodes of the RDF graph
    60  // dataset described by the given statements using the provided hash
    61  // function. If decomp is true, the graphs are decomposed before hashing.
    62  // If dist is true the input graph is decomposed into identical splits, the
    63  // entire graph will be hashed to distinguish nodes. If decomp is false,
    64  // dist has no effect.
    65  // Blank node hashes are initially set to the value of zero. Hash values
    66  // are provided for literal and IRI nodes as well as for blank node. The
    67  // hash input for literal nodes includes the quotes and the input for IRI
    68  // nodes first removes the angle quotes around the IRI, although these are
    69  // included in the map keys.
    70  //
    71  // Note that hashes returned by IsoCanonicalHashes with decomp=true are not
    72  // comparable with hashes returned by IsoCanonicalHashes with decomp=false.
    73  //
    74  // See http://aidanhogan.com/docs/rdf-canonicalisation.pdf for details of
    75  // the hashing algorithm.
    76  func IsoCanonicalHashes(statements []*Statement, decomp, dist bool, h hash.Hash, zero []byte) (hashes map[string][]byte, terms map[string]map[string]bool) {
    77  	if len(statements) == 0 {
    78  		return nil, nil
    79  	}
    80  
    81  	if debug {
    82  		debug.log(0, "Statements:")
    83  		for _, s := range statements {
    84  			debug.log(0, s)
    85  		}
    86  		debug.log(0)
    87  	}
    88  
    89  	hash, parts, ok := hashBNodesPerSplit(statements, decomp, h, zero)
    90  
    91  	if debug {
    92  		debug.log(0, "Blanks:")
    93  		if len(hash.blanks) != 0 {
    94  			for _, b := range hash.blanks {
    95  				debug.log(0, b)
    96  			}
    97  		} else {
    98  			debug.log(0, "none")
    99  		}
   100  		debug.log(0)
   101  
   102  		debug.log(0, "Parts:")
   103  		debug.logParts(0, parts)
   104  
   105  		debug.logf(0, "Hashes from hashBNodesPerSplit (splitting=%t):\n", decomp)
   106  		debug.logHashes(0, hash.hashOf, h.Size())
   107  	}
   108  
   109  	if ok {
   110  		return hash.hashOf, hash.termsFor
   111  	}
   112  
   113  	// TODO: remove the triviality exception in distinguish and return
   114  	// the original hashes if this result is nil. Make the triviality
   115  	// exception optional.
   116  	hashes = distinguish(statements, dist, h, zero, hash, parts, nil, 0)
   117  
   118  	if hashes == nil {
   119  		// distinguish was given trivial parts and
   120  		// we did not ask it to try to merge them.
   121  		return hash.hashOf, hash.termsFor
   122  	}
   123  
   124  	if debug {
   125  		debug.log(0, "Final resolved Hashes:")
   126  		debug.logHashes(0, hashes, h.Size())
   127  	}
   128  
   129  	terms = make(map[string]map[string]bool, len(hashes))
   130  	for k, h := range hashes {
   131  		terms[string(h)] = map[string]bool{k: true}
   132  	}
   133  
   134  	return hashes, terms
   135  }
   136  
   137  // C14n performs a relabeling of the statements in src based on the terms
   138  // obtained from IsoCanonicalHashes, placing the results in dst and returning
   139  // them. The relabeling scheme is the same as for the Universal RDF Dataset
   140  // Normalization Algorithm, blank terms are ordered lexically by their hash
   141  // value and then given a blank label with the prefix "_:c14n" and an
   142  // identifier counter corresponding to the label's sort rank.
   143  //
   144  // If dst is nil, it is allocated, otherwise the length of dst must match the
   145  // length of src.
   146  func C14n(dst, src []*Statement, terms map[string]map[string]bool) ([]*Statement, error) {
   147  	if dst == nil {
   148  		dst = make([]*Statement, len(src))
   149  	}
   150  
   151  	if len(dst) != len(src) {
   152  		return dst, errors.New("rdf: slice length mismatch")
   153  	}
   154  
   155  	need := make(map[string]bool)
   156  	for _, s := range src {
   157  		for _, t := range []string{
   158  			s.Subject.Value,
   159  			s.Object.Value,
   160  			s.Label.Value,
   161  		} {
   162  			if !isBlank(t) {
   163  				continue
   164  			}
   165  			need[t] = true
   166  		}
   167  	}
   168  
   169  	blanks := make([]string, len(need))
   170  	i := 0
   171  	for h, m := range terms {
   172  		var ok bool
   173  		for t := range m {
   174  			if isBlank(t) {
   175  				ok = true
   176  				break
   177  			}
   178  		}
   179  		if !ok {
   180  			continue
   181  		}
   182  		if i == len(blanks) {
   183  			return dst, errors.New("rdf: too many blanks in terms")
   184  		}
   185  		blanks[i] = h
   186  		i++
   187  	}
   188  	sort.Strings(blanks)
   189  
   190  	c14n := make(map[string]string)
   191  	for i, b := range blanks {
   192  		if len(terms[b]) == 0 {
   193  			return nil, fmt.Errorf("rdf: no term for blank with hash %x", b)
   194  		}
   195  		for t := range terms[b] {
   196  			if !isBlank(t) {
   197  				continue
   198  			}
   199  			if _, exists := c14n[t]; exists {
   200  				continue
   201  			}
   202  			delete(need, t)
   203  			c14n[t] = fmt.Sprintf("_:c14n%d", i)
   204  		}
   205  	}
   206  
   207  	if len(need) != 0 {
   208  		return dst, fmt.Errorf("rdf: missing term hashes for %d terms", len(need))
   209  	}
   210  
   211  	for i, s := range src {
   212  		if dst[i] == nil {
   213  			dst[i] = &Statement{}
   214  		}
   215  		n := dst[i]
   216  		n.Subject = Term{Value: translate(s.Subject.Value, c14n)}
   217  		n.Predicate = s.Predicate
   218  		n.Object = Term{Value: translate(s.Object.Value, c14n)}
   219  		n.Label = Term{Value: translate(s.Label.Value, c14n)}
   220  	}
   221  	sort.Sort(c14nStatements(dst))
   222  
   223  	return dst, nil
   224  }
   225  
   226  func translate(term string, mapping map[string]string) string {
   227  	if term, ok := mapping[term]; ok {
   228  		return term
   229  	}
   230  	return term
   231  }
   232  
   233  type c14nStatements []*Statement
   234  
   235  func (s c14nStatements) Len() int { return len(s) }
   236  func (s c14nStatements) Less(i, j int) bool {
   237  	si := s[i]
   238  	sj := s[j]
   239  	switch {
   240  	case si.Subject.Value < sj.Subject.Value:
   241  		return true
   242  	case si.Subject.Value > sj.Subject.Value:
   243  		return false
   244  	}
   245  	switch { // Always IRI.
   246  	case si.Predicate.Value < sj.Predicate.Value:
   247  		return true
   248  	case si.Predicate.Value > sj.Predicate.Value:
   249  		return false
   250  	}
   251  	switch {
   252  	case si.Object.Value < sj.Object.Value:
   253  		return true
   254  	case si.Object.Value > sj.Object.Value:
   255  		return false
   256  	}
   257  	return si.Label.Value < sj.Label.Value
   258  }
   259  func (s c14nStatements) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   260  
   261  // hashBNodes returns the hashed blank nodes of the graph described by statements
   262  // using the provided hash function. Hashes are initialised with zero.
   263  //
   264  // This is algorithm 1 in doi:10.1145/3068333.
   265  func hashBNodes(statements []*Statement, h hash.Hash, zero []byte, hash0 map[string][]byte) (hash *table, disjoint bool) {
   266  	curr := newTable()
   267  	for _, s := range statements {
   268  		for i, t := range []string{
   269  			s.Subject.Value,
   270  			s.Predicate.Value,
   271  			s.Object.Value,
   272  			s.Label.Value,
   273  		} {
   274  			switch {
   275  			case i == 3 && t == "":
   276  				continue
   277  			case isBlank(t):
   278  				if hash0 == nil {
   279  					curr.set(t, zero)
   280  				} else {
   281  					curr.set(t, hash0[t])
   282  				}
   283  			case isIRI(t):
   284  				h.Reset()
   285  				h.Write([]byte(t[1 : len(t)-1])) //nolint:errcheck
   286  				curr.set(t, h.Sum(nil))
   287  			default:
   288  				h.Reset()
   289  				h.Write([]byte(t)) //nolint:errcheck
   290  				curr.set(t, h.Sum(nil))
   291  			}
   292  		}
   293  	}
   294  
   295  	bag := newHashBag(h, curr)
   296  	last := curr.clone()
   297  	for {
   298  		curr, last = last, curr
   299  		for _, s := range statements {
   300  			if isBlank(s.Subject.Value) {
   301  				var lab []byte
   302  				if s.Label.Value != "" {
   303  					lab = last.hashOf[s.Label.Value]
   304  				}
   305  				c := hashTuple(h, last.hashOf[s.Object.Value], last.hashOf[s.Predicate.Value], lab, []byte{'+'})
   306  				bag.add(s.Subject.Value, c)
   307  			}
   308  
   309  			if isBlank(s.Object.Value) {
   310  				var lab []byte
   311  				if s.Label.Value != "" {
   312  					lab = last.hashOf[s.Label.Value]
   313  				}
   314  				c := hashTuple(h, last.hashOf[s.Subject.Value], last.hashOf[s.Predicate.Value], lab, []byte{'-'})
   315  				bag.add(s.Object.Value, c)
   316  			}
   317  
   318  			// This and the lab value above implement the label hashing
   319  			// required for RDF dataset hashing as described in
   320  			// https://doi.org/10.5281/zenodo.3154322 v1.0
   321  			// Readme.md#adaptation-of-the-algorithms-to-handle-datasets.
   322  			if isBlank(s.Label.Value) {
   323  				c := hashTuple(h, last.hashOf[s.Subject.Value], last.hashOf[s.Predicate.Value], last.hashOf[s.Object.Value], []byte{'.'})
   324  				bag.add(s.Label.Value, c)
   325  			}
   326  		}
   327  
   328  		for t := range bag.hashesFor {
   329  			curr.set(t, bag.sum(t))
   330  		}
   331  
   332  		disjoint = curr.allUnique()
   333  		if disjoint || !curr.changedFrom(last) {
   334  			return curr, disjoint
   335  		}
   336  	}
   337  }
   338  
   339  // table is a collision aware hash collection for RDF terms.
   340  type table struct {
   341  	// hashOf holds the hash for each term.
   342  	hashOf map[string][]byte
   343  	// termsFor holds the set of nodes in
   344  	// the second key for terms that share
   345  	// the hash in the first key.
   346  	termsFor map[string]map[string]bool
   347  
   348  	// isBlank and blanks are the set of blank
   349  	// nodes.
   350  	// isBlank is nil for cloned tables.
   351  	isBlank map[string]bool
   352  	// blanks is nil for tables created
   353  	// with newTable.
   354  	blanks []string
   355  }
   356  
   357  // newTable returns a new hash table.
   358  func newTable() *table {
   359  	return &table{
   360  		hashOf:   make(map[string][]byte),
   361  		termsFor: make(map[string]map[string]bool),
   362  		isBlank:  make(map[string]bool),
   363  	}
   364  }
   365  
   366  // wasCloned returns whether t is a parent or child of a cloning operation.
   367  func (t *table) wasCloned() bool { return t.isBlank == nil }
   368  
   369  // isNew returns whether t is a new table.
   370  func (t *table) isNew() bool { return t.blanks == nil }
   371  
   372  // clone returns a clone of the receiver.
   373  func (t *table) clone() *table {
   374  	new := &table{
   375  		hashOf:   make(map[string][]byte),
   376  		termsFor: make(map[string]map[string]bool),
   377  	}
   378  	for term, hash := range t.hashOf {
   379  		new.hashOf[term] = hash
   380  	}
   381  	for hash, coll := range t.termsFor {
   382  		if len(coll) == 0 {
   383  			continue
   384  		}
   385  		terms := make(map[string]bool)
   386  		for term := range coll {
   387  			terms[term] = true
   388  		}
   389  		new.termsFor[hash] = terms
   390  	}
   391  	if t.isNew() {
   392  		t.blanks = make([]string, len(t.isBlank))
   393  		i := 0
   394  		for n := range t.isBlank {
   395  			t.blanks[i] = n
   396  			i++
   397  		}
   398  		t.isBlank = nil
   399  	}
   400  	new.blanks = t.blanks
   401  	return new
   402  }
   403  
   404  // TODO(kortschak): Make hash table in table.hashOf reuse the []byte on update.
   405  // This is not trivial since we need to check for changes, so we can't just get
   406  // the current hash buffer and write into it. So if this is done we probably
   407  // a pair of buffers, a current and a waiting.
   408  
   409  // set sets the hash of the term, removing any previously set hash.
   410  func (t *table) set(term string, hash []byte) {
   411  	prev := t.hashOf[term]
   412  	if bytes.Equal(prev, hash) {
   413  		return
   414  	}
   415  	t.hashOf[term] = hash
   416  
   417  	// Delete any existing hashes for this term.
   418  	switch terms := t.termsFor[string(prev)]; {
   419  	case len(terms) == 1:
   420  		delete(t.termsFor, string(prev))
   421  	case len(terms) > 1:
   422  		delete(terms, term)
   423  	}
   424  
   425  	terms, ok := t.termsFor[string(hash)]
   426  	if ok {
   427  		terms[term] = true
   428  	} else {
   429  		t.termsFor[string(hash)] = map[string]bool{term: true}
   430  	}
   431  
   432  	if !t.wasCloned() && isBlank(term) {
   433  		// We are in the original table, so note
   434  		// any blank node label that we see.
   435  		t.isBlank[term] = true
   436  	}
   437  }
   438  
   439  // allUnique returns whether every term has an unique hash. allUnique
   440  // can only be called on a table that was returned by clone.
   441  func (t *table) allUnique() bool {
   442  	if t.isNew() {
   443  		panic("checked hash bag from uncloned table")
   444  	}
   445  	for _, term := range t.blanks {
   446  		if len(t.termsFor[string(t.hashOf[term])]) > 1 {
   447  			return false
   448  		}
   449  	}
   450  	return true
   451  }
   452  
   453  // changedFrom returns whether the receiver has been updated from last.
   454  // changedFrom can only be called on a table that was returned by clone.
   455  func (t *table) changedFrom(last *table) bool {
   456  	if t.isNew() {
   457  		panic("checked hash bag from uncloned table")
   458  	}
   459  	for i, x := range t.blanks {
   460  		for _, y := range t.blanks[i+1:] {
   461  			if bytes.Equal(t.hashOf[x], t.hashOf[y]) != bytes.Equal(last.hashOf[x], last.hashOf[y]) {
   462  				return true
   463  			}
   464  		}
   465  	}
   466  	return false
   467  }
   468  
   469  // hashBag implements a commutative and associative hash.
   470  // See notes in https://doi.org/10.5281/zenodo.3154322 v1.0
   471  // Readme.md#what-is-the-precise-specification-of-hashbag.
   472  type hashBag struct {
   473  	hash      hash.Hash
   474  	hashesFor map[string][][]byte
   475  }
   476  
   477  // newHashBag returns a new hashBag using the provided hash function for
   478  // the given hash table. newHashBag can only take a table parameter that
   479  // was returned by newTable.
   480  func newHashBag(h hash.Hash, t *table) hashBag {
   481  	if t.wasCloned() {
   482  		panic("made hash bag from cloned table")
   483  	}
   484  	b := hashBag{hash: h, hashesFor: make(map[string][][]byte, len(t.isBlank))}
   485  	for n := range t.isBlank {
   486  		b.hashesFor[n] = [][]byte{t.hashOf[n]}
   487  	}
   488  	return b
   489  }
   490  
   491  // add adds the hash to the hash bag for the term.
   492  func (b hashBag) add(term string, hash []byte) {
   493  	b.hashesFor[term] = append(b.hashesFor[term], hash)
   494  }
   495  
   496  // sum calculates the hash sum for the given term, updates the hash bag
   497  // state and returns the hash.
   498  func (b hashBag) sum(term string) []byte {
   499  	p := b.hashesFor[term]
   500  	sort.Sort(lexical(p))
   501  	h := hashTuple(b.hash, p...)
   502  	b.hashesFor[term] = b.hashesFor[term][:1]
   503  	b.hashesFor[term][0] = h
   504  	return h
   505  }
   506  
   507  // lexical implements lexical sorting of [][]byte.
   508  type lexical [][]byte
   509  
   510  func (b lexical) Len() int           { return len(b) }
   511  func (b lexical) Less(i, j int) bool { return string(b[i]) < string(b[j]) }
   512  func (b lexical) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   513  
   514  // hashTuple returns the h hash of the concatenation of t.
   515  func hashTuple(h hash.Hash, t ...[]byte) []byte {
   516  	h.Reset()
   517  	for _, b := range t {
   518  		h.Write(b) //nolint:errcheck
   519  	}
   520  	return h.Sum(nil)
   521  }
   522  
   523  // hashBNodesPerSplit returns the independently hashed blank nodes of the
   524  // graph described by statements using the provided hash function. Hashes
   525  // are initialised with zero.
   526  //
   527  // This is algorithm 2 in doi:10.1145/3068333.
   528  func hashBNodesPerSplit(statements []*Statement, decomp bool, h hash.Hash, zero []byte) (hash *table, parts byLengthHash, disjoint bool) {
   529  	if !decomp {
   530  		hash, ok := hashBNodes(statements, h, zero, nil)
   531  		parts = appendOrdered(byLengthHash{}, hash.termsFor)
   532  		sort.Sort(parts)
   533  		return hash, parts, ok
   534  	}
   535  
   536  	splits := split(statements)
   537  
   538  	// Avoid recombination work if there is only one split.
   539  	if len(splits) == 1 {
   540  		hash, ok := hashBNodes(statements, h, zero, nil)
   541  		parts = appendOrdered(byLengthHash{}, hash.termsFor)
   542  		sort.Sort(parts)
   543  		return hash, parts, ok
   544  	}
   545  
   546  	hash = &table{hashOf: make(map[string][]byte)}
   547  	disjoint = true
   548  	for _, g := range splits {
   549  		part, ok := hashBNodes(g, h, zero, nil)
   550  		// Each split is guaranteed to be disjoint in its
   551  		// set of blank nodes, so we can just append to our
   552  		// collection of blanks.
   553  		hash.blanks = append(hash.blanks, part.blanks...)
   554  		if !ok {
   555  			// Allow a short-circuit of the allUnique check.
   556  			disjoint = false
   557  		}
   558  		for k, v := range part.hashOf {
   559  			hash.hashOf[k] = v
   560  		}
   561  		parts = appendOrdered(parts, part.termsFor)
   562  	}
   563  	sort.Sort(parts)
   564  	return hash, parts, disjoint && allUnique(hash.hashOf)
   565  }
   566  
   567  // appendOrdered adds parts (labels stored in the second key) for each
   568  // hash (stored in the first key) to parts.
   569  func appendOrdered(parts byLengthHash, partSets map[string]map[string]bool) byLengthHash {
   570  	for h, s := range partSets {
   571  		var p []string
   572  		for e := range s {
   573  			if isBlank(e) {
   574  				p = append(p, e)
   575  			}
   576  		}
   577  		if p != nil {
   578  			parts.nodes = append(parts.nodes, p)
   579  			parts.hashes = append(parts.hashes, h)
   580  		}
   581  	}
   582  	return parts
   583  }
   584  
   585  // byLengthHash implements ascending length sort of a set of blank RDF
   586  // term partitions with ties broken by lexical ordering of the partitions'
   587  // hashes.
   588  type byLengthHash struct {
   589  	// nodes holds the blank nodes of a part.
   590  	nodes [][]string
   591  	// hashes holds the hashes corresponding
   592  	// to the nodes in the nodes field, using
   593  	// the same index.
   594  	hashes []string
   595  }
   596  
   597  func (s byLengthHash) Len() int { return len(s.nodes) }
   598  func (s byLengthHash) Less(i, j int) bool {
   599  	switch {
   600  	case len(s.nodes[i]) < len(s.nodes[j]):
   601  		return true
   602  	case len(s.nodes[i]) > len(s.nodes[j]):
   603  		return false
   604  	}
   605  	return s.hashes[i] < s.hashes[j]
   606  }
   607  func (s byLengthHash) Swap(i, j int) {
   608  	s.nodes[i], s.nodes[j] = s.nodes[j], s.nodes[i]
   609  	s.hashes[i], s.hashes[j] = s.hashes[j], s.hashes[i]
   610  }
   611  
   612  // allUnique returns whether the []byte hash values in hashes are all unique.
   613  func allUnique(hashes map[string][]byte) bool {
   614  	set := make(map[string]bool)
   615  	for _, h := range hashes {
   616  		if set[string(h)] {
   617  			return false
   618  		}
   619  		set[string(h)] = true
   620  	}
   621  	return true
   622  }
   623  
   624  // split returns the statements forming connected components in the graph
   625  // described by statements.
   626  //
   627  // This is split in algorithm 2 in doi:10.1145/3068333.
   628  func split(statements []*Statement) [][]*Statement {
   629  	ds := make(djSet)
   630  	for _, s := range statements {
   631  		ds.add(s.Subject.Value)
   632  		ds.add(s.Object.Value)
   633  		if isBlank(s.Subject.Value) && isBlank(s.Object.Value) {
   634  			ds.union(ds.find(s.Subject.Value), ds.find(s.Object.Value))
   635  		}
   636  	}
   637  
   638  	var (
   639  		splits [][]*Statement
   640  		ground []*Statement
   641  	)
   642  	idxOf := make(map[*dsNode]int)
   643  	for _, s := range statements {
   644  		var t string
   645  		switch {
   646  		case isBlank(s.Subject.Value):
   647  			t = s.Subject.Value
   648  		case isBlank(s.Object.Value):
   649  			t = s.Object.Value
   650  		default:
   651  			ground = append(ground, s)
   652  			continue
   653  		}
   654  		r := ds.find(t)
   655  		if r == nil {
   656  			panic(fmt.Sprintf("term not found: %q", t))
   657  		}
   658  		i, ok := idxOf[r]
   659  		if !ok {
   660  			i = len(splits)
   661  			idxOf[r] = i
   662  			splits = append(splits, []*Statement{s})
   663  		} else {
   664  			splits[i] = append(splits[i], s)
   665  		}
   666  	}
   667  	if ground != nil {
   668  		splits = append(splits, ground)
   669  	}
   670  
   671  	if debug {
   672  		debug.log(0, "Splits:")
   673  		for i, s := range splits {
   674  			for j, t := range s {
   675  				if j == 0 {
   676  					debug.logf(0, "%d.\t%s\n", i+1, t)
   677  				} else {
   678  					debug.logf(0, "\t%s\n", t)
   679  				}
   680  			}
   681  			debug.log(0)
   682  		}
   683  	}
   684  
   685  	return splits
   686  }
   687  
   688  // distinguish returns G⊥: smallest hash-labelled graph found thus far.
   689  // The graph is returned as a node to hash lookup.
   690  //
   691  // This is part of algorithm 3 in doi:10.1145/3068333.
   692  //
   693  // The correspondence between the parameters for the function in the paper
   694  // with the implementation here is as follows:
   695  //  - G = statements
   696  //  - hash = hash
   697  //  - P = parts (already sorted by hashBNodesPerSplit)
   698  //  - G⊥ = lowest
   699  //  - B = hash.blanks
   700  // The additional parameter dist specifies that distinguish should treat
   701  // coequal trivial parts as a coarse of intermediate part and distinguish
   702  // the nodes in that merged part.
   703  func distinguish(statements []*Statement, dist bool, h hash.Hash, zero []byte, hash *table, parts byLengthHash, lowest map[string][]byte, depth int) map[string][]byte {
   704  	if debug {
   705  		debug.log(depth, "Running Distinguish")
   706  	}
   707  
   708  	var small []string
   709  	var k int
   710  	for k, small = range parts.nodes {
   711  		if len(small) > 1 {
   712  			break
   713  		}
   714  	}
   715  	if len(small) < 2 {
   716  		if lowest != nil || !dist {
   717  			if debug {
   718  				debug.log(depth, "Return lowest (no non-trivial parts):")
   719  				debug.logHashes(depth, lowest, h.Size())
   720  			}
   721  
   722  			return lowest
   723  		}
   724  
   725  		// We have been given a set of fine parts,
   726  		// but to reach here they must have been
   727  		// non-uniquely labeled, so treat them
   728  		// as a single coarse part.
   729  		k, small = 0, parts.nodes[0]
   730  	}
   731  
   732  	if debug {
   733  		debug.logf(depth, "Part: %v %x\n\n", small, parts.hashes[k])
   734  		debug.log(depth, "Orig hash:")
   735  		debug.logHashes(depth, hash.hashOf, h.Size())
   736  	}
   737  
   738  	smallHash := hash.hashOf[small[0]]
   739  	for _, p := range parts.nodes[k:] {
   740  		if !bytes.Equal(smallHash, hash.hashOf[p[0]]) {
   741  
   742  			if debug {
   743  				debug.logf(depth, "End of co-equal hashes: %x != %x\n\n", smallHash, hash.hashOf[p[0]])
   744  			}
   745  
   746  			break
   747  		}
   748  		for i, b := range p {
   749  
   750  			if debug {
   751  				debug.logf(depth, "Iter: %d — B = %q\n\n", i, b)
   752  
   753  				if depth == 0 {
   754  					debug.log(depth, "Current lowest:\n")
   755  					debug.logHashes(depth, lowest, h.Size())
   756  				}
   757  			}
   758  
   759  			hashP := hash.clone()
   760  			hashP.set(b, hashTuple(h, hashP.hashOf[b], []byte{'@'}))
   761  			hashPP, ok := hashBNodes(statements, h, zero, hashP.hashOf)
   762  			if ok {
   763  
   764  				if debug {
   765  					debug.log(depth, "hashPP is trivial")
   766  					debug.log(depth, "comparing hashPP\n")
   767  					debug.logHashes(depth, hashPP.hashOf, h.Size())
   768  					debug.log(depth, "with previous\n")
   769  					debug.logHashes(depth, lowest, h.Size())
   770  				}
   771  
   772  				if lowest == nil || graphLess(statements, hashPP.hashOf, lowest) {
   773  					lowest = hashPP.hashOf
   774  					debug.log(depth, "choose hashPP\n")
   775  				}
   776  			} else {
   777  				partsP := appendOrdered(byLengthHash{}, hashPP.termsFor)
   778  				sort.Sort(partsP)
   779  
   780  				if debug {
   781  					debug.log(depth, "Parts':")
   782  					debug.logParts(depth, partsP)
   783  					debug.log(depth, "Recursive distinguish")
   784  					debug.log(depth, "Called with current lowest:\n")
   785  					debug.logHashes(depth, lowest, h.Size())
   786  				}
   787  
   788  				lowest = distinguish(statements, dist, h, zero, hashPP, partsP, lowest, depth+1)
   789  			}
   790  		}
   791  	}
   792  
   793  	if debug {
   794  		debug.log(depth, "Return lowest:")
   795  		debug.logHashes(depth, lowest, h.Size())
   796  	}
   797  
   798  	return lowest
   799  }
   800  
   801  // terms ordered syntactically, triples ordered lexicographically, and graphs
   802  // ordered such that G < H if and only if G ⊂ H or there exists a triple
   803  // t ∈ G \ H such that no triple t' ∈ H \ G exists where t' < t.
   804  // p9 https://doi.org/10.1145/3068333
   805  func graphLess(statements []*Statement, a, b map[string][]byte) bool {
   806  	g := newLexicalStatements(statements, a)
   807  	sort.Sort(g)
   808  	h := newLexicalStatements(statements, b)
   809  	sort.Sort(h)
   810  
   811  	gSubH := sub(g, h, len(g.statements))
   812  	if len(gSubH) == 0 {
   813  		return true
   814  	}
   815  
   816  	hSubG := sub(h, g, 1)
   817  	if len(hSubG) == 0 {
   818  		return true
   819  	}
   820  	lowestH := relabeledStatement{hSubG[0], h.hashes}
   821  
   822  	for _, s := range gSubH {
   823  		rs := relabeledStatement{s, g.hashes}
   824  		if rs.less(lowestH) {
   825  			return true
   826  		}
   827  	}
   828  	return false
   829  }
   830  
   831  // lexicalStatements is a sort implementation for Statements with blank
   832  // node labels replaced with their hash.
   833  type lexicalStatements struct {
   834  	statements []*Statement
   835  	hashes     map[string][]byte
   836  }
   837  
   838  func newLexicalStatements(statements []*Statement, hash map[string][]byte) lexicalStatements {
   839  	s := lexicalStatements{
   840  		statements: make([]*Statement, len(statements)),
   841  		hashes:     hash,
   842  	}
   843  	copy(s.statements, statements)
   844  	return s
   845  }
   846  
   847  // sub returns the difference between a and b up to max elements long.
   848  func sub(a, b lexicalStatements, max int) []*Statement {
   849  	var d []*Statement
   850  	var i, j int
   851  	for i < len(a.statements) && j < len(b.statements) && len(d) < max {
   852  		ra := relabeledStatement{a.statements[i], a.hashes}
   853  		rb := relabeledStatement{b.statements[j], b.hashes}
   854  		switch {
   855  		case ra.less(rb):
   856  			d = append(d, a.statements[i])
   857  			i++
   858  		case rb.less(ra):
   859  			j++
   860  		default:
   861  			i++
   862  		}
   863  	}
   864  	if len(d) < max {
   865  		d = append(d, a.statements[i:min(len(a.statements), i+max-len(d))]...)
   866  	}
   867  	return d
   868  }
   869  
   870  func min(a, b int) int {
   871  	if a < b {
   872  		return a
   873  	}
   874  	return b
   875  }
   876  
   877  func (s lexicalStatements) Len() int { return len(s.statements) }
   878  func (s lexicalStatements) Less(i, j int) bool {
   879  	return relabeledStatement{s.statements[i], s.hashes}.less(relabeledStatement{s.statements[j], s.hashes})
   880  }
   881  func (s lexicalStatements) Swap(i, j int) {
   882  	s.statements[i], s.statements[j] = s.statements[j], s.statements[i]
   883  }
   884  
   885  // relabeledStatement is a statement that is orderable by its blank node
   886  // hash relabeling.
   887  type relabeledStatement struct {
   888  	statement *Statement
   889  	labels    map[string][]byte
   890  }
   891  
   892  func (a relabeledStatement) less(b relabeledStatement) bool {
   893  	switch {
   894  	case relabeledTerm{a.statement.Subject, a.labels}.less(relabeledTerm{b.statement.Subject, b.labels}):
   895  		return true
   896  	case relabeledTerm{b.statement.Subject, b.labels}.less(relabeledTerm{a.statement.Subject, a.labels}):
   897  		return false
   898  	}
   899  	switch { // Always IRI.
   900  	case a.statement.Predicate.Value < b.statement.Predicate.Value:
   901  		return true
   902  	case a.statement.Predicate.Value > b.statement.Predicate.Value:
   903  		return false
   904  	}
   905  	switch {
   906  	case relabeledTerm{a.statement.Object, a.labels}.less(relabeledTerm{b.statement.Object, b.labels}):
   907  		return true
   908  	case relabeledTerm{b.statement.Object, b.labels}.less(relabeledTerm{a.statement.Object, a.labels}):
   909  		return false
   910  	}
   911  	return relabeledTerm{a.statement.Label, a.labels}.less(relabeledTerm{b.statement.Label, b.labels})
   912  }
   913  
   914  func (s relabeledStatement) String() string {
   915  	subj := relabeledTerm{term: s.statement.Subject, labels: s.labels}
   916  	obj := relabeledTerm{term: s.statement.Object, labels: s.labels}
   917  	if s.statement.Label.Value == "" {
   918  		return fmt.Sprintf("%s %s %s .", subj, s.statement.Predicate.Value, obj)
   919  	}
   920  	lab := relabeledTerm{term: s.statement.Label, labels: s.labels}
   921  	return fmt.Sprintf("%s %s %s %s .", subj, s.statement.Predicate.Value, obj, lab)
   922  }
   923  
   924  // relabeledTerm is a term that is orderable by its blank node hash relabeling.
   925  type relabeledTerm struct {
   926  	term   Term
   927  	labels map[string][]byte
   928  }
   929  
   930  func (a relabeledTerm) less(b relabeledTerm) bool {
   931  	aIsBlank := isBlank(a.term.Value)
   932  	bIsBlank := isBlank(b.term.Value)
   933  	switch {
   934  	case aIsBlank && bIsBlank:
   935  		return bytes.Compare(a.labels[a.term.Value], b.labels[b.term.Value]) < 0
   936  	case aIsBlank:
   937  		return blankPrefix < unquoteIRI(b.term.Value)
   938  	case bIsBlank:
   939  		return unquoteIRI(a.term.Value) < blankPrefix
   940  	default:
   941  		return unquoteIRI(a.term.Value) < unquoteIRI(b.term.Value)
   942  	}
   943  }
   944  
   945  func unquoteIRI(s string) string {
   946  	if len(s) > 1 && s[0] == '<' && s[len(s)-1] == '>' {
   947  		s = s[1 : len(s)-1]
   948  	}
   949  	return s
   950  }
   951  
   952  func (t relabeledTerm) String() string {
   953  	if !isBlank(t.term.Value) {
   954  		return t.term.Value
   955  	}
   956  	h, ok := t.labels[t.term.Value]
   957  	if !ok {
   958  		return t.term.Value + "_missing_hash"
   959  	}
   960  	return fmt.Sprintf("_:%0x", h)
   961  }