aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyo Nihei <nihei.dev@gmail.com>2022-05-08 13:07:56 +0900
committerRyo Nihei <nihei.dev@gmail.com>2022-05-10 23:14:41 +0900
commit2438fa4435d6393168412574a3ef94396a4debe5 (patch)
tree390ea502d7472af78b68f7bfef928d48525d533e
parentChange syntax for top-level directives (diff)
downloadurubu-2438fa4435d6393168412574a3ef94396a4debe5.tar.gz
urubu-2438fa4435d6393168412574a3ef94396a4debe5.tar.xz
Add #assign directive
An #assign directive changes only precedence.
-rw-r--r--README.md2
-rw-r--r--driver/conflict_test.go165
-rw-r--r--grammar/grammar.go2
-rw-r--r--grammar/grammar_test.go331
-rw-r--r--spec/parser_test.go20
5 files changed, 507 insertions, 13 deletions
diff --git a/README.md b/README.md
index 92c270c..bf89c10 100644
--- a/README.md
+++ b/README.md
@@ -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),
),