C++17 僅標頭 PEG(解析表達式語法)函式庫。您只需在專案中包含peglib.h即可立即開始使用它。
由於該程式庫僅支援 C++17 編譯器,因此請確保啟用編譯器選項-std=c++17 。 ( /std:c++17 /Zc:__cplusplus對於 MSVC)
您也可以嘗試線上版本 PEG Playground,網址為 https://yhirose.github.io/cpp-peglib。
Bryan Ford 的文檔第 2 頁對 PEG 語法進行了詳細描述。 cpp-peglib目前也支援以下附加語法:
'...'i (不區分大小寫的文字運算子)
[...]i (不區分大小寫的字元類運算子)
[^...] (否定字元類運算子)
[^...]i (不區分大小寫的否定字元類運算子)
{2,5} (類似正規表示式的重複運算子)
< ... > (令牌邊界運算子)
~ (忽略運算子)
x20 (十六進位數字字元)
u10FFFF (Unicode 字元)
%whitespace (自動跳過空格)
%word (字表達式)
$name( ... ) (捕獲範圍運算子)
$name< ... > (命名捕獲運算子)
$name (反向引用運算子)
| (字典運算子)
↑ (剪切運算子)
MACRO_NAME( ... ) (參數化規則或巨集)
{ precedence L - + L / * } (解析中綴表達式)
%recovery( ... ) (錯誤恢復運算子)
exp⇑label或exp^label ( (exp / %recover(label))的語法糖)
label { error_message "..." } (錯誤訊息說明)
{ no_ast_opt } (無 AST 節點最佳化指令)
預設將進行「輸入結束」檢查。若要停用檢查,請呼叫disable_eoi_check 。
該函式庫支援稱為Packrat解析的線性時間解析。
對於某些 Linux 發行版(例如 Ubuntu 和 CentOS)的重要提示:連結時需要-pthread選項。參見#23、#46 和#62。
我相信您會喜歡 bert hubert 撰寫的這篇精彩的「使用 PEG 和 cpp-peglib 進行實用解析」文章!
這是一個簡單的計算器範例。它展示瞭如何定義語法、將語義動作與語法關聯以及處理語義值。
// (1) 包含頭檔#include <peglib.h>#include <assert.h>#include <iostream>using namespace peg;using namespace std;int main(void) { // (2) 建立解析器
parser parser(R"( # 計算機語法... Additive <- 乘法 '+' 加法 / 乘法 乘法 <- Primary '*' 乘法 / Primary Primary <- '(' 加法 ')' / Number Number <- < [ 0-9]+ > %空白<- [ t]* )"); 斷言(static_cast<bool>(解析器) == true); // (3) 設定動作
parser["Additive"] = [](const SemanticValues &vs) {switch (vs.choice()) {case 0: // “乘法 '+' 加法” return any_cast<int>(vs[0]) + any_cast< int>(vs[1]);預設值: // “乘法” return any_cast<int>(vs[0]);
}
};
parser["Multiplicative"] = [](const SemanticValues &vs) {switch (vs.choice()) {case 0: // "Primary '*' Multiplicative" return any_cast<int>(vs[0]) * any_cast< int>(vs[1]);default: // “Primary” return any_cast<int>(vs[0]);
}
};
parser["Number"] = [](const SemanticValues &vs) {return vs.token_to_number<int>();
}; // (4) 解析
parser.enable_packrat_parsing(); // 啟用 Packrat 解析。
整數值;
parser.parse(" (1 + 2) * 3 ", val); 斷言(val == 9);
}若要顯示語法文字中的語法錯誤:
auto Grammar = R"( # 計算機語法... Additive <- 乘法 '+' Additive / Multiplicative Multiplicative <- Primary '*' Multiplicative / Primary Primary <- '(' Additive ')' / Number Number <- < [ 0-9]+ > %空白<- [ t]*)";
解析器 解析器;
parser.set_logger([](size_t line, size_t col, const string& msg, const string &rule) {
cerr << 行 << ":" << col << ":" << msg << "n";
});自動確定 = parser.load_grammar(語法);斷言(確定);有四種可用的語意操作:
[](const SemanticValues& vs, any& dt) [](const SemanticValues& vs) [](SemanticValues& vs, any& dt) [](語意值& vs)
SemanticValues值包含以下資訊:
語意值
匹配的字串訊息
令牌資訊(如果規則是文字規則或使用令牌邊界運算子)
規則為「優先選擇」時的選擇編號
any& dt是一個「讀寫」上下文數據,可用於任何目的。初始上下文資料在peg::parser::parse方法中設定。
語意操作可以傳回任意資料類型的值,該值將由peg::any包裝。如果使用者在語意操作中不傳回任何內容,則會傳回const SemanticValues& vs參數中的第一個語意值。 (Yacc 解析器具有相同的行為。)
這裡顯示了SemanticValues結構:
struct SemanticValues :受保護的 std::vector<any>
{ // 輸入文字
const char* 路徑; 常量字元* ss; // 匹配的字串
std::string_view sv() const { 回傳 sv_; } // 符合字串所在的行號和列
std::pair<size_t, size_t> line_info() const; // 代幣
std::vector<std::string_view> 標記;
std::string_view 標記(size_t id = 0) const; // 令牌轉換
std::string token_to_string(size_t id = 0) const; template <typename T> T token_to_number() const; // 選擇編號(基於 0 的索引)
size_t choice() const; // 將語意值向量轉換為另一個向量
模板 <類型名稱 T> 向量 <T> 變換(size_t beg = 0, size_t end = -1) const;
}以下範例使用< ... >運算符,它是標記邊界運算符。
peg::parser 解析器(R"( ROOT <- _ TOKEN (',' _ TOKEN)* TOKEN <- < [a-z0-9]+ > _ _ <- [ trn]*)");
parser["TOKEN"] = [](const SemanticValues& vs) { // 'token' 不包含尾隨空格
自動令牌 = vs.token();
};auto ret = parser.parse(" token1, token2 ");我們可以使用~運算子從列表中忽略不必要的語意值。
peg::parser 解析器(R"( ROOT <- _ ITEM (',' _ ITEM _)* ITEM <- ([a-z0-9])+ ~_ <- [ t]*)");
parser["ROOT"] = [&](const SemanticValues& vs) { 斷言(vs.size() == 2); // 應該是 2 而不是 5。下面的語法與上面的相同。
peg::parser 解析器(R"( ROOT <- ~_ ITEM (',' ~_ ITEM ~_)* ITEM <- ([a-z0-9])+ _ <- [ t]*)") ;語義謂詞支援可透過謂詞操作獲得。
peg::parser 解析器("NUMBER <- [0-9]+");
parser["NUMBER"] = [](const SemanticValues &vs) { return vs.token_to_number<long>();
};
parser["NUMBER"].predicate = [](const SemanticValues &vs,const std::any & /*dt*/, std::string &msg) { if (vs.token_to_number<long>() != 100) {
msg = "值錯誤!!";返回 false;
返回真;
};long val;auto ret = parser.parse("100", val);assert(ret == true);assert(val == 100);
ret = parser.parse("200", val);assert(ret == false);還可以執行進入和離開操作。
parser["RULE"].enter = [](const Context &c, const char* s, size_t n, any& dt) {
std::cout <<“輸入”<< std::endl;
};
解析器["RULE"] = [](const SemanticValues& vs, any& dt) {
std::cout <<“行動!” << std::endl;
};
parser["RULE"].leave = [](const Context &c, const char* s, size_t n, size_t matchlen, any& value, any& dt) {
std::cout << "離開" << std::endl;
};您可以透過記錄器接收錯誤訊息:
parser.set_logger([](size_t line, size_t col, const string& msg) {
……
});
parser.set_logger([](size_t line, size_t col, const string& msg, const string &rule) {
……
});正如您在第一個範例中看到的,我們可以使用%whitespace規則自動忽略標記之間的空格。
%whitespace規則可以套用於以下三種情況:
標記上的尾隨空格
文字上的前導空格
規則中文字字串的尾隨空格
這些是有效的令牌:
KEYWORD <- 'keyword' KEYWORDI <- 'case_insensitive_keyword' WORD <- < [a-zA-Z0-9] [a-zA-Z0-9-_]* > # token boundary operator is used. IDNET <- < IDENT_START_CHAR IDENT_CHAR* > # token boundary operator is used.
以下語法接受one, "two three", four 。
ROOT <- ITEM (',' ITEM)*
ITEM <- WORD / PHRASE
WORD <- < [a-z]+ >
PHRASE <- < '"' (!'"' .)* '"' >
%whitespace <- [ trn]*peg::parser 解析器(R"( ROOT <- 'hello' 'world' %whitespace <- [ trn]* %word <- [az]+)");
parser.parse("你好世界"); // OKparser.parse("helloworld"); // 錯誤peg::parser 解析器(R"( ROOT <- CONTENT CONTENT <- (ELEMENT / TEXT)* ELEMENT <- $(STAG CONTENT ETAG) STAG <- '<' $tag< TAG_NAME > '>' ETAG <- ' < /' $tag '>' TAG_NAME <- 'b' / 'u' TEXT <- TEXT_DATA TEXT_DATA <- ![<] .)");
parser.parse("這是<b>一個<u>測試</u>文字</b>。"); // OKparser.parse("這是<b>一個<u>測試</b>文字</u>。"); // NGparser.parse("這是<b><u>測試文字</b>。"); // 錯誤|運算符允許我們透過內部使用Trie結構來製作一個用於快速查找的單字字典。我們不必擔心單字的順序。
START <- 'This month is ' MONTH '.'
MONTH <- 'Jan' | 'January' | 'Feb' | 'February' | '...'我們能夠找到與choice()相符的項目。
parser["MONTH"] = [](const SemanticValues &vs) { auto id = vs.choice();
};它支援不區分大小寫的模式。
START <- 'This month is ' MONTH '.'
MONTH <- 'Jan'i | 'January'i | 'Feb'i | 'February'i | '...'i↑運算子可以緩解回溯效能問題,但有改變語法意義的風險。
S <- '(' ↑ P ')' / '"' ↑ P '"' / P
P <- 'a' / 'b' / 'c'當我們用上面的語法解析(z時,在(匹配後,我們不必在S中回溯,因為那裡插入了一個剪切運算子。
# Syntax
Start ← _ Expr
Expr ← Sum
Sum ← List(Product, SumOpe)
Product ← List(Value, ProOpe)
Value ← Number / T('(') Expr T(')')
# Token
SumOpe ← T('+' / '-')
ProOpe ← T('*' / '/')
Number ← T([0-9]+)
~_ ← [ trn]*
# Macro
List(I, D) ← I (D I)*
T(x) ← < x > _關於優先攀爬演算法,請看這篇文章。
解析器解析器(R"( EXPRESSION <- IFIX_EXPRESSION(ATOM, OPERATOR) ATOM <- NUMBER / '(' EXPRESSION ')' OPERATOR <- < [-+/*] > NUMBER <- < '-'? [0 -9 ]+ > %whitespace <- [ t]* # 宣告優先權順序 IFIX_EXPRESSION(A, O) <- A (O A)* { 優先權 L + - L * / })");
parser["INFIX_EXPRESSION"] = [](const SemanticValues& vs) -> long { auto result = any_cast<long>(vs[0]); if (vs.size() > 1) {auto ope = any_cast<char>(vs[1]);auto num = any_cast<long>(vs[2]);switch (ope) { case '+': 結果+= 數字;休息; case '-': 結果 -= num;休息; case '*': 結果 *= num;休息; case '/': 結果 /= num;休息;
}
返回結果;
};
parser["OPERATOR"] = [](const SemanticValues& vs) { return *vs.sv(); };
parser["NUMBER"] = [](const SemanticValues& vs) { return vs.token_to_number<long>(); };長值;
parser.parse(" -1 + (1 + 2) * 3 - -1", val);assert(val == 9);優先指令只能套用於以下「清單」樣式規則。
Rule <- Atom (Operator Atom)* {
precedence
L - +
L / *
R ^
}優先指令包含優先資訊條目。每個條目都以關聯性“L”(左)或“R”(右)開頭,然後是運算符文字標記。第一個條目具有最高的訂單等級。
cpp-peglib能夠在解析時產生 AST(抽象語法樹)。 peg::parser類別上的enable_ast方法啟用該功能。
注意:AST 節點將對應的令牌儲存為std::string_vew以提高效能並減少記憶體使用。使用者有責任保留原始來源文字以及生成的 AST 樹。
peg::parser parser(R"(
...
definition1 <- ... { no_ast_opt }
definition2 <- ... { no_ast_opt }
...
)");
parser.enable_ast();
shared_ptr<peg::Ast> ast;
if (parser.parse("...", ast)) {
cout << peg::ast_to_s(ast);
ast = parser.optimize_ast(ast);
cout << peg::ast_to_s(ast);
} optimize_ast刪除冗餘節點以使 AST 更簡單。如果您想從特定規則中停用此行為,可以使用no_ast_opt指令。
它在內部調用peg::AstOptimizer來完成這項工作。您可以建立自己的 AST 優化器來滿足您的需求。
請參閱 AST 計算器範例和 PL/0 語言範例中的實際用法。
我們也可以使用解析器組合器手動建立解析器,而不是透過解析 PEG 語法文字來製作解析器。這是一個例子:
使用命名空間 peg;使用命名空間 std;向量<string> 標籤;
定義ROOT、TAG_NAME、_;
ROOT <= seq(_, zom(seq(chr('['), TAG_NAME, chr(']'), _)));
TAG_NAME <= oom(seq(npd(chr(']')), dot())), [&](const SemanticValues& vs) {
Tags.push_back(vs.token_to_string());
};
_ <= zom(cls(" t"));auto ret = ROOT.parse(" [tag1] [tag:2] [tag-3] ");以下是可用的運算符:
| 操作員 | 描述 | 操作員 | 描述 |
|---|---|---|---|
| 序列 | 順序 | 町 | 優先選擇 |
| 佐姆 | 零個或更多 | 奧姆 | 一個或多個 |
| 選擇 | 選修的 | 阿帕德 | 和謂詞 |
| NPD | 不是謂詞 | 點亮 | 文字字串 |
| 利蒂 | 不區分大小寫 文字字串 | CLS | 字元類 |
| NCLS | 否定字元類 | chr | 特點 |
| 點 | 任意角色 | 托克 | 代幣邊界 |
| 伊恩 | 忽略語意值 | CSC | 捕捉範圍 |
| 帽子 | 捕獲 | 貝克爾 | 反向參考 |
| 迪克 | 字典 | 前 | 中綴表達式 |
| 記錄 | 中綴表達式 | 使用者 | 使用者定義的解析器 |
| 代表 | 重複 |
可以新增/覆蓋定義。
自動語法 = R"( ROOT <- _ 'Hello' _ NAME '!' _)";
規則 附加規則 = {
{"NAME", usr([](const char* s, size_t n, SemanticValues& vs, any& dt) -> size_t { 靜態向量<string> 名稱 = { "PEG", "BNF" }; for (const auto& name : names) {if (name.size() <= n && !name.compare(0, name.size(), s, name.size())) { return name.size(); // 處理後的長度 }
返回-1; // 解析錯誤})
},
{“~_”,zom(cls(“trn”))
}
};auto g = 解析器(語法,additional_rules);assert(g.parse(“你好BNF!”));cpp-peglib 接受 UTF8 文本。 .匹配 Unicode 碼點。還有,支持u???? 。
cpp-peglib 支援最遠故障錯誤位置報告,如 Bryan Ford 原始文件中所述。
為了更好的錯誤報告和恢復,cpp-peglib 支援帶有標籤的「恢復」運算符,該標籤可以與恢復表達式和自訂錯誤訊息關聯。這個想法來自 Sergio Medeiros 和 Fabio Mascarenhas 撰寫的精彩論文「解析表達式語法中的語法錯誤恢復」。
自訂訊息支援%t (表示意外標記的佔位符)和%c (表示意外 Unicode 字元)。
下面是一個類似 Java 語法的範例:
# java.peg
Prog ← 'public' 'class' NAME '{' 'public' 'static' 'void' 'main' '(' 'String' '[' ']' NAME ')' BlockStmt '}'
BlockStmt ← '{' (!'}' Stmt^stmtb)* '}' # Annotated with `stmtb`
Stmt ← IfStmt / WhileStmt / PrintStmt / DecStmt / AssignStmt / BlockStmt
IfStmt ← 'if' '(' Exp ')' Stmt ('else' Stmt)?
WhileStmt ← 'while' '(' Exp^condw ')' Stmt # Annotated with `condw`
DecStmt ← 'int' NAME ('=' Exp)? ';'
AssignStmt ← NAME '=' Exp ';'^semia # Annotated with `semi`
PrintStmt ← 'System.out.println' '(' Exp ')' ';'
Exp ← RelExp ('==' RelExp)*
RelExp ← AddExp ('<' AddExp)*
AddExp ← MulExp (('+' / '-') MulExp)*
MulExp ← AtomExp (('*' / '/') AtomExp)*
AtomExp ← '(' Exp ')' / NUMBER / NAME
NUMBER ← < [0-9]+ >
NAME ← < [a-zA-Z_][a-zA-Z_0-9]* >
%whitespace ← [ tn]*
%word ← NAME
# Recovery operator labels
semia ← '' { error_message "missing semicolon in assignment." }
stmtb ← (!(Stmt / 'else' / '}') .)* { error_message "invalid statement" }
condw ← &'==' ('==' RelExp)* / &'<' ('<' AddExp)* / (!')' .)*例如, ';'^semi是(';' / %recovery(semi))的語法糖。 %recover運算子嘗試恢復「;」處的錯誤透過使用恢復表達式semi跳過輸入文字。 semi也與自訂訊息「賦值中缺少分號」相關聯。
結果如下:
> cat Sample.javapublic class 範例 { public static void main(String[] args) {int n = 5;int f = 1;while( < n) { f = f * n; } n = n - 1};System.out.println(f);
}
}
> peglint java.peg sample.javasample.java:5:12: 語法錯誤,意外的'<', 期望'(', <NUMBER>, <NAME>.sample.java:8:5: 賦值.sample 中缺少分號.java:8:6:無效語句正如您所看到的,它現在可以顯示多個錯誤,並提供比預設訊息更有意義的錯誤訊息。
我們可以將自訂錯誤訊息與定義相關聯。
# custom_message.peg
START <- CODE (',' CODE)*
CODE <- < '0x' [a-fA-F0-9]+ > { error_message 'code format error...' }
%whitespace <- [ t]*> cat custom_message.txt 0x1234,0x@@@@,0xABCD > peglint custom_message.peg custom_message.txt custom_message.txt:1:8: code format error...
注意:如果優先選擇中有多個帶有錯誤訊息指令的元素,則此功能可能無法如您的預期運作。
我們可以如下更改開始定義規則。
自動語法 = R"( Start <- A A <- B (',' B)* B <- '[一]' / '[二]' %whitespace <- [ tn]*)";
peg::parser 解析器(語法,“A”); // 開始規則是“A”
orpeg::parser 解析器;
parser.load_grammar(文法, "A"); // 起始規則為 "A"parser.parse(" [one] , [two] "); // 好的> cd lint > mkdir build > cd build > cmake .. > make > ./peglint usage: grammar_file_path [source_file_path] options: --source: source text --packrat: enable packrat memoise --ast: show AST tree --opt, --opt-all: optimize all AST nodes except nodes selected with `no_ast_opt` instruction --opt-only: optimize only AST nodes selected with `no_ast_opt` instruction --trace: show concise trace messages --profile: show profile report --verbose: verbose output for trace and profile
> cat a.peg
Additive <- Multiplicative '+' Additive / Multiplicative
Multiplicative <- Primary '*' Multiplicative / Primary
Primary <- '(' Additive ')' / Number
%whitespace <- [ trn]*
> peglint a.peg
[commandline]:3:35: 'Number' is not defined.> cat a.peg
Additive <- Multiplicative '+' Additive / Multiplicative
Multiplicative <- Primary '*' Multiplicative / Primary
Primary <- '(' Additive ')' / Number
Number <- < [0-9]+ >
%whitespace <- [ trn]*
> peglint --source "1 + a * 3" a.peg
[commandline]:1:3: syntax error> cat a.txt 1 + 2 * 3 > peglint --ast a.peg a.txt + Additive + Multiplicative + Primary - Number (1) + Additive + Multiplicative + Primary - Number (2) + Multiplicative + Primary - Number (3)
> peglint --ast --opt --source "1 + 2 * 3" a.peg + Additive - Multiplicative[Number] (1) + Additive[Multiplicative] - Primary[Number] (2) - Multiplicative[Number] (3)
no_ast_opt指令調整 AST 最佳化> cat a.peg
Additive <- Multiplicative '+' Additive / Multiplicative
Multiplicative <- Primary '*' Multiplicative / Primary
Primary <- '(' Additive ')' / Number { no_ast_opt }
Number <- < [0-9]+ >
%whitespace <- [ trn]*
> peglint --ast --opt --source "1 + 2 * 3" a.peg
+ Additive/0
+ Multiplicative/1[Primary]
- Number (1)
+ Additive/1[Multiplicative]
+ Primary/1
- Number (2)
+ Multiplicative/1[Primary]
- Number (3)
> peglint --ast --opt-only --source "1 + 2 * 3" a.peg
+ Additive/0
+ Multiplicative/1
- Primary/1[Number] (1)
+ Additive/1
+ Multiplicative/0
- Primary/1[Number] (2)
+ Multiplicative/1
- Primary/1[Number] (3)計算機
計算器(帶有解析器運算子)
計算器(AST版)
計算器(依優先權攀爬解析表達式)
計算器(AST 版本和按優先級攀爬解析表達式)
一個微型 PL/0 JIT 編譯器,少於 900 個 LOC,帶有 LLVM 和 PEG 解析器
一種專門用於編寫 Fizz Buzz 程式的程式語言。 :)
麻省理工學院許可證(© 2022 Yuji Hirose)