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。};auto ret = parser.parse(" item1, item2 ");下面的语法与上面的相同。
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 : 名称) {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)