使用 git 中的 cparse 库解析用户输入的字符串

Parsing strings of user input using the cparse library from git

本文关键字:用户 输入 字符串 git 中的 cparse 使用      更新时间:2023-10-16

我是 c++ 编程的新手,希望在我的项目中 https://github.com/cparse/cparse 使用这里的cparse 库。我想解释像"a*(b+3)"这样的用户输入字符串(对于变量ab),并在不同的输入集上反复使用它作为函数。

例如,将一个文本文件作为输入,每行有 2 个double数字,我的代码将写入一个新文件,每行的结果为 "a*(b+3)"(假设a是第一个数字,b是第二个数字)。

当我尝试从 git 包含cparse 库时,我的问题出现了。我天真地遵循了设置说明(是 git 的新手):

$ cd cparse
$ make release

但是我不能使用 make 命令,因为我正在使用窗口。我尝试下载zip文件并将.cpp.h文件直接复制到项目中,并使用Visual Studio中的"包括现有">选项,但是遇到大量编译器错误,无法使代码自己工作。

我是否以某种方式错过了重点?我如何让它工作?

如果做不到这一点,有没有另一种方法来解析用户输入的字符串并将它们用作函数?

如果做不到这一点,有没有另一种方法可以解析用户输入字符串并将它们用作函数?

我想回答你问题的这一部分,因为我觉得一个功能齐全的 C 解析器对于你的意图来说可能有点太重了。(顺便说一句,一旦你让 C 解析器运行 – 如何处理它的输出?动态链接?

相反,我想向您展示如何自己构建一个简单的计算器(带有递归下降解析器)。对于我将使用的技术的文档,我热烈推荐Aho,Lam,Sethi,Ullman的编译器(原理,技术和工具)(更广为人知的"龙书"),尤其是第4章

微型计算器项目

在下文中,我将逐个部分地描述我的示例解决方案。

语言设计

在开始编写编译器或解释器之前,定义一种应该被接受的语言是合理的。我想使用一个非常有限的 C 子集:表达式,包括

  • C 类浮点数(常量)
  • 类 C 标识符(变量)
  • 一元运算符+-
  • 二元运算符+-*/
  • 括号()
  • 分号;(用于标记表达式的结尾,必填项)。

空格(包括换行符)将被简单地忽略,但可用于分隔事物以及提高人类可读性。C 或C++喜欢注释(以及许多其他糖),我没有考虑将源代码尽可能少。(尽管如此,我得到了近500行。

OP 的具体示例将适合此语言,并添加分号:

a*(b+3);

将仅支持一种类型:double。因此,我不需要类型或任何使事情变得更容易的声明。

在我开始勾勒这种语言的语法之前,我正在考虑编译的"目标",并决定为...

抽象语法树

首先 – 一个存储变量的类:

// storage of variables
class Var {
private:
double _value;
public:
Var(): _value() { }
~Var() = default;
double get() const { return _value; }
void set(double value) { _value = value; }
};

变量为值提供存储,但不为标识符提供存储。后者是单独存储的,因为它不需要用于变量的实际使用,而只是按名称查找它:

typedef std::map<std::string, Var> VarTable;

使用std::map可自动创建变量。正如许多高级语言所知道的那样,变量在第一次访问时就开始存在。

抽象语法树是一个

用编程语言编写的源代码的抽象语法结构的树表示形式。树的每个节点表示源代码中发生的构造。

我从上面链接的维基百科文章中获取了这段文字 - 不能说得更短。在以下我的 AST 课程中:

// abstract syntax tree -> storage of "executable"
namespace AST {
class Expr {
protected:
Expr() = default;
public:
virtual ~Expr() = default;
public:
virtual double solve() const = 0;
};
class ExprConst: public Expr {
private:
double _value;
public:
ExprConst(double value): Expr(), _value(value) { }
virtual ~ExprConst() = default;
virtual double solve() const { return _value; }
};
class ExprVar: public Expr {
private:
Var *_pVar;
public:
ExprVar(Var *pVar): Expr(), _pVar(pVar) { }
virtual ~ExprVar() = default;
virtual double solve() const { return _pVar->get(); }
};
class ExprUnOp: public Expr {
protected:
Expr *_pArg1;
protected:
ExprUnOp(Expr *pArg1): Expr(), _pArg1(pArg1) { }
virtual ~ExprUnOp() { delete _pArg1; }
};
class ExprUnOpNeg: public ExprUnOp {
public:
ExprUnOpNeg(Expr *pArg1): ExprUnOp(pArg1) { }
virtual ~ExprUnOpNeg() = default;
virtual double solve() const
{
return -_pArg1->solve();
}
};
class ExprBinOp: public Expr {
protected:
Expr *_pArg1, *_pArg2;
protected:
ExprBinOp(Expr *pArg1, Expr *pArg2):
Expr(), _pArg1(pArg1), _pArg2(pArg2)
{ }
virtual ~ExprBinOp() { delete _pArg1; delete _pArg2; }
};
class ExprBinOpAdd: public ExprBinOp {
public:
ExprBinOpAdd(Expr *pArg1, Expr *pArg2): ExprBinOp(pArg1, pArg2) { }
virtual ~ExprBinOpAdd() = default;
virtual double solve() const
{
return _pArg1->solve() + _pArg2->solve();
}
};
class ExprBinOpSub: public ExprBinOp {
public:
ExprBinOpSub(Expr *pArg1, Expr *pArg2): ExprBinOp(pArg1, pArg2) { }
virtual ~ExprBinOpSub() = default;
virtual double solve() const
{
return _pArg1->solve() - _pArg2->solve();
}
};
class ExprBinOpMul: public ExprBinOp {
public:
ExprBinOpMul(Expr *pArg1, Expr *pArg2): ExprBinOp(pArg1, pArg2) { }
virtual ~ExprBinOpMul() = default;
virtual double solve() const
{
return _pArg1->solve() * _pArg2->solve();
}
};
class ExprBinOpDiv: public ExprBinOp {
public:
ExprBinOpDiv(Expr *pArg1, Expr *pArg2): ExprBinOp(pArg1, pArg2) { }
virtual ~ExprBinOpDiv() = default;
virtual double solve() const
{
return _pArg1->solve() / _pArg2->solve();
}
};

因此,使用 AST 类,示例a*(b+3)的表示形式将是

VarTable varTable;
Expr *pExpr
= new ExprBinOpMul(
new ExprVar(&varTable["a"]),
new ExprBinOpAdd(
new ExprVar(&varTable["b"]),
new ExprConst(3)));

注意:

没有从Expr派生的类来表示括号()因为这根本不必要。在构建树本身时,会考虑括号的处理。通常,优先级较高的运算符将成为优先级较低的运算符的子级。因此,前者在后者之前计算。在上面的示例中,ExprBinOpAdd的实例是ExprBinOpMul实例的子实例(尽管乘法的优先级高于 add 的优先级),这是正确考虑括号的结果。

除了存储解析的表达式之外,此树还可用于通过调用根节点的Expr::solve()方法来计算表达式:

double result = pExpr->solve();

为我们的小计算器有一个后端,接下来是前端。

语法

形式语言最好用语法来描述。

program
: expr Semicolon program
| <empty>
;
expr
: addExpr
;
addExpr
: mulExpr addExprRest
;
addExprRest
: addOp mulExpr addExprRest
| <empty>
;
addOp
: Plus | Minus
;
mulExpr
: unaryExpr mulExprRest
;
mulExprRest
: mulOp unaryExpr mulExprRest
| <empty>
;
mulOp
: Star | Slash
;
unaryExpr
: unOp unaryExpr
| primExpr
;
unOp
: Plus | Minus
;
primExpr
: Number
| Id
| LParen expr RParen
;

带有开始符号program.

规则包含

  • 终端符号(以大写字母开头)和
  • 非终端符号(以小写开头)
  • 冒号(:)用于分隔左侧和右侧(左侧的非终端可以扩展到右侧的符号)。
  • 垂直条(|)以分离替代方案
  • 一个<empty>符号,用于扩展为零(用于终止递归)。

从终端符号中,我将派生扫描仪的令牌。

非终端符号将转换为解析器函数。

addExprmulExpr的分离是有意为之的。因此,乘法运算符优先于加法运算符将被"烧毁"在语法本身中。显然,浮点常量、变量标识符或括号中的表达式(在primExpr中接受)将具有最高的优先级。

规则仅包含右递归。这是递归下降解析器的要求(正如我在 Dragon 书籍和调试器的实际经验中从理论上学到的那样,直到我完全理解原因)。在递归下降解析器中实现左递归规则会导致非终止递归,进而以StackOverflow结束。

扫描仪

通常将编译器拆分为扫描程序和分析器。

扫描程序处理输入字符流,并将字符分组到令牌中。令牌在解析器中用作终端符号。

对于令牌的输出,我创建了一个类。在我的专业项目中,它还会存储确切的文件位置以表示其来源。这很方便地使用源代码引用以及错误消息和调试信息的任何输出来标记创建的对象。(...留在这里以使其尽可能少...

// token class - produced in scanner, consumed in parser
struct Token {
// tokens
enum Tk {
Plus, Minus, Star, Slash, LParen, RParen, Semicolon,
Number, Id,
EOT, Error
};
// token number
Tk tk;
// lexem as floating point number
double number;
// lexem as identifier
std::string id;
// constructors.
explicit Token(Tk tk): tk(tk), number() { }
explicit Token(double number): tk(Number), number(number) { }
explicit Token(const std::string &id): tk(Id), number(), id(id) { }
};

特殊令牌有两个枚举器:

  • EOT......文本结尾(备注输入结尾)
  • Error......为不适合任何其他令牌的任何字符生成。

令牌用作实际扫描程序的输出:

// the scanner - groups characters to tokens
class Scanner {
private:
std::istream &_in;
public:
// constructor.
Scanner(std::istream &in): _in(in) { }
/* groups characters to next token until the first character
* which does not match (or end-of-file is reached).
*/
Token scan()
{
char c;
// skip white space
do {
if (!(_in >> c)) return Token(Token::EOT);
} while (isspace(c));
// classify character and build token
switch (c) {
case '+': return Token(Token::Plus);
case '-': return Token(Token::Minus);
case '*': return Token(Token::Star);
case '/': return Token(Token::Slash);
case '(': return Token(Token::LParen);
case ')': return Token(Token::RParen);
case ';': return Token(Token::Semicolon);
default:
if (isdigit(c)) {
_in.unget(); double value; _in >> value;
return Token(value);
} else if (isalpha(c) || c == '_') {
std::string id(1, c);
while (_in >> c) {
if (isalnum(c) || c == '_') id += c;
else { _in.unget(); break; }
}
return Token(id);
} else {
_in.unget();
return Token(Token::Error);
}
}
}
};

扫描程序在解析器中使用。

解析器

class Parser {
private:
Scanner _scanner;
VarTable &_varTable;
Token _lookAhead;
private:
// constructor.
Parser(std::istream &in, VarTable &varTable):
_scanner(in), _varTable(varTable), _lookAhead(Token::EOT)
{
scan(); // load look ahead initially
}
// calls the scanner to read the next look ahead token.
void scan() { _lookAhead = _scanner.scan(); }
// consumes a specific token.
bool match(Token::Tk tk)
{
if (_lookAhead.tk != tk) {
std::cerr << "SYNTAX ERROR! Unexpected token!" << std::endl;
return false;
}
scan();
return true;
}
// the rules:
std::vector<AST::Expr*> parseProgram()
{
// right recursive rule
// -> can be done as iteration
std::vector<AST::Expr*> pExprs;
for (;;) {
if (AST::Expr *pExpr = parseExpr()) {
pExprs.push_back(pExpr);
} else break;
// special error checking for missing ';' (usual error)
if (_lookAhead.tk != Token::Semicolon) {
std::cerr << "SYNTAX ERROR: Semicolon expected!" << std::endl;
break;
}
scan(); // consume semicolon
if (_lookAhead.tk == Token::EOT) return pExprs;
}
// error handling
for (AST::Expr *pExpr : pExprs) delete pExpr;
pExprs.clear();
return pExprs;
}
AST::Expr* parseExpr()
{
return parseAddExpr();
}
AST::Expr* parseAddExpr()
{
if (AST::Expr *pExpr1 = parseMulExpr()) {
return parseAddExprRest(pExpr1);
} else return nullptr; // ERROR!
}
AST::Expr* parseAddExprRest(AST::Expr *pExpr1)
{
// right recursive rule for left associative operators
// -> can be done as iteration
for (;;) {
switch (_lookAhead.tk) {
case Token::Plus:
scan(); // consume token
if (AST::Expr *pExpr2 = parseMulExpr()) {
pExpr1 = new AST::ExprBinOpAdd(pExpr1, pExpr2);
} else {
delete pExpr1;
return nullptr; // ERROR!
}
break;
case Token::Minus:
scan(); // consume token
if (AST::Expr *pExpr2 = parseMulExpr()) {
pExpr1 = new AST::ExprBinOpSub(pExpr1, pExpr2);
} else {
delete pExpr1;
return nullptr; // ERROR!
}
break;
case Token::Error:
std::cerr << "SYNTAX ERROR: Unexpected character!" << std::endl;
delete pExpr1;
return nullptr;
default: return pExpr1;
}
}
}
AST::Expr* parseMulExpr()
{
if (AST::Expr *pExpr1 = parseUnExpr()) {
return parseMulExprRest(pExpr1);
} else return nullptr; // ERROR!
}
AST::Expr* parseMulExprRest(AST::Expr *pExpr1)
{
// right recursive rule for left associative operators
// -> can be done as iteration
for (;;) {
switch (_lookAhead.tk) {
case Token::Star:
scan(); // consume token
if (AST::Expr *pExpr2 = parseUnExpr()) {
pExpr1 = new AST::ExprBinOpMul(pExpr1, pExpr2);
} else {
delete pExpr1;
return nullptr; // ERROR!
}
break;
case Token::Slash:
scan(); // consume token
if (AST::Expr *pExpr2 = parseUnExpr()) {
pExpr1 = new AST::ExprBinOpDiv(pExpr1, pExpr2);
} else {
delete pExpr1;
return nullptr; // ERROR!
}
break;
case Token::Error:
std::cerr << "SYNTAX ERROR: Unexpected character!" << std::endl;
delete pExpr1;
return nullptr;
default: return pExpr1;
}
}
}
AST::Expr* parseUnExpr()
{
// right recursive rule for right associative operators
// -> must be done as recursion
switch (_lookAhead.tk) {
case Token::Plus:
scan(); // consume token
// as a unary plus has no effect it is simply ignored
return parseUnExpr();
case Token::Minus:
scan();
if (AST::Expr *pExpr = parseUnExpr()) {
return new AST::ExprUnOpNeg(pExpr);
} else return nullptr; // ERROR!
default:
return parsePrimExpr();
}
}
AST::Expr* parsePrimExpr()
{
AST::Expr *pExpr = nullptr;
switch (_lookAhead.tk) {
case Token::Number:
pExpr = new AST::ExprConst(_lookAhead.number);
scan(); // consume token
break;
case Token::Id: {
Var &var = _varTable[_lookAhead.id]; // find or create
pExpr = new AST::ExprVar(&var);
scan(); // consume token
} break;
case Token::LParen:
scan(); // consume token
if (!(pExpr = parseExpr())) return nullptr; // ERROR!
if (!match(Token::RParen)) {
delete pExpr; return nullptr; // ERROR!
}
break;
case Token::EOT:
std::cerr << "SYNTAX ERROR: Premature EOF!" << std::endl;
break;
case Token::Error:
std::cerr << "SYNTAX ERROR: Unexpected character!" << std::endl;
break;
default:
std::cerr << "SYNTAX ERROR: Unexpected token!" << std::endl;
}
return pExpr;
}
public:
// the parser function
static std::vector<AST::Expr*> parse(
std::istream &in, VarTable &varTable)
{
Parser parser(in, varTable);
return parser.parseProgram();
}
};

基本上,解析器基本上由一堆相互调用的规则函数(根据语法规则)组成。围绕规则函数的类负责管理一些全局解析器上下文。因此,唯一的公共class Parser方法是

static std::vector<AST::Expr*> Parser::parse();

它构造一个实例(使用私有构造函数)并调用对应于起始符号Parser::parseProgram()program函数。

在内部,分析器调用Scanner::scan()方法来填充其前瞻令牌。

这是在Parser::scan()中完成的,当必须使用令牌时,总是调用它。

仔细观察,可以看到规则如何转换为解析器函数的模式:

  • 左侧的每个非终端都成为一个解析函数。(仔细观察源代码,你会发现我并没有完全这样做。一些规则已被"内联"。– 从我的角度来看,我插入了一些额外的规则来简化我从一开始就不打算改变的语法。对不起。

  • 替代方案(|)按switch (_lookAhead.tk)实现。因此,每个案例标签对应于最左侧符号可能扩展到的第一个终端(令牌)。(我相信这就是为什么它被称为"前瞻解析器"——应用规则的决定总是基于前瞻令牌来完成的。龙书有一个关于FIRST-FOLLOW集合的主题,它更详细地解释了这一点。

  • 对于终端符号,调用Parser::scan()。在特殊情况下,如果正好需要一个终端(令牌),则将其替换为Parser::match()

  • 对于非终端符号,完成相应函数的调用。

  • 符号序列只是作为上述调用的序列完成的。

这个解析器的错误处理是我做过的最简单的。它可以/应该做更多的支持(投入更多的精力,即额外的代码行)。(...但在这里我试图保持最小...

为了进行测试和演示,我准备了一个带有一些内置示例(要处理的程序和数据源代码)的main()函数:

// a sample application
using namespace std;
int main()
{
// the program:
const char *sourceCode =
"1 + 2 * 3 / 4 - 5;n"
"a + b;n"
"a - b;n"
"a * b;n"
"a / b;n"
"a * (b + 3);n";
// the variables
const char *vars[] = { "a", "b" };
enum { nVars = sizeof vars / sizeof *vars };
// the data
const double data[][nVars] = {
{ 4.0, 2.0 },
{ 2.0, 4.0 },
{ 10.0, 5.0 },
{ 42, 6 * 7 }
};
// compile program
stringstream in(sourceCode);
VarTable varTable;
vector<AST::Expr*> program = Parser::parse(in, varTable);
if (program.empty()) {
cerr << "ERROR: Compile failed!" << endl;
string line;
if (getline(in, line)) {
cerr << "Text at error: '" << line << "'" << endl;
}
return 1;
}
// apply program to the data
enum { nDataSets = sizeof data / sizeof *data };
for (size_t i = 0; i < nDataSets; ++i) {
const char *sep = "";
cout << "Data Set:" << endl;
for (size_t j = 0; j < nVars; ++j, sep = ", ") {
cout << sep << vars[j] << ": " << data[i][j];
}
cout << endl;
// load data
for (size_t j = 0; j < nVars; ++j) varTable[vars[j]].set(data[i][j]);
// perform program
cout << "Compute:" << endl;
istringstream in(sourceCode);
for (const AST::Expr *pExpr : program) {
string line; getline(in, line);
cout << line << ": " << pExpr->solve() << endl;
}
cout << endl;
}
// clear the program
for (AST::Expr *pExpr : program) delete pExpr;
program.clear();
// done
return 0;  
}

我在VS2013(Windows 10)上编译和测试并得到:

Data Set:
a: 4, b: 2
Compute:
1 + 2 * 3 / 4 - 5;: -2.5
a + b;: 6
a - b;: 2
a * b;: 8
a / b;: 2
a * (b + 3);: 20
Data Set:
a: 2, b: 4
Compute:
1 + 2 * 3 / 4 - 5;: -2.5
a + b;: 6
a - b;: -2
a * b;: 8
a / b;: 0.5
a * (b + 3);: 14
Data Set:
a: 10, b: 5
Compute:
1 + 2 * 3 / 4 - 5;: -2.5
a + b;: 15
a - b;: 5
a * b;: 50
a / b;: 2
a * (b + 3);: 80
Data Set:
a: 42, b: 42
Compute:
1 + 2 * 3 / 4 - 5;: -2.5
a + b;: 84
a - b;: 0
a * b;: 1764
a / b;: 1
a * (b + 3);: 1890

请注意,解析器本身会忽略任何空格和换行符。但是,为了使示例输出格式简单,我必须在每行使用一个以分号结尾的表达式。否则,将很难将源代码行与相应的编译表达式相关联。 (请记住我上面关于可能添加源代码引用(又名文件位置)的Token的说明。

完整示例

。源代码和测试运行可以在IDEone上找到。