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)