【问题标题】:Parsing strings of user input using the cparse library from git使用来自 git 的 cparse 库解析用户输入的字符串
【发布时间】:2018-04-08 00:37:16
【问题描述】:

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

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

当我尝试从 git 中包含 cparse 库 时,我的问题就出现了。我天真地按照设置说明进行操作(刚接触 git):

$ cd cparse
$ make release

但是我不能使用 make 命令,因为我使用的是 windows。我尝试下载 zip 文件并将 .cpp.h 文件直接复制到项目中,并使用 Visual Studio 中的 "include existing" 选项,但会出现大量编译器错误并且可以不要让代码自己工作。

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

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

【问题讨论】:

  • 下载 MinGW64 并将其安装到您的系统上。
  • @JohnZwinck 没有任何迹象表明 OP 正在使用 Python,并且“使用这种完全不同的编程语言,你可能知道也可能不知道”很少有用的建议。
  • @user:太好了。现在你有两个问题。

标签: c++ windows parsing compilation


【解决方案1】:

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

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

相反,我想向您展示如何自己构建一个简单的计算器(使用递归下降解析器)。对于我将使用的技术的文档,我强烈推荐Compilers (Principles, Techniques & Tools) by Aho, Lam, Sethi, Ullman(更好地称为“龙书”),尤其是第 4 章

微型计算器项目

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

语言设计

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

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

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

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

a*(b+3);

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

在我开始草拟这种语言的grammar 之前,我在考虑编译的“目标”并决定为......制作类

抽象语法树

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

// 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 的使用会自动创建变量。正如许多高级语言所知,变量在第一次访问时就开始存在。

Abstract Syntax Tree 是一个

以编程语言编写的源代码的抽象句法结构的树状表示。树的每个节点表示源代码中出现的一个构造。

我从上面链接的 Wikipedia 文章中提取了这段文字——不能说得更短。以下是我的 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 实例的子代(尽管乘法的优先级高于加法的优先级),这是对括号进行适当考虑的结果。

除了存储解析后的表达式,这棵树还可以通过调用根节点的Expr::solve()方法来计算表达式:

double result = pExpr->solve();

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

语法

grammar 最好地描述正式语言。

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

规则包含

  • 终端符号(以大写字母开头)和
  • 非终结符(以小写开头)
  • 冒号 (:) 分隔左侧和右侧(左侧的非终结符可能会扩展为右侧的符号)。
  • 竖线 (|) 用于分隔备选方案
  • &lt;empty&gt; 符号,用于扩展为空(用于终止递归)。

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

非终结符将被转换为解析器函数。

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();

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

在内部,解析器调用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找到。

【讨论】:

  • 感谢您的深入解释,您完全正确,这更像是我想自己编程但目前无法实现的。非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-08-22
  • 1970-01-01
  • 2012-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多