diff options
author | Ryo Nihei <nihei.dev@gmail.com> | 2022-05-08 13:07:56 +0900 |
---|---|---|
committer | Ryo Nihei <nihei.dev@gmail.com> | 2022-05-10 23:14:41 +0900 |
commit | 2438fa4435d6393168412574a3ef94396a4debe5 (patch) | |
tree | 390ea502d7472af78b68f7bfef928d48525d533e | |
parent | Change syntax for top-level directives (diff) | |
download | urubu-2438fa4435d6393168412574a3ef94396a4debe5.tar.gz urubu-2438fa4435d6393168412574a3ef94396a4debe5.tar.xz |
Add #assign directive
An #assign directive changes only precedence.
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | driver/conflict_test.go | 165 | ||||
-rw-r--r-- | grammar/grammar.go | 2 | ||||
-rw-r--r-- | grammar/grammar_test.go | 331 | ||||
-rw-r--r-- | spec/parser_test.go | 20 |
5 files changed, 507 insertions, 13 deletions
@@ -501,6 +501,8 @@ foobar `#left` and `#right` directives allow you to define precedence and associativiry of symbols. `#left`/`#right` each assign the left/right associativity to symbols. +If you want to change precedence, `#assign` directive helps you. `#assign` directive changes only precedence, not associativity. + When the right-most terminal symbol of an alternative has precedence or associativity defined explicitly, the alternative inherits its precedence and associativity. `#prec` directive assigns the same precedence as a specified symbol to an alternative. diff --git a/driver/conflict_test.go b/driver/conflict_test.go index 042d932..e767e5b 100644 --- a/driver/conflict_test.go +++ b/driver/conflict_test.go @@ -275,13 +275,122 @@ id ), }, { - caption: "left and right associativities can be mixed", + caption: "terminal symbols with an #assign directive defined earlier in the grammar have higher precedence", + specSrc: ` +#name test; + +#prec ( + #assign a1 + #assign a2 +); + +expr + : expr a2 expr + | expr a1 expr + | id + ; + +whitespaces #skip + : "[\u{0009}\u{0020}]+"; +a1 + : 'a1'; +a2 + : 'a2'; +id + : "[A-Za-z0-9_]+"; +`, + src: `a a2 b a1 c a1 d a2 e`, + cst: nonTermNode("expr", + nonTermNode("expr", + termNode("id", "a"), + ), + termNode("a2", "a2"), + nonTermNode("expr", + nonTermNode("expr", + nonTermNode("expr", + termNode("id", "b"), + ), + termNode("a1", "a1"), + nonTermNode("expr", + nonTermNode("expr", + termNode("id", "c"), + ), + termNode("a1", "a1"), + nonTermNode("expr", + termNode("id", "d"), + ), + ), + ), + termNode("a2", "a2"), + nonTermNode("expr", + termNode("id", "e"), + ), + ), + ), + }, + { + caption: "terminal symbols with an #assign directive defined in the same line have the same precedence", + specSrc: ` +#name test; + +#prec ( + #assign a1 a2 +); + +expr + : expr a2 expr + | expr a1 expr + | id + ; + +whitespaces #skip + : "[\u{0009}\u{0020}]+"; +a1 + : 'a1'; +a2 + : 'a2'; +id + : "[A-Za-z0-9_]+"; +`, + src: `a a2 b a1 c a1 d a2 e`, + cst: nonTermNode("expr", + nonTermNode("expr", + termNode("id", "a"), + ), + termNode("a2", "a2"), + nonTermNode("expr", + nonTermNode("expr", + termNode("id", "b"), + ), + termNode("a1", "a1"), + nonTermNode("expr", + nonTermNode("expr", + termNode("id", "c"), + ), + termNode("a1", "a1"), + nonTermNode("expr", + nonTermNode("expr", + termNode("id", "d"), + ), + termNode("a2", "a2"), + nonTermNode("expr", + termNode("id", "e"), + ), + ), + ), + ), + ), + }, + { + caption: "#left, #right, and #assign can be mixed", specSrc: ` #name test; #prec ( #left mul div #left add sub + #assign else + #assign then #right assign ); @@ -290,10 +399,16 @@ expr | expr sub expr | expr mul expr | expr div expr - | expr assign expr - | id + | expr assign expr + | if expr then expr + | if expr then expr else expr + | id ; +ws #skip: "[\u{0009}\u{0020}]+"; +if: 'if'; +then: 'then'; +else: 'else'; id: "[A-Za-z0-9_]+"; add: '+'; sub: '-'; @@ -301,7 +416,7 @@ mul: '*'; div: '/'; assign: '='; `, - src: `x=y=a+b*c-d/e`, + src: `x = y = a + b * c - d / e + if f then if g then h else i`, cst: nonTermNode( "expr", nonTermNode("expr", @@ -316,27 +431,51 @@ assign: '='; nonTermNode("expr", nonTermNode("expr", nonTermNode("expr", - termNode("id", "a"), + nonTermNode("expr", + termNode("id", "a"), + ), + termNode("add", "+"), + nonTermNode("expr", + nonTermNode("expr", + termNode("id", "b"), + ), + termNode("mul", "*"), + nonTermNode("expr", + termNode("id", "c"), + ), + ), ), - termNode("add", "+"), + termNode("sub", "-"), nonTermNode("expr", nonTermNode("expr", - termNode("id", "b"), + termNode("id", "d"), ), - termNode("mul", "*"), + termNode("div", "/"), nonTermNode("expr", - termNode("id", "c"), + termNode("id", "e"), ), ), ), - termNode("sub", "-"), + termNode("add", "+"), nonTermNode("expr", + termNode("if", "if"), nonTermNode("expr", - termNode("id", "d"), + termNode("id", "f"), ), - termNode("div", "/"), + termNode("then", "then"), nonTermNode("expr", - termNode("id", "e"), + termNode("if", "if"), + nonTermNode("expr", + termNode("id", "g"), + ), + termNode("then", "then"), + nonTermNode("expr", + termNode("id", "h"), + ), + termNode("else", "else"), + nonTermNode("expr", + termNode("id", "i"), + ), ), ), ), diff --git a/grammar/grammar.go b/grammar/grammar.go index da0460b..da1c77c 100644 --- a/grammar/grammar.go +++ b/grammar/grammar.go @@ -1020,6 +1020,8 @@ func (b *GrammarBuilder) genPrecAndAssoc(symTab *symbolTable, prods *productionS assocTy = assocTypeLeft case "right": assocTy = assocTypeRight + case "assign": + assocTy = assocTypeNil default: b.errs = append(b.errs, &verr.SpecError{ Cause: semErrDirInvalidName, diff --git a/grammar/grammar_test.go b/grammar/grammar_test.go index 07d8b58..d4a361d 100644 --- a/grammar/grammar_test.go +++ b/grammar/grammar_test.go @@ -201,6 +201,9 @@ fragment f t.Fatalf("symbol having expected mode was not found: want: %v #mode %v", kind, expectedMode) }, }, + } + + precTests := []*okTest{ { caption: "a `#prec` allows the empty directive group", specSrc: ` @@ -217,6 +220,165 @@ foo `, }, { + caption: "a `#left` directive gives a precedence and the left associativity to specified terminal symbols", + specSrc: ` +#name test; + +#prec ( + #left foo bar +); + +s + : foo bar baz + ; + +foo + : 'foo'; +bar + : 'bar'; +baz + : 'baz'; +`, + validate: func(t *testing.T, g *Grammar) { + var fooPrec int + var fooAssoc assocType + { + s, _ := g.symbolTable.toSymbol("foo") + fooPrec = g.precAndAssoc.terminalPrecedence(s.num()) + fooAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if fooPrec != 1 || fooAssoc != assocTypeLeft { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", 1, assocTypeLeft, fooPrec, fooAssoc) + } + var barPrec int + var barAssoc assocType + { + s, _ := g.symbolTable.toSymbol("bar") + barPrec = g.precAndAssoc.terminalPrecedence(s.num()) + barAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if barPrec != 1 || barAssoc != assocTypeLeft { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", 1, assocTypeLeft, barPrec, barAssoc) + } + var bazPrec int + var bazAssoc assocType + { + s, _ := g.symbolTable.toSymbol("baz") + bazPrec = g.precAndAssoc.terminalPrecedence(s.num()) + bazAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if bazPrec != precNil || bazAssoc != assocTypeNil { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", precNil, assocTypeNil, bazPrec, bazAssoc) + } + }, + }, + { + caption: "a `#right` directive gives a precedence and the right associativity to specified terminal symbols", + specSrc: ` +#name test; + +#prec ( + #right foo bar +); + +s + : foo bar baz + ; + +foo + : 'foo'; +bar + : 'bar'; +baz + : 'baz'; +`, + validate: func(t *testing.T, g *Grammar) { + var fooPrec int + var fooAssoc assocType + { + s, _ := g.symbolTable.toSymbol("foo") + fooPrec = g.precAndAssoc.terminalPrecedence(s.num()) + fooAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if fooPrec != 1 || fooAssoc != assocTypeRight { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", 1, assocTypeRight, fooPrec, fooAssoc) + } + var barPrec int + var barAssoc assocType + { + s, _ := g.symbolTable.toSymbol("bar") + barPrec = g.precAndAssoc.terminalPrecedence(s.num()) + barAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if barPrec != 1 || barAssoc != assocTypeRight { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", 1, assocTypeRight, barPrec, barAssoc) + } + var bazPrec int + var bazAssoc assocType + { + s, _ := g.symbolTable.toSymbol("baz") + bazPrec = g.precAndAssoc.terminalPrecedence(s.num()) + bazAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if bazPrec != precNil || bazAssoc != assocTypeNil { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", precNil, assocTypeNil, bazPrec, bazAssoc) + } + }, + }, + { + caption: "an `#assign` directive gives only a precedence to specified terminal symbols", + specSrc: ` +#name test; + +#prec ( + #assign foo bar +); + +s + : foo bar baz + ; + +foo + : 'foo'; +bar + : 'bar'; +baz + : 'baz'; +`, + validate: func(t *testing.T, g *Grammar) { + var fooPrec int + var fooAssoc assocType + { + s, _ := g.symbolTable.toSymbol("foo") + fooPrec = g.precAndAssoc.terminalPrecedence(s.num()) + fooAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if fooPrec != 1 || fooAssoc != assocTypeNil { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", 1, assocTypeNil, fooPrec, fooAssoc) + } + var barPrec int + var barAssoc assocType + { + s, _ := g.symbolTable.toSymbol("bar") + barPrec = g.precAndAssoc.terminalPrecedence(s.num()) + barAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if barPrec != 1 || barAssoc != assocTypeNil { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", 1, assocTypeNil, barPrec, barAssoc) + } + var bazPrec int + var bazAssoc assocType + { + s, _ := g.symbolTable.toSymbol("baz") + bazPrec = g.precAndAssoc.terminalPrecedence(s.num()) + bazAssoc = g.precAndAssoc.terminalAssociativity(s.num()) + } + if bazPrec != precNil || bazAssoc != assocTypeNil { + t.Fatalf("unexpected terminal precedence and associativity: want: (prec: %v, assoc: %v), got: (prec: %v, assoc: %v)", precNil, assocTypeNil, bazPrec, bazAssoc) + } + }, + }, + { caption: "a production has the same precedence and associativity as the right-most terminal symbol", specSrc: ` #name test; @@ -401,6 +563,7 @@ bar var tests []*okTest tests = append(tests, nameTests...) tests = append(tests, modeTests...) + tests = append(tests, precTests...) for _, test := range tests { t.Run(test.caption, func(t *testing.T) { @@ -1221,6 +1384,173 @@ foo }, } + assignDirTests := []*specErrTest{ + { + caption: "the `#assign` directive needs ID parameters", + specSrc: ` +#name test; + +#prec ( + #assign +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#assign` directive cannot take an undefined symbol", + specSrc: ` +#name test; + +#prec ( + #assign x +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#assign` directive cannot take a non-terminal symbol", + specSrc: ` +#name test; + +#prec ( + #assign s +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#assign` directive cannot take a pattern parameter", + specSrc: ` +#name test; + +#prec ( + #assign "foo" +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#assign` directive cannot take a string parameter", + specSrc: ` +#name test; + +#prec ( + #assign 'foo' +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#assign` directive cannot take a directive parameter", + specSrc: ` +#name test; + +#prec ( + #assign () +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDirInvalidParam}, + }, + { + caption: "the `#assign` dirctive cannot be specified multiple times for a symbol", + specSrc: ` +#name test; + +#prec ( + #assign foo foo +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDuplicateAssoc}, + }, + { + caption: "a symbol cannot have different precedence", + specSrc: ` +#name test; + +#prec ( + #assign foo + #assign foo +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDuplicateAssoc}, + }, + { + caption: "a symbol cannot have different associativity", + specSrc: ` +#name test; + +#prec ( + #assign foo + #left foo +); + +s + : foo + ; + +foo + : 'foo'; +`, + errs: []*SemanticError{semErrDuplicateAssoc}, + }, + } + errorSymTests := []*specErrTest{ { caption: "cannot use the error symbol as a non-terminal symbol", @@ -2086,6 +2416,7 @@ bar tests = append(tests, precDirTests...) tests = append(tests, leftDirTests...) tests = append(tests, rightDirTests...) + tests = append(tests, assignDirTests...) tests = append(tests, errorSymTests...) tests = append(tests, astDirTests...) tests = append(tests, altPrecDirTests...) diff --git a/spec/parser_test.go b/spec/parser_test.go index de2c6f7..3fe950f 100644 --- a/spec/parser_test.go +++ b/spec/parser_test.go @@ -34,6 +34,12 @@ func TestParse(t *testing.T) { Parameters: params, } } + assign := func(params ...*ParameterNode) *DirectiveNode { + return &DirectiveNode{ + Name: "assign", + Parameters: params, + } + } prod := func(lhs string, alts ...*AlternativeNode) *ProductionNode { return &ProductionNode{ LHS: lhs, @@ -148,6 +154,7 @@ func TestParse(t *testing.T) { #prec ( #left a b #right c d + #assign e f ); `, ast: &RootNode{ @@ -191,6 +198,19 @@ func TestParse(t *testing.T) { ), newPos(6), ), + withDirPos( + assign( + withParamPos( + idParam("e"), + newPos(7), + ), + withParamPos( + idParam("f"), + newPos(7), + ), + ), + newPos(7), + ), ), newPos(4), ), |