让bison开始使用除开始规则之外的规则进行解析

时间:2013-09-03 14:34:14

标签: bison

目前我正在研究源到源编译器,我已经编写了一个野牛解析器,可以正确地为输入创建AST。我现在需要在语法树上进行几次转换,因此我需要在树中插入许多节点。

我可以手动创建我想要添加到语法树的所有结构/联合,但这似乎非常很多工作。

创建一个字符串对我来说要容易得多,我希望这个字符串能够被我已经拥有的解析器解析。然后,解析器应返回此字符串的树,我可以在原始语法树中插入该字符串。

不幸的是,字符串无法使用我的解析器的启动规则进行解析,因为它必须由子规则解析(例如,我的解析器解析包含语句的函数列表,字符串是单个语句)。

如何让bison解析字符串,从不同于起始规则的规则开始?

提前致谢!

3 个答案:

答案 0 :(得分:5)

有一个简单的hack,在bison FAQ.中描述。

基本上,对于您希望能够使用的每个非终端,您创建一个伪令牌,然后创建一个“元启动”非终端,它选择您要使用的终端: / p>

%token START_PROGRAM
%token START_STATEMENT
%token START_EXPRESSION

%start meta_start

%%

meta_start: START_PROGRAM program
          | START_STATEMENT statement
          | START_EXPRESSION expression
          ;

(在每个产品的操作中,您可以将$2的值保存在调用者可以获得的位置。)

现在你只需要安排你的词法分析器来提供正确的开始令牌。您可以通过使用纯解析器和纯词法分析器并通过共享数据结构传递消息来实现此目的。这将是最好的方法,但为了这个答案的目的,我将展示如何使用全局变量,因为原则是相同的:

extern int start_token;

// If all your start non-terminals produce a value of the same type, possibly a union
// type, you could arrange to return it instead of the error report.

int yyparse(int start) {
  // I left out the code to cause the lexer to scan a given string.
  start_token = start;
  return real_yyparse();
}

int yylex() {
  int rv = start_token;
  if (start_token)
    start_token = 0;
  else
    rv = real_yylex();
  return rv;
}

答案 1 :(得分:2)

鉴于您似乎愿意做的事情,您可能会发现有趣的想法可以从this paper回收。

它提供了从C ++代码支持具体语法的方法,例如(这里,从Bison解析器本身中消失):

exp: exp "&" exp
{
  // Sub−ASTs are used as−is (they are not printed, then parsed,
  // as in the previous example). Spaces are no longer critical.
  $$ = parse(Tweast() <<
             "if" << $1 << "then" << $3 << "<> 0 else 0");
};

答案 2 :(得分:0)

假代币并没有完全解决问题。如果您要解析某种语言的子规则,则可能还需要重复调​​用该规则(多次调用yyparse)。

这要求您使用假令牌“填充”解析器,这会导致它在每次调用时返回到有趣的规则维护实际非伪令牌流的正确状态。另外,您需要一种方法来检测yyparse调用是否遇到过EOF。

同样,理想情况下,您希望能够将调用与yyparse以及流上的其他操作混合,这意味着可以精确控制Flex + Yacc组合执行的前瞻。

我在TXR语言的解析器中解决了所有这些问题。在这种语言中,有一些有趣的子语言:Lisp语法,以及正则表达式语法。

问题是提供一个Lisp读取函数,它从流中提取单个对象并使流处于合理状态(关于前瞻)。例如,假设流包含:

 (a b c d) (e f g h)

我们使用到达Lisp子语法所需的伪令牌来填充解析器。然后拨打yyparse。完成yyparse后,它将消耗此处的所有内容:

 (a b c d) (e f g h)
            ^ stream pointer is here
           ^ the lookahead token is the parenthesis

在此次通话结束后,如果有人调用某个功能从流中获取某个字符,那么很遗憾,他们将获得e,而不是(括号。

无论如何,我们调用了yyparse,获得了(a b c d) Lisp对象,流指针位于e,前瞻标记为(

下次调用yyparse时,它将忽略此前瞻标记,我们将得到一个错误的错误。我们不仅要使用导致它解析Lisp表达式的伪伪令牌来填充解析器,而且我们还必须让它开始在(前瞻令牌处解析。

执行此操作的方法是将此令牌插入到启动流中。

在TXR解析器中,我实现了一个令牌流对象,该对象最多可以使用四个推回令牌。调用yylex时,令牌会从此后推中被拉出,只有当它是空的时才执行真正的lexing。

这用于prime_parser函数:

void prime_parser(parser_t *p, val name, enum prime_parser prim)
{
  struct yy_token sec_tok = { 0 };

  switch (prim) {
  case prime_lisp:
    sec_tok.yy_char = SECRET_ESCAPE_E;
    break;
  case prime_interactive:
    sec_tok.yy_char = SECRET_ESCAPE_I;
    break;
  case prime_regex:
    sec_tok.yy_char = SECRET_ESCAPE_R;
    break;
  }

  if (p->recent_tok.yy_char)
    pushback_token(p, &p->recent_tok);
  pushback_token(p, &sec_tok);
  prime_scanner(p->scanner, prim);
  set(mkloc(p->name, p->parser), name);
}

解析器的recent_tok成员跟踪最近看到的令牌,这使我们可以从最近的解析中访问先行令牌。

为了控制yylex,我在parser.l中实施了这个黑客:

/* Near top of file */

#define YY_DECL \
  static int yylex_impl(YYSTYPE *yylval_param, yyscan_t yyscanner)

/* Later */

int yylex(YYSTYPE *yylval_param, yyscan_t yyscanner)
{
  struct yyguts_t * yyg = convert(struct yyguts_t *, yyscanner);
  int yy_char;

  if (yyextra->tok_idx > 0) {
    struct yy_token *tok = &yyextra->tok_pushback[--yyextra->tok_idx];
    yyextra->recent_tok = *tok;
    *yylval_param = tok->yy_lval;
    return tok->yy_char;
  }

  yy_char = yyextra->recent_tok.yy_char = yylex_impl(yylval_param, yyscanner);
  yyextra->recent_tok.yy_lval = *yylval_param;

  return yy_char;

如果令牌回送索引非零,我们弹出固定令牌并将其返回给Yacc。否则我们会调用真正的词法分析器yylex_impl

请注意,当我们这样做时,我们还会查看词法分析器返回的内容并将其存储在recent_tok.yy_charrecent_tok.yy_lval中。

(如果yy_lval是堆分配的对象类型怎么办?我们在这个项目中有垃圾收集的好事!)

在符合这些子语言的规则中,我必须使用YYACCEPT。并注意byacc_fool业务:这是让黑客与Berkeley Yacc合作所需要的。 (T.E. Dickey维护它的版本,支持Bison重入解析器方案。)

spec : clauses_opt              { parser->syntax_tree = $1; }
     | SECRET_ESCAPE_R regexpr  { parser->syntax_tree = $2; end_of_regex(scnr);
     | SECRET_ESCAPE_E n_expr   { parser->syntax_tree = $2; YYACCEPT; }
       byacc_fool               { internal_error("notreached"); }
     | SECRET_ESCAPE_I i_expr   { parser->syntax_tree = $2; YYACCEPT; }
       byacc_fool               { internal_error("notreached"); }
     | SECRET_ESCAPE_E          { if (yychar == YYEOF) {
                                    parser->syntax_tree = nao;
                                    YYACCEPT;
                                  } else {
                                    yybadtok(yychar, nil);
                                    parser->syntax_tree = nil;
                                  } }
     | SECRET_ESCAPE_I          { if (yychar == YYEOF) {
                                    parser->syntax_tree = nao;
                                    YYACCEPT;
                                  } else {
                                    yybadtok(yychar, nil);
                                    parser->syntax_tree = nil;
                                  } }
     | error '\n'               { parser->syntax_tree = nil;
                                  if (parser->errors >= 8)
                                    YYABORT;
                                  yyerrok;
                                  yybadtok(yychar, nil); }

     ;

    }

为什么YYACCEPT?我不记得了;好的,我们有详细的ChangeLog消息:

* parser.y (spec): Use YYACCEPT in the SECRET_ESCAPE_E clause for
pulling a single expression out of the token stream. YYACCEPT
is a trick for not invoking the $accept : spec . $end production
which is implicitly built into the grammar, and which causes
a token of lookahead to occur.  This allows us to read a full
expression without stealing any further token: but only if the
grammar is structured right.

我认为此评论因遗漏而略有误导。隐式$end生成导致的问题不仅仅是不必要的前瞻:它正在向前看,因为实际上想要匹配EOF。我似乎记得YYACCEPT是一种拯救解析器的方式,这样当下一个令牌不是$end令牌时,它就不会抛出语法错误,这是一个内置的代表EOF。

无论如何,Yacc最终向前看;我们不希望它做的是引发语法错误,因为前瞻不是文件的结尾,正如规则所预期的那样。当我们有

 (a b c d) (e f g h)

我们有一个简单的语法规则匹配表达式,(e f g h)东西看起来像杂散材料,这是一个语法错误!在解析器获得第一个)令牌后,它再次调用yylex并获得(,这是语法错误;它希望yylex在此时指示EOF。 YYACCEPT是一种解决方法。我们让Yacc拨打yylex并拨出第二个(,并记下它,以便我们可以在下一个yyparse电话中将其推回;但是我们阻止Yacc适应这个令牌。