gonum.org/v1/gonum@v0.14.0/graph/formats/rdf/equi_canonical_test.go (about)

     1  // Copyright ©2021 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  	"fmt"
     9  	"io"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  )
    14  
    15  func TestRemoveRedundantNodes(t *testing.T) {
    16  	tests := []struct {
    17  		name       string
    18  		statements string
    19  		want       string
    20  	}{
    21  		{
    22  			name: "Example 5.1",
    23  			statements: `
    24  <ex:Chile> <ex:cabinet> _:b1 .
    25  <ex:Chile> <ex:cabinet> _:b2 .
    26  <ex:Chile> <ex:cabinet> _:b3 .
    27  <ex:Chile> <ex:cabinet> _:b4 .
    28  <ex:Chile> <ex:presidency> _:a1 .
    29  <ex:Chile> <ex:presidency> _:a2 .
    30  <ex:Chile> <ex:presidency> _:a3 .
    31  <ex:Chile> <ex:presidency> _:a4 .
    32  <ex:MBachelet> <ex:spouse> _:c .
    33  _:a1 <ex:next> _:a2 .
    34  _:a2 <ex:next> _:a3 .
    35  _:a2 <ex:president> <ex:MBachelet> .
    36  _:a3 <ex:next> _:a4 .
    37  _:a4 <ex:president> <ex:MBachelet> .
    38  _:b2 <ex:members> "23" .
    39  _:b3 <ex:members> "23" .
    40  `,
    41  			want: `<ex:Chile> <ex:cabinet> _:b3 .
    42  <ex:Chile> <ex:presidency> _:a1 .
    43  <ex:Chile> <ex:presidency> _:a2 .
    44  <ex:Chile> <ex:presidency> _:a3 .
    45  <ex:Chile> <ex:presidency> _:a4 .
    46  <ex:MBachelet> <ex:spouse> _:c .
    47  _:a1 <ex:next> _:a2 .
    48  _:a2 <ex:next> _:a3 .
    49  _:a2 <ex:president> <ex:MBachelet> .
    50  _:a3 <ex:next> _:a4 .
    51  _:a4 <ex:president> <ex:MBachelet> .
    52  _:b3 <ex:members> "23" .
    53  `,
    54  		},
    55  		{
    56  			name: "Example 5.2",
    57  			statements: `
    58  _:a <ex:p> _:b .
    59  _:a <ex:p> _:d .
    60  _:c <ex:p> _:b .
    61  _:c <ex:p> _:f .
    62  _:e <ex:p> _:b .
    63  _:e <ex:p> _:d .
    64  _:e <ex:p> _:f .
    65  _:e <ex:p> _:h .
    66  _:g <ex:p> _:d .
    67  _:g <ex:p> _:h .
    68  _:i <ex:p> _:f .
    69  _:i <ex:p> _:h .
    70  `,
    71  			want: `_:e <ex:p> _:b .
    72  `,
    73  		},
    74  	}
    75  
    76  	for _, test := range tests {
    77  		g := parseStatements(strings.NewReader(test.statements))
    78  		gWant := parseStatements(strings.NewReader(test.want))
    79  
    80  		g = removeRedundantBnodes(g)
    81  
    82  		got := canonicalStatements(g)
    83  		want := canonicalStatements(gWant)
    84  		if got != want {
    85  			got = formatStatements(g)
    86  			t.Errorf("unexpected result for %s:\ngot: \n%s\nwant:\n%s",
    87  				test.name, got, test.want)
    88  		}
    89  
    90  	}
    91  }
    92  func TestFindCandidates(t *testing.T) {
    93  	tests := []struct {
    94  		name         string
    95  		statements   string
    96  		want         string
    97  		wantFixed    []map[string]bool
    98  		wantAllFixed bool
    99  		wantCands    []map[string]map[string]bool
   100  	}{
   101  		{
   102  			name: "Example 5.1",
   103  			statements: `
   104  <ex:Chile> <ex:cabinet> _:b1 .
   105  <ex:Chile> <ex:cabinet> _:b2 .
   106  <ex:Chile> <ex:cabinet> _:b3 .
   107  <ex:Chile> <ex:cabinet> _:b4 .
   108  <ex:Chile> <ex:presidency> _:a1 .
   109  <ex:Chile> <ex:presidency> _:a2 .
   110  <ex:Chile> <ex:presidency> _:a3 .
   111  <ex:Chile> <ex:presidency> _:a4 .
   112  <ex:MBachelet> <ex:spouse> _:c .
   113  _:a1 <ex:next> _:a2 .
   114  _:a2 <ex:next> _:a3 .
   115  _:a2 <ex:president> <ex:MBachelet> .
   116  _:a3 <ex:next> _:a4 .
   117  _:a4 <ex:president> <ex:MBachelet> .
   118  _:b2 <ex:members> "23" .
   119  _:b3 <ex:members> "23" .
   120  `,
   121  			want: `<ex:Chile> <ex:cabinet> _:b3 .
   122  <ex:Chile> <ex:presidency> _:a1 .
   123  <ex:Chile> <ex:presidency> _:a2 .
   124  <ex:Chile> <ex:presidency> _:a3 .
   125  <ex:Chile> <ex:presidency> _:a4 .
   126  <ex:MBachelet> <ex:spouse> _:c .
   127  _:a1 <ex:next> _:a2 .
   128  _:a2 <ex:next> _:a3 .
   129  _:a2 <ex:president> <ex:MBachelet> .
   130  _:a3 <ex:next> _:a4 .
   131  _:a4 <ex:president> <ex:MBachelet> .
   132  _:b3 <ex:members> "23" .
   133  `,
   134  			// Hence, in this particular case, we have managed to fix all blank
   135  			// nodes, and the graph is thus lean, and we need to go no further.
   136  			// In other cases we will look at later, however, some blank nodes
   137  			// may maintain multiple candidates.
   138  			//
   139  			// Note that there are two valid labellings of the graph since _:b2
   140  			// and _:b3 are not distinguishable.
   141  			wantFixed: []map[string]bool{
   142  				{
   143  					"_:a1": true, "_:a2": true, "_:a3": true, "_:a4": true,
   144  					"_:b2": true, "_:c": true,
   145  				},
   146  				{
   147  					"_:a1": true, "_:a2": true, "_:a3": true, "_:a4": true,
   148  					"_:b3": true, "_:c": true,
   149  				},
   150  			},
   151  			wantAllFixed: true,
   152  			wantCands: []map[string]map[string]bool{
   153  				{
   154  					"_:a1": {"_:a1": true},
   155  					"_:a2": {"_:a2": true},
   156  					"_:a3": {"_:a3": true},
   157  					"_:a4": {"_:a4": true},
   158  					"_:b2": {"_:b2": true},
   159  					"_:c":  {"_:c": true},
   160  				},
   161  				{
   162  					"_:a1": {"_:a1": true},
   163  					"_:a2": {"_:a2": true},
   164  					"_:a3": {"_:a3": true},
   165  					"_:a4": {"_:a4": true},
   166  					"_:b3": {"_:b3": true},
   167  					"_:c":  {"_:c": true},
   168  				},
   169  			},
   170  		},
   171  		{
   172  			name: "Example 5.6", // This is 5.1, but simplified.
   173  			statements: `
   174  <ex:Chile> <ex:cabinet> _:b3 .
   175  <ex:Chile> <ex:presidency> _:a1 .
   176  <ex:Chile> <ex:presidency> _:a2 .
   177  <ex:Chile> <ex:presidency> _:a3 .
   178  <ex:Chile> <ex:presidency> _:a4 .
   179  <ex:MBachelet> <ex:spouse> _:c .
   180  _:a1 <ex:next> _:a2 .
   181  _:a2 <ex:next> _:a3 .
   182  _:a2 <ex:president> <ex:MBachelet> .
   183  _:a3 <ex:next> _:a4 .
   184  _:a4 <ex:president> <ex:MBachelet> .
   185  _:b3 <ex:members> "23" .
   186  `,
   187  			want: `<ex:Chile> <ex:cabinet> _:b3 .
   188  <ex:Chile> <ex:presidency> _:a1 .
   189  <ex:Chile> <ex:presidency> _:a2 .
   190  <ex:Chile> <ex:presidency> _:a3 .
   191  <ex:Chile> <ex:presidency> _:a4 .
   192  <ex:MBachelet> <ex:spouse> _:c .
   193  _:a1 <ex:next> _:a2 .
   194  _:a2 <ex:next> _:a3 .
   195  _:a2 <ex:president> <ex:MBachelet> .
   196  _:a3 <ex:next> _:a4 .
   197  _:a4 <ex:president> <ex:MBachelet> .
   198  _:b3 <ex:members> "23" .
   199  `,
   200  			// Hence, in this particular case, we have managed to fix all blank
   201  			// nodes, and the graph is thus lean, and we need to go no further.
   202  			// In other cases we will look at later, however, some blank nodes
   203  			// may maintain multiple candidates.
   204  			wantFixed: []map[string]bool{{
   205  				"_:a1": true, "_:a2": true, "_:a3": true, "_:a4": true,
   206  				"_:b3": true, "_:c": true,
   207  			}},
   208  			wantAllFixed: true,
   209  			wantCands: []map[string]map[string]bool{{
   210  				"_:a1": {"_:a1": true},
   211  				"_:a2": {"_:a2": true},
   212  				"_:a3": {"_:a3": true},
   213  				"_:a4": {"_:a4": true},
   214  				"_:b3": {"_:b3": true},
   215  				"_:c":  {"_:c": true},
   216  			}},
   217  		},
   218  		{
   219  			name: "Example 5.9",
   220  			statements: `
   221  <ex:Chile> <ex:presidency> _:a1 .
   222  <ex:Chile> <ex:presidency> _:a2 .
   223  <ex:Chile> <ex:presidency> _:a3 .
   224  <ex:Chile> <ex:presidency> _:a4 .
   225  _:a1 <ex:next> _:a2 .
   226  _:a2 <ex:next> _:a3 .
   227  _:a3 <ex:next> _:a4 .
   228  		`,
   229  			want: `<ex:Chile> <ex:presidency> _:a1 .
   230  <ex:Chile> <ex:presidency> _:a2 .
   231  <ex:Chile> <ex:presidency> _:a3 .
   232  <ex:Chile> <ex:presidency> _:a4 .
   233  _:a1 <ex:next> _:a2 .
   234  _:a2 <ex:next> _:a3 .
   235  _:a3 <ex:next> _:a4 .
   236  `,
   237  			wantFixed:    []map[string]bool{nil},
   238  			wantAllFixed: true,
   239  			wantCands: []map[string]map[string]bool{{
   240  				"_:a1": {"_:a1": true, "_:a2": true, "_:a3": true},
   241  				"_:a2": {"_:a2": true, "_:a3": true},
   242  				"_:a3": {"_:a2": true, "_:a3": true},
   243  				"_:a4": {"_:a2": true, "_:a3": true, "_:a4": true},
   244  			}},
   245  		},
   246  		{
   247  			name: "Example 5.10",
   248  			statements: `
   249  _:a <ex:p> _:b .
   250  _:a <ex:p> _:d .
   251  _:b <ex:q> _:e .
   252  _:c <ex:p> _:b .
   253  _:c <ex:p> _:f .
   254  _:d <ex:q> _:e .
   255  _:f <ex:q> _:e .
   256  _:g <ex:p> _:d .
   257  _:g <ex:p> _:h .
   258  _:h <ex:q> _:e .
   259  _:i <ex:p> _:f .
   260  _:i <ex:p> _:h .
   261  `,
   262  			want: `_:a <ex:p> _:b .
   263  _:a <ex:p> _:d .
   264  _:b <ex:q> _:e .
   265  _:c <ex:p> _:b .
   266  _:c <ex:p> _:f .
   267  _:d <ex:q> _:e .
   268  _:f <ex:q> _:e .
   269  _:g <ex:p> _:d .
   270  _:g <ex:p> _:h .
   271  _:h <ex:q> _:e .
   272  _:i <ex:p> _:f .
   273  _:i <ex:p> _:h .
   274  `,
   275  			wantFixed:    []map[string]bool{{"_:e": true}},
   276  			wantAllFixed: false,
   277  			wantCands: []map[string]map[string]bool{{
   278  				"_:a": {"_:a": true, "_:c": true, "_:g": true, "_:i": true},
   279  				"_:b": {"_:b": true, "_:d": true, "_:f": true, "_:h": true},
   280  				"_:c": {"_:a": true, "_:c": true, "_:g": true, "_:i": true},
   281  				"_:d": {"_:b": true, "_:d": true, "_:f": true, "_:h": true},
   282  				"_:e": {"_:e": true},
   283  				"_:f": {"_:b": true, "_:d": true, "_:f": true, "_:h": true},
   284  				"_:g": {"_:a": true, "_:c": true, "_:g": true, "_:i": true},
   285  				"_:h": {"_:b": true, "_:d": true, "_:f": true, "_:h": true},
   286  				"_:i": {"_:a": true, "_:c": true, "_:g": true, "_:i": true},
   287  			}},
   288  		},
   289  	}
   290  
   291  	for _, test := range tests[:1] {
   292  		g := parseStatements(strings.NewReader(test.statements))
   293  		gWant := parseStatements(strings.NewReader(test.want))
   294  
   295  		g, fixed, cands, allFixed := findCandidates(g)
   296  
   297  		got := canonicalStatements(g)
   298  		want := canonicalStatements(gWant)
   299  		if got != want {
   300  			got = formatStatements(g)
   301  			t.Errorf("unexpected result for %s:\ngot: \n%s\nwant:\n%s",
   302  				test.name, got, test.want)
   303  		}
   304  
   305  		matchedFixed := false
   306  		for _, wantFixed := range test.wantFixed {
   307  			if reflect.DeepEqual(fixed, wantFixed) {
   308  				matchedFixed = true
   309  				break
   310  			}
   311  		}
   312  		if !matchedFixed {
   313  			t.Errorf("unexpected fixed result for %s:\ngot: \n%v\nwant:\n%v",
   314  				test.name, fixed, test.wantFixed)
   315  		}
   316  
   317  		if allFixed != test.wantAllFixed {
   318  			t.Errorf("unexpected all-fixed result for %s:\ngot:%t\nwant:%t",
   319  				test.name, allFixed, test.wantAllFixed)
   320  		}
   321  
   322  		matchedCands := false
   323  		for _, wantCands := range test.wantCands {
   324  			if reflect.DeepEqual(cands, wantCands) {
   325  				matchedCands = true
   326  				break
   327  			}
   328  		}
   329  		if !matchedCands {
   330  			t.Errorf("unexpected candidates result for %s:\ngot: \n%v\nwant:\n%v",
   331  				test.name, cands, test.wantCands)
   332  		}
   333  	}
   334  }
   335  
   336  func TestLean(t *testing.T) {
   337  	var tests = []struct {
   338  		name       string
   339  		statements string
   340  		want       string
   341  		wantErr    error
   342  	}{
   343  		{
   344  			name: "Example 5.1",
   345  			statements: `
   346  <ex:Chile> <ex:cabinet> _:b1 .
   347  <ex:Chile> <ex:cabinet> _:b2 .
   348  <ex:Chile> <ex:cabinet> _:b3 .
   349  <ex:Chile> <ex:cabinet> _:b4 .
   350  <ex:Chile> <ex:presidency> _:a1 .
   351  <ex:Chile> <ex:presidency> _:a2 .
   352  <ex:Chile> <ex:presidency> _:a3 .
   353  <ex:Chile> <ex:presidency> _:a4 .
   354  <ex:MBachelet> <ex:spouse> _:c .
   355  _:a1 <ex:next> _:a2 .
   356  _:a2 <ex:next> _:a3 .
   357  _:a2 <ex:president> <ex:MBachelet> .
   358  _:a3 <ex:next> _:a4 .
   359  _:a4 <ex:president> <ex:MBachelet> .
   360  _:b2 <ex:members> "23" .
   361  _:b3 <ex:members> "23" .
   362  `,
   363  			want: `<ex:Chile> <ex:cabinet> _:b3 .
   364  <ex:Chile> <ex:presidency> _:a1 .
   365  <ex:Chile> <ex:presidency> _:a2 .
   366  <ex:Chile> <ex:presidency> _:a3 .
   367  <ex:Chile> <ex:presidency> _:a4 .
   368  <ex:MBachelet> <ex:spouse> _:c .
   369  _:a1 <ex:next> _:a2 .
   370  _:a2 <ex:next> _:a3 .
   371  _:a2 <ex:president> <ex:MBachelet> .
   372  _:a3 <ex:next> _:a4 .
   373  _:a4 <ex:president> <ex:MBachelet> .
   374  _:b3 <ex:members> "23" .
   375  `,
   376  		},
   377  		{
   378  			name: "Example 5.6",
   379  			statements: `
   380  <ex:Chile> <ex:cabinet> _:b3 .
   381  <ex:Chile> <ex:presidency> _:a1 .
   382  <ex:Chile> <ex:presidency> _:a2 .
   383  <ex:Chile> <ex:presidency> _:a3 .
   384  <ex:Chile> <ex:presidency> _:a4 .
   385  <ex:MBachelet> <ex:spouse> _:c .
   386  _:a1 <ex:next> _:a2 .
   387  _:a2 <ex:next> _:a3 .
   388  _:a2 <ex:president> <ex:MBachelet> .
   389  _:a3 <ex:next> _:a4 .
   390  _:a4 <ex:president> <ex:MBachelet> .
   391  _:b3 <ex:members> "23" .
   392  `,
   393  			want: `<ex:Chile> <ex:cabinet> _:b3 .
   394  <ex:Chile> <ex:presidency> _:a1 .
   395  <ex:Chile> <ex:presidency> _:a2 .
   396  <ex:Chile> <ex:presidency> _:a3 .
   397  <ex:Chile> <ex:presidency> _:a4 .
   398  <ex:MBachelet> <ex:spouse> _:c .
   399  _:a1 <ex:next> _:a2 .
   400  _:a2 <ex:next> _:a3 .
   401  _:a2 <ex:president> <ex:MBachelet> .
   402  _:a3 <ex:next> _:a4 .
   403  _:a4 <ex:president> <ex:MBachelet> .
   404  _:b3 <ex:members> "23" .
   405  `,
   406  		},
   407  		{
   408  			name: "Example 5.9",
   409  			statements: `
   410  <ex:Chile> <ex:presidency> _:a1 .
   411  <ex:Chile> <ex:presidency> _:a2 .
   412  <ex:Chile> <ex:presidency> _:a3 .
   413  <ex:Chile> <ex:presidency> _:a4 .
   414  _:a1 <ex:next> _:a2 .
   415  _:a2 <ex:next> _:a3 .
   416  _:a3 <ex:next> _:a4 .
   417  		`,
   418  			want: `<ex:Chile> <ex:presidency> _:a1 .
   419  <ex:Chile> <ex:presidency> _:a2 .
   420  <ex:Chile> <ex:presidency> _:a3 .
   421  <ex:Chile> <ex:presidency> _:a4 .
   422  _:a1 <ex:next> _:a2 .
   423  _:a2 <ex:next> _:a3 .
   424  _:a3 <ex:next> _:a4 .
   425  `,
   426  		},
   427  		{
   428  			name: "Example 5.10",
   429  			statements: `
   430  _:a <ex:p> _:b .
   431  _:a <ex:p> _:d .
   432  _:b <ex:q> _:e .
   433  _:c <ex:p> _:b .
   434  _:c <ex:p> _:f .
   435  _:d <ex:q> _:e .
   436  _:f <ex:q> _:e .
   437  _:g <ex:p> _:d .
   438  _:g <ex:p> _:h .
   439  _:h <ex:q> _:e .
   440  _:i <ex:p> _:f .
   441  _:i <ex:p> _:h .
   442  `,
   443  			want: `_:a <ex:p> _:b .
   444  _:b <ex:q> _:e .
   445  `,
   446  		},
   447  		{
   448  			name: "Example 5.10 halved",
   449  			statements: `
   450  _:a <ex:p> _:b .
   451  _:a <ex:p> _:d .
   452  _:b <ex:q> _:e .
   453  _:c <ex:p> _:b .
   454  _:c <ex:p> _:f .
   455  _:d <ex:q> _:e .
   456  _:f <ex:q> _:e .
   457  `,
   458  			want: `_:a <ex:p> _:b .
   459  _:b <ex:q> _:e .
   460  `,
   461  		},
   462  		{
   463  			name: "Example 5.10 quartered",
   464  			statements: `
   465  _:a <ex:p> _:b .
   466  _:a <ex:p> _:d .
   467  _:b <ex:q> _:e .
   468  _:d <ex:q> _:e .
   469  `,
   470  			want: `_:a <ex:p> _:b .
   471  _:b <ex:q> _:e .
   472  `,
   473  		},
   474  	}
   475  
   476  	for _, test := range tests {
   477  		g := parseStatements(strings.NewReader(test.statements))
   478  		gWant := parseStatements(strings.NewReader(test.want))
   479  
   480  		lean, err := Lean(g)
   481  		if err != test.wantErr {
   482  			t.Errorf("unexpected error for %v: got:%v want:%v",
   483  				test.name, err, test.wantErr)
   484  		}
   485  
   486  		got := canonicalStatements(lean)
   487  		want := canonicalStatements(gWant)
   488  
   489  		if got != want {
   490  			got = formatStatements(g)
   491  			t.Errorf("unexpected result for %s:\ngot: \n%s\nwant:\n%s",
   492  				test.name, got, test.want)
   493  		}
   494  	}
   495  }
   496  
   497  func TestJoin(t *testing.T) {
   498  	var tests = []struct {
   499  		name       string
   500  		q          string
   501  		statements string
   502  		cands      map[string]map[string]bool
   503  		mu         map[string]string
   504  		want       []map[string]string
   505  	}{
   506  		{
   507  			name: "Identity",
   508  			q:    `_:a <ex:p> _:b .`,
   509  			statements: `
   510  _:a <ex:p> _:b .
   511  `,
   512  			cands: map[string]map[string]bool{
   513  				"_:a": {"_:a": true},
   514  				"_:b": {"_:b": true},
   515  			},
   516  			mu: nil,
   517  			want: []map[string]string{
   518  				{"_:a": "_:a", "_:b": "_:b"},
   519  			},
   520  		},
   521  		{
   522  			name: "Cross identity",
   523  			q:    `_:a <ex:p> _:b .`,
   524  			statements: `
   525  _:a <ex:p> _:b .
   526  _:b <ex:p> _:a .
   527  `,
   528  			cands: map[string]map[string]bool{
   529  				"_:a": {"_:a": true, "_:b": true},
   530  				"_:b": {"_:b": true, "_:a": true},
   531  			},
   532  			mu: nil,
   533  			want: []map[string]string{
   534  				{"_:a": "_:a", "_:b": "_:b"},
   535  				{"_:a": "_:b", "_:b": "_:a"},
   536  			},
   537  		},
   538  		{
   539  			name: "Cross identity with restriction",
   540  			q:    `_:a <ex:p> _:b .`,
   541  			statements: `
   542  _:a <ex:p> _:b .
   543  _:b <ex:p> _:a .
   544  `,
   545  			cands: map[string]map[string]bool{
   546  				"_:a": {"_:a": true, "_:b": true},
   547  				"_:b": {"_:b": true, "_:a": true},
   548  			},
   549  			mu: map[string]string{"_:a": "_:a"},
   550  			want: []map[string]string{
   551  				{"_:a": "_:a", "_:b": "_:b"},
   552  			},
   553  		},
   554  		{
   555  			name: "Cross identity with complete restriction",
   556  			q:    `_:a <ex:p> _:b .`,
   557  			statements: `
   558  _:a <ex:p> _:b .
   559  _:b <ex:p> _:a .
   560  `,
   561  			cands: map[string]map[string]bool{
   562  				"_:a": {"_:a": true, "_:b": true},
   563  				"_:b": {"_:b": true, "_:a": true},
   564  			},
   565  			mu:   map[string]string{"_:a": "_:a", "_:b": "_:a"},
   566  			want: nil,
   567  		},
   568  		{
   569  			name: "Cross identity with extension",
   570  			q:    `_:a <ex:p> _:b .`,
   571  			statements: `
   572  _:a <ex:p> _:b .
   573  _:b <ex:p> _:a .
   574  `,
   575  			cands: map[string]map[string]bool{
   576  				"_:a": {"_:a": true, "_:b": true},
   577  				"_:b": {"_:b": true, "_:a": true},
   578  			},
   579  			mu: map[string]string{"_:c": "_:a"},
   580  			want: []map[string]string{
   581  				{"_:a": "_:a", "_:b": "_:b", "_:c": "_:a"},
   582  				{"_:a": "_:b", "_:b": "_:a", "_:c": "_:a"},
   583  			},
   584  		},
   585  
   586  		{
   587  			name: "Loop",
   588  			q:    `_:a <ex:p> _:a .`,
   589  			statements: `
   590  _:a <ex:p> _:a .
   591  `,
   592  			cands: map[string]map[string]bool{
   593  				"_:a": {"_:a": true},
   594  			},
   595  			mu: nil,
   596  			want: []map[string]string{
   597  				{"_:a": "_:a"},
   598  			},
   599  		},
   600  		{
   601  			name: "Cross identity loop",
   602  			q:    `_:a <ex:p> _:a .`,
   603  			statements: `
   604  _:a <ex:p> _:a .
   605  _:b <ex:p> _:b .
   606  `,
   607  			cands: map[string]map[string]bool{
   608  				"_:a": {"_:a": true, "_:b": true},
   609  				"_:b": {"_:b": true, "_:a": true},
   610  			},
   611  			mu: nil,
   612  			want: []map[string]string{
   613  				{"_:a": "_:a"},
   614  				{"_:a": "_:b"},
   615  			},
   616  		},
   617  		{
   618  			name: "Cross identity loop with restriction",
   619  			q:    `_:a <ex:p> _:a .`,
   620  			statements: `
   621  _:a <ex:p> _:a .
   622  _:b <ex:p> _:b .
   623  `,
   624  			cands: map[string]map[string]bool{
   625  				"_:a": {"_:a": true, "_:b": true},
   626  				"_:b": {"_:b": true, "_:a": true},
   627  			},
   628  			mu: map[string]string{"_:a": "_:a"},
   629  			want: []map[string]string{
   630  				{"_:a": "_:a"},
   631  			},
   632  		},
   633  		{
   634  			name: "Cross identity loop with complete restriction",
   635  			q:    `_:a <ex:p> _:a .`,
   636  			statements: `
   637  _:a <ex:p> _:a .
   638  _:b <ex:p> _:b .
   639  `,
   640  			cands: map[string]map[string]bool{
   641  				"_:a": {"_:a": true, "_:b": true},
   642  				"_:b": {"_:b": true, "_:a": true},
   643  			},
   644  			mu: map[string]string{"_:a": "_:b", "_:b": "_:a"},
   645  			want: []map[string]string{
   646  				{"_:a": "_:b", "_:b": "_:a"},
   647  			},
   648  		},
   649  		{
   650  			name: "Cross identity loop with extension",
   651  			q:    `_:a <ex:p> _:a .`,
   652  			statements: `
   653  _:a <ex:p> _:a .
   654  _:b <ex:p> _:b .
   655  `,
   656  			cands: map[string]map[string]bool{
   657  				"_:a": {"_:a": true, "_:b": true},
   658  				"_:b": {"_:b": true, "_:a": true},
   659  			},
   660  			mu: map[string]string{"_:c": "_:a"},
   661  			want: []map[string]string{
   662  				{"_:a": "_:a", "_:c": "_:a"},
   663  				{"_:a": "_:b", "_:c": "_:a"},
   664  			},
   665  		},
   666  
   667  		{
   668  			name: "Example 5.9 step 1",
   669  			q:    `_:a1 <ex:next> _:a2 .`,
   670  			statements: `
   671  <ex:Chile> <ex:presidency> _:a1 .
   672  <ex:Chile> <ex:presidency> _:a2 .
   673  <ex:Chile> <ex:presidency> _:a3 .
   674  <ex:Chile> <ex:presidency> _:a4 .
   675  _:a1 <ex:next> _:a2 .
   676  _:a2 <ex:next> _:a3 .
   677  _:a3 <ex:next> _:a4 .
   678  `,
   679  			cands: map[string]map[string]bool{
   680  				"_:a1": {"_:a1": true, "_:a2": true, "_:a3": true},
   681  				"_:a2": {"_:a2": true, "_:a3": true},
   682  				"_:a3": {"_:a2": true, "_:a3": true},
   683  				"_:a4": {"_:a2": true, "_:a3": true, "_:a4": true},
   684  			},
   685  			mu: map[string]string{},
   686  			want: []map[string]string{
   687  				{"_:a1": "_:a1", "_:a2": "_:a2"},
   688  				{"_:a1": "_:a2", "_:a2": "_:a3"},
   689  			},
   690  		},
   691  		{
   692  			name: "Example 5.9 step 2",
   693  			q:    `_:a2 <ex:next> _:a3 .`,
   694  			statements: `
   695  <ex:Chile> <ex:presidency> _:a1 .
   696  <ex:Chile> <ex:presidency> _:a2 .
   697  <ex:Chile> <ex:presidency> _:a3 .
   698  <ex:Chile> <ex:presidency> _:a4 .
   699  _:a1 <ex:next> _:a2 .
   700  _:a2 <ex:next> _:a3 .
   701  _:a3 <ex:next> _:a4 .
   702  `,
   703  			cands: map[string]map[string]bool{
   704  				"_:a1": {"_:a1": true, "_:a2": true, "_:a3": true},
   705  				"_:a2": {"_:a2": true, "_:a3": true},
   706  				"_:a3": {"_:a2": true, "_:a3": true},
   707  				"_:a4": {"_:a2": true, "_:a3": true, "_:a4": true},
   708  			},
   709  			mu:   map[string]string{"_:a1": "_:a2", "_:a2": "_:a3"},
   710  			want: nil,
   711  		},
   712  		{
   713  			name: "Example 5.9 step 3",
   714  			q:    `_:a2 <ex:next> _:a3 .`,
   715  			statements: `
   716  <ex:Chile> <ex:presidency> _:a1 .
   717  <ex:Chile> <ex:presidency> _:a2 .
   718  <ex:Chile> <ex:presidency> _:a3 .
   719  <ex:Chile> <ex:presidency> _:a4 .
   720  _:a1 <ex:next> _:a2 .
   721  _:a2 <ex:next> _:a3 .
   722  _:a3 <ex:next> _:a4 .
   723  `,
   724  			cands: map[string]map[string]bool{
   725  				"_:a1": {"_:a1": true, "_:a2": true, "_:a3": true},
   726  				"_:a2": {"_:a2": true, "_:a3": true},
   727  				"_:a3": {"_:a2": true, "_:a3": true},
   728  				"_:a4": {"_:a2": true, "_:a3": true, "_:a4": true},
   729  			},
   730  			mu: map[string]string{"_:a1": "_:a1", "_:a2": "_:a2"},
   731  			want: []map[string]string{
   732  				{"_:a1": "_:a1", "_:a2": "_:a2", "_:a3": "_:a3"},
   733  			},
   734  		},
   735  		{
   736  			name: "Example 5.9 step 4",
   737  			q:    `_:a3 <ex:next> _:a4 .`,
   738  			statements: `
   739  <ex:Chile> <ex:presidency> _:a1 .
   740  <ex:Chile> <ex:presidency> _:a2 .
   741  <ex:Chile> <ex:presidency> _:a3 .
   742  <ex:Chile> <ex:presidency> _:a4 .
   743  _:a1 <ex:next> _:a2 .
   744  _:a2 <ex:next> _:a3 .
   745  _:a3 <ex:next> _:a4 .
   746  `,
   747  			cands: map[string]map[string]bool{
   748  				"_:a1": {"_:a1": true, "_:a2": true, "_:a3": true},
   749  				"_:a2": {"_:a2": true, "_:a3": true},
   750  				"_:a3": {"_:a2": true, "_:a3": true},
   751  				"_:a4": {"_:a2": true, "_:a3": true, "_:a4": true},
   752  			},
   753  			mu: map[string]string{"_:a1": "_:a1", "_:a2": "_:a2", "_:a3": "_:a3"},
   754  			want: []map[string]string{
   755  				{"_:a1": "_:a1", "_:a2": "_:a2", "_:a3": "_:a3", "_:a4": "_:a4"},
   756  			},
   757  		},
   758  	}
   759  
   760  	for _, test := range tests {
   761  		q := parseStatement(strings.NewReader(test.q))
   762  		g := parseStatements(strings.NewReader(test.statements))
   763  
   764  		st := dfs{}
   765  		got := st.join(q, g, test.cands, test.mu)
   766  		if !reflect.DeepEqual(got, test.want) {
   767  			t.Errorf("unexpected result for %s:\ngot:\n%#v\nwant:\n%#v",
   768  				test.name, got, test.want)
   769  		}
   770  
   771  		naive := joinNaive(q, g, test.cands, []map[string]string{test.mu})
   772  		if !reflect.DeepEqual(naive, test.want) {
   773  			t.Errorf("unexpected naive result for %s:\ngot:\n%#v\nwant:\n%#v",
   774  				test.name, naive, test.want)
   775  		}
   776  
   777  	}
   778  }
   779  
   780  // joinNaive is a direct translation of lines 47-51 of algorithm 6 in doi:10.1145/3068333.
   781  func joinNaive(q *Statement, G []*Statement, cands map[string]map[string]bool, M []map[string]string) []map[string]string {
   782  	isLoop := q.Subject.Value == q.Object.Value
   783  	// Line 48: M_q ← {µ | µ(q) ∈ G}
   784  	var M_q []map[string]string
   785  	for _, s := range G {
   786  		// µ(q) ∈ G ↔ (µ(q_s),q_p,µ(q_o)) ∈ G
   787  		if q.Predicate.Value != s.Predicate.Value {
   788  			continue
   789  		}
   790  		// q_s = q_o ↔ µ(q_s) =_µ(q_o)
   791  		if isLoop && s.Subject.Value != s.Object.Value {
   792  			continue
   793  		}
   794  
   795  		var µ map[string]string
   796  		if isLoop {
   797  			µ = map[string]string{
   798  				q.Subject.Value: s.Subject.Value,
   799  			}
   800  		} else {
   801  			µ = map[string]string{
   802  				q.Subject.Value: s.Subject.Value,
   803  				q.Object.Value:  s.Object.Value,
   804  			}
   805  		}
   806  		M_q = append(M_q, µ)
   807  	}
   808  
   809  	// Line 49: M_q' ← {µ ∈ M_q | for all b ∈ bnodes({q}), µ(b) ∈ cands[b]}
   810  	var M_qPrime []map[string]string
   811  	for _, µ := range M_q {
   812  		if !cands[q.Subject.Value][µ[q.Subject.Value]] {
   813  			continue
   814  		}
   815  		if !cands[q.Object.Value][µ[q.Object.Value]] {
   816  			continue
   817  		}
   818  		M_qPrime = append(M_qPrime, µ)
   819  	}
   820  
   821  	// Line 50: M' ← M_q' ⋈ M
   822  	// M₁ ⋈ M₂ = {μ₁ ∪ μ₂ | μ₁ ∈ M₁, μ₂ ∈ M₂ and μ₁, μ₂ are compatible mappings}
   823  	var MPrime []map[string]string
   824  	for _, µ := range M {
   825  	join:
   826  		for _, µ_qPrime := range M_qPrime {
   827  			for b, x_qPrime := range µ_qPrime {
   828  				if x, ok := µ[b]; ok && x != x_qPrime {
   829  					continue join
   830  				}
   831  			}
   832  			// Line 50: μ₁ ∪ μ₂
   833  			for b, x := range µ {
   834  				µ_qPrime[b] = x
   835  			}
   836  			MPrime = append(MPrime, µ_qPrime)
   837  		}
   838  	}
   839  	return MPrime
   840  }
   841  
   842  func parseStatement(r io.Reader) *Statement {
   843  	g := parseStatements(r)
   844  	if len(g) != 1 {
   845  		panic(fmt.Sprintf("invalid statement stream length %d != 1", len(g)))
   846  	}
   847  	return g[0]
   848  }
   849  
   850  func parseStatements(r io.Reader) []*Statement {
   851  	var g []*Statement
   852  	dec := NewDecoder(r)
   853  	for {
   854  		s, err := dec.Unmarshal()
   855  		if err != nil {
   856  			if err == io.EOF {
   857  				break
   858  			}
   859  			panic(err)
   860  		}
   861  		g = append(g, s)
   862  	}
   863  	return g
   864  }
   865  
   866  func canonicalStatements(g []*Statement) string {
   867  	g, _ = URDNA2015(nil, g)
   868  	return formatStatements(g)
   869  }
   870  
   871  func formatStatements(g []*Statement) string {
   872  	var buf strings.Builder
   873  	for _, s := range g {
   874  		fmt.Fprintln(&buf, s)
   875  	}
   876  	return buf.String()
   877  }