[pseudo] Sort nonterminals based on their reduction order.

Reductions need to be performed in a careful order in GLR parser, to
make sure we gather all alternatives before creating an ambigous forest
node.

This patch encodes the nonterminal order into the rule id, so that we
can efficiently to determinal ordering of reductions in GLR parser.

This patch also abstracts to a TestGrammar, which is shared among tests.

This is a part of the GLR parser, https://reviews.llvm.org/D121368,
https://reviews.llvm.org/D121150

Differential Revision: https://reviews.llvm.org/D122303
This commit is contained in:
Haojian Wu 2022-03-23 11:38:49 +01:00
parent 895e5b2d80
commit f383b88d82
5 changed files with 107 additions and 17 deletions

View File

@ -165,8 +165,15 @@ struct GrammarTable {
} RuleRange; } RuleRange;
}; };
// The rules are sorted (and thus grouped) by target symbol. // RuleID is an index into this table of rule definitions.
// RuleID is the index of the vector. //
// Rules with the same target symbol (LHS) are grouped into a single range.
// The relative order of different target symbols is *not* by SymbolID, but
// rather a topological sort: if S := T then the rules producing T have lower
// RuleIDs than rules producing S.
// (This strange order simplifies the GLR parser: for a given token range, if
// we reduce in increasing RuleID order then we need never backtrack --
// prerequisite reductions are reached before dependent ones).
std::vector<Rule> Rules; std::vector<Rule> Rules;
// A table of terminals (aka tokens). It corresponds to the clang::Token. // A table of terminals (aka tokens). It corresponds to the clang::Token.
// clang::tok::TokenKind is the index of the table. // clang::tok::TokenKind is the index of the table.

View File

@ -9,9 +9,11 @@
#include "clang-pseudo/Grammar.h" #include "clang-pseudo/Grammar.h"
#include "clang/Basic/TokenKinds.h" #include "clang/Basic/TokenKinds.h"
#include "llvm/ADT/DenseSet.h" #include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringExtras.h"
#include "llvm/Support/FormatVariadic.h" #include "llvm/Support/FormatVariadic.h"
#include <memory> #include <memory>
#include <utility>
namespace clang { namespace clang {
namespace pseudo { namespace pseudo {
@ -87,23 +89,75 @@ public:
} }
assert(T->Rules.size() < (1 << RuleBits) && assert(T->Rules.size() < (1 << RuleBits) &&
"Too many rules to fit in RuleID bits!"); "Too many rules to fit in RuleID bits!");
llvm::sort(T->Rules, [](const Rule &Left, const Rule &Right) { const auto &SymbolOrder = getTopologicalOrder(T.get());
// Sorted by the Target. llvm::stable_sort(
return std::tie(Left.Target, Left.Size) < T->Rules, [&SymbolOrder](const Rule &Left, const Rule &Right) {
std::tie(Right.Target, Right.Size); // Sorted by the topological order of the nonterminal Target.
}); return SymbolOrder[Left.Target] < SymbolOrder[Right.Target];
RuleID RulePos = 0; });
for (SymbolID SID = 0; SID < T->Nonterminals.size(); ++SID) { for (SymbolID SID = 0; SID < T->Nonterminals.size(); ++SID) {
RuleID Start = RulePos; auto StartIt = llvm::partition_point(T->Rules, [&](const Rule &R) {
while (RulePos < T->Rules.size() && T->Rules[RulePos].Target == SID) return SymbolOrder[R.Target] < SymbolOrder[SID];
++RulePos; });
T->Nonterminals[SID].RuleRange = {Start, RulePos}; RuleID Start = StartIt - T->Rules.begin();
RuleID End = Start;
while (End < T->Rules.size() && T->Rules[End].Target == SID)
++End;
T->Nonterminals[SID].RuleRange = {Start, End};
} }
auto G = std::make_unique<Grammar>(std::move(T)); auto G = std::make_unique<Grammar>(std::move(T));
diagnoseGrammar(*G); diagnoseGrammar(*G);
return G; return G;
} }
// Gets topological order for nonterminal symbols.
//
// The topological order is defined as: if a *single* nonterminal A produces
// (or transitively) a nonterminal B (that said, there is a production rule
// B := A), then A is less than B.
//
// It returns the sort key for each symbol, the array is indexed by SymbolID.
std::vector<unsigned> getTopologicalOrder(GrammarTable *T) {
std::vector<std::pair<SymbolID, SymbolID>> Dependencies;
for (const auto &Rule : T->Rules) {
// if A := B, A depends on B.
if (Rule.Size == 1 && pseudo::isNonterminal(Rule.Sequence[0]))
Dependencies.push_back({Rule.Target, Rule.Sequence[0]});
}
llvm::sort(Dependencies);
std::vector<SymbolID> Order;
// Each nonterminal state flows: NotVisited -> Visiting -> Visited.
enum State {
NotVisited,
Visiting,
Visited,
};
std::vector<State> VisitStates(T->Nonterminals.size(), NotVisited);
std::function<void(SymbolID)> DFS = [&](SymbolID SID) -> void {
if (VisitStates[SID] == Visited)
return;
if (VisitStates[SID] == Visiting) {
Diagnostics.push_back(
llvm::formatv("The grammar contains a cycle involving symbol {0}",
T->Nonterminals[SID].Name));
return;
}
VisitStates[SID] = Visiting;
for (auto It = llvm::lower_bound(Dependencies,
std::pair<SymbolID, SymbolID>{SID, 0});
It != Dependencies.end() && It->first == SID; ++It)
DFS(It->second);
VisitStates[SID] = Visited;
Order.push_back(SID);
};
for (SymbolID ID = 0; ID != T->Nonterminals.size(); ++ID)
DFS(ID);
std::vector<unsigned> Result(T->Nonterminals.size(), 0);
for (size_t I = 0; I < Order.size(); ++I)
Result[Order[I]] = I;
return Result;
}
private: private:
// Text representation of a BNF grammar rule. // Text representation of a BNF grammar rule.
struct RuleSpec { struct RuleSpec {

View File

@ -26,4 +26,4 @@ id := IDENTIFIER
# TABLE-NEXT: State 2 # TABLE-NEXT: State 2
# TABLE-NEXT: 'EOF': reduce by rule 1 'expr := id' # TABLE-NEXT: 'EOF': reduce by rule 1 'expr := id'
# TABLE-NEXT: State 3 # TABLE-NEXT: State 3
# TABLE-NEXT: 'EOF': reduce by rule 2 'id := IDENTIFIER' # TABLE-NEXT: 'EOF': reduce by rule 0 'id := IDENTIFIER'

View File

@ -5,8 +5,8 @@ expr := IDENTIFIER
# RUN: clang-pseudo -grammar %s -print-graph | FileCheck %s --check-prefix=GRAPH # RUN: clang-pseudo -grammar %s -print-graph | FileCheck %s --check-prefix=GRAPH
# GRAPH: States # GRAPH: States
# GRAPH-NEXT: State 0 # GRAPH-NEXT: State 0
# GRAPH-NEXT: _ := • expr
# GRAPH-NEXT: expr := • expr - expr # GRAPH-NEXT: expr := • expr - expr
# GRAPH-NEXT: _ := • expr
# GRAPH-NEXT: expr := • IDENTIFIER # GRAPH-NEXT: expr := • IDENTIFIER
# GRAPH-NEXT: State 1 # GRAPH-NEXT: State 1
# GRAPH-NEXT: _ := expr • # GRAPH-NEXT: _ := expr •
@ -42,6 +42,6 @@ expr := IDENTIFIER
# TABLE-NEXT: 'IDENTIFIER': shift state 2 # TABLE-NEXT: 'IDENTIFIER': shift state 2
# TABLE-NEXT: 'expr': go to state 4 # TABLE-NEXT: 'expr': go to state 4
# TABLE-NEXT: State 4 # TABLE-NEXT: State 4
# TABLE-NEXT: 'EOF': reduce by rule 2 'expr := expr - expr' # TABLE-NEXT: 'EOF': reduce by rule 0 'expr := expr - expr'
# TABLE-NEXT: '-': shift state 3 # TABLE-NEXT: '-': shift state 3
# TABLE-NEXT: '-': reduce by rule 2 'expr := expr - expr' # TABLE-NEXT: '-': reduce by rule 0 'expr := expr - expr'

View File

@ -44,6 +44,16 @@ public:
return 0; return 0;
} }
RuleID ruleFor(llvm::StringRef NonterminalName) const {
auto RuleRange = G->table().Nonterminals[id(NonterminalName)].RuleRange;
if (RuleRange.End - RuleRange.Start == 1)
return G->table().Nonterminals[id(NonterminalName)].RuleRange.Start;
ADD_FAILURE() << "Expected a single rule for " << NonterminalName
<< ", but it has " << RuleRange.End - RuleRange.Start
<< " rule!\n";
return 0;
}
protected: protected:
std::unique_ptr<Grammar> G; std::unique_ptr<Grammar> G;
std::vector<std::string> Diags; std::vector<std::string> Diags;
@ -74,6 +84,21 @@ TEST_F(GrammarTest, EliminatedOptional) {
Sequence(id("INT"), id(";")))); Sequence(id("INT"), id(";"))));
} }
TEST_F(GrammarTest, RuleIDSorted) {
build(R"bnf(
_ := x
x := y
y := z
z := IDENTIFIER
)bnf");
ASSERT_TRUE(Diags.empty());
EXPECT_LT(ruleFor("z"), ruleFor("y"));
EXPECT_LT(ruleFor("y"), ruleFor("x"));
EXPECT_LT(ruleFor("x"), ruleFor("_"));
}
TEST_F(GrammarTest, Diagnostics) { TEST_F(GrammarTest, Diagnostics) {
build(R"cpp( build(R"cpp(
_ := ,_opt _ := ,_opt
@ -82,6 +107,9 @@ TEST_F(GrammarTest, Diagnostics) {
_ := IDENFIFIE # a typo of the terminal IDENFITIER _ := IDENFIFIE # a typo of the terminal IDENFITIER
invalid invalid
# cycle
a := b
b := a
)cpp"); )cpp");
EXPECT_EQ(G->startSymbol(), id("_")); EXPECT_EQ(G->startSymbol(), id("_"));
@ -91,7 +119,8 @@ TEST_F(GrammarTest, Diagnostics) {
"No rules for nonterminal: undefined-sym", "No rules for nonterminal: undefined-sym",
"Failed to parse 'invalid': no separator :=", "Failed to parse 'invalid': no separator :=",
"Token-like name IDENFIFIE is used as a nonterminal", "Token-like name IDENFIFIE is used as a nonterminal",
"No rules for nonterminal: IDENFIFIE")); "No rules for nonterminal: IDENFIFIE",
"The grammar contains a cycle involving symbol a"));
} }
TEST_F(GrammarTest, FirstAndFollowSets) { TEST_F(GrammarTest, FirstAndFollowSets) {