diff options
author | Ryo Nihei <nihei.dev@gmail.com> | 2022-04-13 23:09:49 +0900 |
---|---|---|
committer | Ryo Nihei <nihei.dev@gmail.com> | 2022-04-14 00:40:39 +0900 |
commit | 18a3317ac9c79651e5c74a2afc6b14fd9a3f9d4a (patch) | |
tree | 2e935d489805de4ba310a4dfbdfde702a1d5b0e7 /grammar/grammar_test.go | |
parent | Update CHANGELOG (diff) | |
download | urubu-18a3317ac9c79651e5c74a2afc6b14fd9a3f9d4a.tar.gz urubu-18a3317ac9c79651e5c74a2afc6b14fd9a3f9d4a.tar.xz |
Move compiler tests from driver package to grammar package
Diffstat (limited to 'grammar/grammar_test.go')
-rw-r--r-- | grammar/grammar_test.go | 641 |
1 files changed, 641 insertions, 0 deletions
diff --git a/grammar/grammar_test.go b/grammar/grammar_test.go new file mode 100644 index 0000000..f59acef --- /dev/null +++ b/grammar/grammar_test.go @@ -0,0 +1,641 @@ +package grammar + +import ( + "strings" + "testing" + + verr "github.com/nihei9/vartan/error" + "github.com/nihei9/vartan/spec" +) + +func TestGrammarBuilderSpecError(t *testing.T) { + type specErrTest struct { + caption string + specSrc string + errs []*SemanticError + } + + prodTests := []*specErrTest{ + { + caption: "a production `b` is unused", + specSrc: ` +%name test + +a + : foo + ; +b + : foo; +foo: "foo"; +`, + errs: []*SemanticError{semErrUnusedProduction}, + }, + { + caption: "a terminal symbol `bar` is unused", + specSrc: ` +%name test + +s + : foo + ; +foo: "foo"; +bar: "bar"; +`, + errs: []*SemanticError{semErrUnusedTerminal}, + }, + { + caption: "a production `b` and terminal symbol `bar` is unused", + specSrc: ` +%name test + +a + : foo + ; +b + : bar + ; +foo: "foo"; +bar: "bar"; +`, + errs: []*SemanticError{ + semErrUnusedProduction, + semErrUnusedTerminal, + }, + }, + { + caption: "a production cannot have production directives", + specSrc: ` +%name test + +s #prec foo + : foo + ; + +foo: 'foo'; +`, + errs: []*SemanticError{semErrInvalidProdDir}, + }, + { + caption: "a lexical production cannot have alternative directives", + specSrc: ` +%name test + +s + : foo + ; + +foo: 'foo' #skip; +`, + errs: []*SemanticError{semErrInvalidAltDir}, + }, + { + caption: "a production directive must not be duplicated", + specSrc: ` +%name test + +s + : foo + ; + +foo #skip #skip + : 'foo'; +`, + errs: []*SemanticError{semErrDuplicateDir}, + }, + { + caption: "an alternative directive must not be duplicated", + specSrc: ` +%name test + +s + : foo bar #ast foo bar #ast foo bar + ; + +foo + : 'foo'; +bar + : 'bar'; +`, + errs: []*SemanticError{semErrDuplicateDir}, + }, + { + caption: "a production must not have a duplicate alternative (non-empty alternatives)", + specSrc: ` +%name test + +s + : foo + | foo + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrDuplicateProduction}, + }, + { + caption: "a production must not have a duplicate alternative (non-empty and split alternatives)", + specSrc: ` +%name test + +a + : foo + | b + ; +b + : bar + ; +a + : foo + ; +foo: "foo"; +bar: "bar"; +`, + errs: []*SemanticError{semErrDuplicateProduction}, + }, + { + caption: "a production must not have a duplicate alternative (empty alternatives)", + specSrc: ` +%name test + +a + : foo + | b + ; +b + : + | + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrDuplicateProduction}, + }, + { + caption: "a production must not have a duplicate alternative (empty and split alternatives)", + specSrc: ` +%name test + +a + : foo + | b + ; +b + : + | foo + ; +b + : + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrDuplicateProduction}, + }, + { + caption: "a terminal symbol and a non-terminal symbol (start symbol) are duplicates", + specSrc: ` +%name test + +a + : foo + ; +foo: "foo"; +a: "a"; +`, + errs: []*SemanticError{semErrDuplicateName}, + }, + { + caption: "a terminal symbol and a non-terminal symbol (not start symbol) are duplicates", + specSrc: ` +%name test + +a + : foo + | b + ; +b + : bar + ; +foo: "foo"; +bar: "bar"; +b: "a"; +`, + errs: []*SemanticError{semErrDuplicateName}, + }, + { + caption: "an invalid associativity type", + specSrc: ` +%name test + +%foo + +s + : a + ; + +a: 'a'; +`, + errs: []*SemanticError{semErrMDInvalidName}, + }, + { + caption: "a label must be unique in an alternative", + specSrc: ` +%name test + +s + : foo@x bar@x + ; + +foo: 'foo'; +bar: 'bar'; +`, + errs: []*SemanticError{semErrDuplicateLabel}, + }, + { + caption: "a label cannot be the same name as terminal symbols", + specSrc: ` +%name test + +s + : foo bar@foo + ; + +foo: 'foo'; +bar: 'bar'; +`, + errs: []*SemanticError{semErrDuplicateLabel}, + }, + { + caption: "a label cannot be the same name as non-terminal symbols", + specSrc: ` +%name test + +s + : foo@a + | a + ; +a + : bar + ; + +foo: 'foo'; +bar: 'bar'; +`, + errs: []*SemanticError{ + semErrInvalidLabel, + }, + }, + } + + nameTests := []*specErrTest{ + { + caption: "the `%name` is missing", + specSrc: ` +a + : foo + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrMDMissingName}, + }, + { + caption: "the `%name` needs a parameter", + specSrc: ` +%name + +a + : foo + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrMDInvalidParam}, + }, + { + caption: "the `%name` takes just one parameter", + specSrc: ` +%name test foo + +a + : foo + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrMDInvalidParam}, + }, + } + + assocTests := []*specErrTest{ + { + caption: "associativity needs at least one symbol", + specSrc: ` +%name test + +%left + +s + : a + ; + +a: 'a'; +`, + errs: []*SemanticError{semErrMDInvalidParam}, + }, + { + caption: "associativity cannot take an undefined symbol", + specSrc: ` +%name test + +%left b + +s + : a + ; + +a: 'a'; +`, + errs: []*SemanticError{semErrMDInvalidParam}, + }, + { + caption: "associativity cannot take a non-terminal symbol", + specSrc: ` +%name test + +%left s + +s + : a + ; + +a: 'a'; +`, + errs: []*SemanticError{semErrMDInvalidParam}, + }, + } + + errorSymTests := []*specErrTest{ + { + caption: "cannot use the error symbol as a non-terminal symbol", + specSrc: ` +%name test + +s + : foo + ; +error + : bar + ; + +foo: 'foo'; +bar: 'bar'; +`, + errs: []*SemanticError{ + semErrErrSymIsReserved, + semErrDuplicateName, + // The compiler determines the symbol `bar` is unreachable because the production rule `error → bar` contains + // a build error and the compiler doesn't recognize the production rule as a valid one. + // This error is essentially irrelevant to this test case. + semErrUnusedTerminal, // This error means `bar` is unreachable. + }, + }, + { + caption: "cannot use the error symbol as a terminal symbol", + specSrc: ` +%name test + +s + : foo + | error + ; + +foo: 'foo'; +error: 'error'; +`, + errs: []*SemanticError{semErrErrSymIsReserved}, + }, + { + caption: "cannot use the error symbol as a terminal symbol, even if given the skip directive", + specSrc: ` +%name test + +s + : foo + ; + +foo + : 'foo'; +error #skip + : 'error'; +`, + errs: []*SemanticError{semErrErrSymIsReserved}, + }, + } + + astDirTests := []*specErrTest{ + { + caption: "a parameter of the `#ast` directive must be either a symbol or a label in an alternative", + specSrc: ` +%name test + +s + : foo bar #ast foo x + ; +foo: "foo"; +bar: "bar"; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "a symbol in a different alternative cannot be a parameter of the `#ast` directive", + specSrc: ` +%name test + +s + : foo #ast bar + | bar + ; +foo: "foo"; +bar: "bar"; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "a label in a different alternative cannot be a parameter of the `#ast` directive", + specSrc: ` +%name test + +s + : foo #ast b + | bar@b + ; +foo: "foo"; +bar: "bar"; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the expansion operator cannot be applied to a terminal symbol", + specSrc: ` +%name test + +s + : foo #ast foo... + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the expansion operator cannot be applied to a pattern", + specSrc: ` +%name test + +s + : foo "bar"@b #ast foo b... + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the expansion operator cannot be applied to a string", + specSrc: ` +%name test + +s + : foo 'bar'@b #ast foo b... + ; +foo: "foo"; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + } + + precDirTests := []*specErrTest{ + { + caption: "the `#prec` directive needs an ID parameter", + specSrc: ` +%name test + +s + : a #prec + ; + +a: 'a'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#prec` directive cannot take an unknown symbol", + specSrc: ` +%name test + +s + : a #prec foo + ; + +a: 'a'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#prec` directive cannot take a non-terminal symbol", + specSrc: ` +%name test + +s + : foo #prec bar + | bar + ; +foo + : a + ; +bar + : b + ; + +a: 'a'; +b: 'b'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + } + + recoverDirTests := []*specErrTest{ + { + caption: "the `#recover` directive cannot take a parameter", + specSrc: ` +%name test + +seq + : seq elem + | elem + ; +elem + : id id id ';' + | error ';' #recover foo + ; + +ws #skip + : "[\u{0009}\u{0020}]+"; +id + : "[A-Za-z_]+"; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + } + + skipDirTests := []*specErrTest{ + { + caption: "a terminal symbol used in productions cannot have the skip directive", + specSrc: ` +%name test + +a + : foo + ; + +foo #skip + : "foo"; +`, + errs: []*SemanticError{semErrTermCannotBeSkipped}, + }, + } + + var tests []*specErrTest + tests = append(tests, prodTests...) + tests = append(tests, nameTests...) + tests = append(tests, assocTests...) + tests = append(tests, errorSymTests...) + tests = append(tests, astDirTests...) + tests = append(tests, precDirTests...) + tests = append(tests, recoverDirTests...) + tests = append(tests, skipDirTests...) + for _, test := range tests { + t.Run(test.caption, func(t *testing.T) { + ast, err := spec.Parse(strings.NewReader(test.specSrc)) + if err != nil { + t.Fatal(err) + } + + b := GrammarBuilder{ + AST: ast, + } + _, err = b.Build() + if err == nil { + t.Fatal("an expected error didn't occur") + } + specErrs, ok := err.(verr.SpecErrors) + if !ok { + t.Fatalf("unexpected error type: want: %T, got: %T: %v", verr.SpecErrors{}, err, err) + } + if len(specErrs) != len(test.errs) { + t.Fatalf("unexpected spec error count: want: %+v, got: %+v", test.errs, specErrs) + } + for _, expected := range test.errs { + for _, actual := range specErrs { + if actual.Cause == expected { + return + } + } + } + t.Fatalf("an expected spec error didn't occur: want: %v, got: %+v", test.errs, specErrs) + }) + } +} |