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 | |
parent | Update CHANGELOG (diff) | |
download | urubu-18a3317ac9c79651e5c74a2afc6b14fd9a3f9d4a.tar.gz urubu-18a3317ac9c79651e5c74a2afc6b14fd9a3f9d4a.tar.xz |
Move compiler tests from driver package to grammar package
-rw-r--r-- | driver/parser_test.go | 547 | ||||
-rw-r--r-- | grammar/grammar.go | 23 | ||||
-rw-r--r-- | grammar/grammar_test.go | 641 |
3 files changed, 658 insertions, 553 deletions
diff --git a/driver/parser_test.go b/driver/parser_test.go index 04c42c5..d6fccc5 100644 --- a/driver/parser_test.go +++ b/driver/parser_test.go @@ -30,7 +30,6 @@ func TestParser_Parse(t *testing.T) { src string cst *Node ast *Node - specErr bool }{ { specSrc: ` @@ -153,118 +152,6 @@ bar_text: "bar"; ), ), }, - // `name` is missing. - { - specSrc: ` -a - : foo - ; -foo: "foo"; -`, - src: `foo`, - specErr: true, - }, - // `name` needs a parameter. - { - specSrc: ` -%name - -a - : foo - ; -foo: "foo"; -`, - src: `foo`, - specErr: true, - }, - // `name` takes just one parameter. - { - specSrc: ` -%name test foo - -a - : foo - ; -foo: "foo"; -`, - src: `foo`, - specErr: true, - }, - // Production `b` is unused. - { - specSrc: ` -%name test - -a - : foo - ; -b - : foo; -foo: "foo"; -`, - src: `foo`, - specErr: true, - }, - // Terminal `bar` is unused. - { - specSrc: ` -%name test - -s - : foo - ; -foo: "foo"; -bar: "bar"; -`, - src: `foo`, - specErr: true, - }, - // Production `b` and terminal `bar` is unused. - { - specSrc: ` -%name test - -a - : foo - ; -b - : bar - ; -foo: "foo"; -bar: "bar"; -`, - src: `foo`, - specErr: true, - }, - // A terminal used in productions cannot have the skip directive. - { - specSrc: ` -%name test - -a - : foo - ; - -foo #skip - : "foo"; -`, - src: `foo`, - specErr: true, - }, - // A production cannot have production directives. - { - specSrc: ` -%name test - -s #prec foo - : foo - ; - -foo: 'foo' #skip; -`, - src: `foo`, - specErr: true, - }, // A production can have multiple alternative productions. { specSrc: ` @@ -348,52 +235,6 @@ pop #mode a b #pop termNode("pop", "<-"), ), }, - // A lexical production cannot have alternative directives. - { - specSrc: ` -%name test - -s - : foo - ; - -foo: 'foo' #skip; -`, - src: `foo`, - specErr: true, - }, - // A production directive must not be duplicated. - { - specSrc: ` -%name test - -s - : foo - ; - -foo #skip #skip - : 'foo'; -`, - src: `foo`, - specErr: true, - }, - // An alternative directive must not be duplicated. - { - specSrc: ` -%name test - -s - : foo bar #ast foo bar #ast foo bar - ; - -foo - : 'foo'; -bar - : 'bar'; -`, - src: `foobar`, - specErr: true, - }, { specSrc: ` %name test @@ -541,221 +382,6 @@ num: "0|[1-9][0-9]*"; ), ), }, - // The expansion cannot be applied to a terminal symbol. - { - specSrc: ` -%name test - -s - : foo #ast foo... - ; -foo: "foo"; -`, - specErr: true, - }, - // The expansion cannot be applied to a pattern. - { - specSrc: ` -%name test - -s - : foo "bar"@b #ast foo b... - ; -foo: "foo"; -`, - specErr: true, - }, - // The expansion cannot be applied to a string. - { - specSrc: ` -%name test - -s - : foo 'bar'@b #ast foo b... - ; -foo: "foo"; -`, - specErr: true, - }, - // A parameter of #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"; -`, - specErr: true, - }, - // A symbol in a different alternative cannot be a parameter of #ast directive. - { - specSrc: ` -%name test - -s - : foo #ast bar - | bar - ; -foo: "foo"; -bar: "bar"; -`, - specErr: true, - }, - // A label in a different alternative cannot be a parameter of #ast directive. - { - specSrc: ` -%name test - -s - : foo #ast b - | bar@b - ; -foo: "foo"; -bar: "bar"; -`, - specErr: true, - }, - // A production must not have a duplicate alternative. - { - specSrc: ` -%name test - -s - : foo - | foo - ; -foo: "foo"; -`, - specErr: true, - }, - // A production must not have a duplicate alternative. - { - specSrc: ` -%name test - -a - : foo - ; -b - : - | - ; -foo: "foo"; -`, - specErr: true, - }, - // A production must not have a duplicate alternative. - { - specSrc: ` -%name test - -a - : foo - ; -b - : bar - ; -a - : foo - ; -foo: "foo"; -bar: "bar"; -`, - specErr: true, - }, - // A terminal and a non-terminal (start symbol) are duplicates. - { - specSrc: ` -%name test - -a - : foo - ; -foo: "foo"; -a: "a"; -`, - specErr: true, - }, - // A terminal and a non-terminal (not start symbol) are duplicates. - { - specSrc: ` -%name test - -a - : foo - ; -b - : bar - ; -foo: "foo"; -bar: "bar"; -b: "a"; -`, - specErr: true, - }, - // Invalid associativity type - { - specSrc: ` -%name test - -%foo - -s - : a - ; - -a: 'a'; -`, - specErr: true, - }, - // Associativity needs at least one symbol. - { - specSrc: ` -%name test - -%left - -s - : a - ; - -a: 'a'; -`, - specErr: true, - }, - // Associativity cannot take an undefined symbol. - { - specSrc: ` -%name test - -%left b - -s - : a - ; - -a: 'a'; -`, - specErr: true, - }, - // Associativity cannot take a non-terminal symbol. - { - specSrc: ` -%name test - -%left s - -s - : a - ; - -a: 'a'; -`, - specErr: true, - }, // The 'prec' directive can set precedence and associativity of a production. { specSrc: ` @@ -815,53 +441,6 @@ div ), ), }, - // The 'prec' directive needs an ID parameter. - { - specSrc: ` -%name test - -s - : a #prec - ; - -a: 'a'; -`, - specErr: true, - }, - // The 'prec' directive cannot take an unknown symbol. - { - specSrc: ` -%name test - -s - : a #prec foo - ; - -a: 'a'; -`, - specErr: true, - }, - // 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'; -`, - specErr: true, - }, // The grammar can contain the 'error' symbol. { specSrc: ` @@ -900,90 +479,6 @@ id `, src: `a b c ; d e f ;`, }, - // 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_]+"; -`, - src: `a b c ; d e f ;`, - specErr: true, - }, - // You cannot use the error symbol as a non-terminal symbol. - { - specSrc: ` -%name test - -s - : foo - ; -error - : bar - ; - -foo: 'foo'; -bar: 'bar'; -`, - specErr: true, - }, - // You cannot use the error symbol as a terminal symbol. - { - specSrc: ` -%name test - -s - : foo - | error - ; - -foo: 'foo'; -error: 'error'; -`, - specErr: true, - }, - // You 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'; -`, - specErr: true, - }, - // A label must be unique in an alternative. - { - specSrc: ` -%name test - -s - : foo@x bar@x - ; - -foo: 'foo'; -bar: 'bar'; -`, - specErr: true, - }, // The same label can be used between different alternatives. { specSrc: ` @@ -999,37 +494,6 @@ bar: 'bar'; `, src: `foo`, }, - // A label cannot be the same name as terminal symbols. - { - specSrc: ` -%name test - -s - : foo bar@foo - ; - -foo: 'foo'; -bar: 'bar'; -`, - specErr: true, - }, - // A label cannot be the same name as non-terminal symbols. - { - specSrc: ` -%name test - -s - : foo@a - ; -a - : bar - ; - -foo: 'foo'; -bar: 'bar'; -`, - specErr: true, - }, } classes := []grammar.Class{ @@ -1049,15 +513,8 @@ bar: 'bar'; AST: ast, } g, err := b.Build() - if tt.specErr { - if err == nil { - t.Fatal("an expected error didn't occur") - } - return - } else { - if err != nil { - t.Fatal(err) - } + if err != nil { + t.Fatal(err) } cg, err := grammar.Compile(g, grammar.SpecifyClass(class)) diff --git a/grammar/grammar.go b/grammar/grammar.go index bc8cc6b..fcc836f 100644 --- a/grammar/grammar.go +++ b/grammar/grammar.go @@ -154,6 +154,9 @@ func (b *GrammarBuilder) Build() (*Grammar, error) { if err != nil { return nil, err } + if pa == nil && len(b.errs) > 0 { + return nil, b.errs + } syms, err := findUsedAndUnusedSymbols(b.AST) if err != nil { @@ -957,39 +960,43 @@ func (b *GrammarBuilder) genPrecAndAssoc(symTab *symbolTable, prods *productionS // Since `name` is used for a purpose other than priority, we will ignore it here. continue default: - return nil, &verr.SpecError{ + b.errs = append(b.errs, &verr.SpecError{ Cause: semErrMDInvalidName, Row: md.Pos.Row, Col: md.Pos.Col, - } + }) + return nil, nil } if len(md.Parameters) == 0 { - return nil, &verr.SpecError{ + b.errs = append(b.errs, &verr.SpecError{ Cause: semErrMDInvalidParam, Detail: "associativity needs at least one symbol", Row: md.Pos.Row, Col: md.Pos.Col, - } + }) + return nil, nil } for _, p := range md.Parameters { sym, ok := symTab.toSymbol(p.ID) if !ok { - return nil, &verr.SpecError{ + b.errs = append(b.errs, &verr.SpecError{ Cause: semErrMDInvalidParam, Detail: fmt.Sprintf("'%v' is undefined", p.ID), Row: p.Pos.Row, Col: p.Pos.Col, - } + }) + return nil, nil } if !sym.isTerminal() { - return nil, &verr.SpecError{ + b.errs = append(b.errs, &verr.SpecError{ Cause: semErrMDInvalidParam, Detail: fmt.Sprintf("associativity can take only terminal symbol ('%v' is a non-terminal)", p.ID), Row: p.Pos.Row, Col: p.Pos.Col, - } + }) + return nil, nil } termPrec[sym.num()] = precN 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) + }) + } +} |