鉴于以下语言描述为:
(identifier operator identifier+)*
在给定任意运算符@
的情况下,该语言的一系列操作示例:
A @ B C X @ Y
空白并不重要,也可以更清楚地写成:
A @ B C
X @ Y
您如何使用类似yacc的LALR解析器解析它?
到目前为止我尝试了什么
我知道如何解析显式分隔的操作,比如A @ B C ; X @ Y
,但我想知道解析上面的输入是否可行以及如何解析。以下是使用Flex / Bison的(非功能性)最小示例。
lex.l:
%{
#include "y.tab.h"
%}
%option noyywrap
%option yylineno
%%
[a-zA-Z][a-zA-Z0-9_]* { return ID; }
@ { return OP; }
[ \t\r\n]+ ; /* ignore whitespace */
. { return ERROR; } /* any other character causes parse error */
%%
yacc.y:
%{
#include <stdio.h>
extern int yylineno;
void yyerror(const char *str);
int yylex();
%}
%define parse.lac full
%define parse.error verbose
%token ID OP ERROR
%left OP
%start opdefs
%%
opright:
| opright ID
;
opdef: ID OP ID opright
;
opdefs:
| opdefs opdef
;
%%
void yyerror(const char *str) {
fprintf(stderr, "error@%d: %s\n", yylineno, str);
}
int main(int argc, char *argv[]) {
yyparse();
}
构建:$ flex lex.l && yacc -d yacc.y --report=all --verbose && gcc lex.yy.c y.tab.c
问题:我无法将 的解析器包含在 rvalue 的下一个左值标识符中第一次行动。
$ ./a.out
A @ B C X @ Y
error@1: syntax error, unexpected OP, expecting $end or ID
上述内容始终解析为:reduce(A @ B reduce(C X)) @ Y
我感觉我必须以某种方式在前瞻标记上设置条件,如果它是操作符,则不应移动最后一个标识符并且应当减少当前堆栈:
A @ B C X @ Y
^ * // ^: current, *: lookahead
-> reduce 'A @ B C' !
-> shift 'X' !
我尝试了所有类型的运算符优先级安排但无法使其正常工作。
我愿意接受不适用于Bison的解决方案。
答案 0 :(得分:3)
该语言的天真语法是LALR(2),而野牛不生成LALR(2)解析器。
任何LALR(2)语法都可以通过机械修改来生成带有兼容解析树的LALR(1)语法,但我不知道有任何自动工具可以做到这一点。
手动进行转换是可能但很烦人,但请注意,您需要调整操作以恢复正确的解析树:
%{
typedef struct IdList { char* id; struct IdList* next; };
typedef struct Def { char* lhs; IdList* rhs; };
typedef struct DefList { Def* def; struct DefList* next; };
%}
union {
Def* def;
DefList* defs;
char* id;
}
%type <def> ophead
%type <defs> opdefs
%token <id> ID
%%
prog : opdefs { $1->def->rhs = IdList_reverse($1->def->rhs);
DefList_show(DefList_reverse($1)); }
ophead: ID '@' ID { $$ = Def_new($1);
$$->rhs = IdList_push($$->rhs, $3); }
opdefs: ophead { $$ = DefList_push(NULL, $1); }
| opdefs ID { $1->def->rhs = IdList_push($1->def->rhs, $2); }
| opdefs ophead { $1->def->rhs = IdList_reverse($1->def->rhs);
$$ = DefList_push($1, $2); }
具有讽刺意味的是,这个确切的问题是bison
本身的一部分,因为制作不需要;
终止符。 Bison使用它自己生成一个解析器,它解决了词法分析器中的这个问题,而不是如上所述跳过循环。在词法分析器中,一旦找到ID
,扫描将继续到下一个非空白字符。如果那是:
,则词法分析器返回identifier-definition
标记;否则,非空白字符将返回到输入流,并返回一个普通的identifier
标记。
这是在词法分析器中实现它的一种方法:
%x SEEK_AT
%%
/* See below for explanation, if needed */
static int deferred_eof = 0;
if (deferred_eof) { deferred_eof = 0; return 0; }
[[:alpha:]][[:alnum:]_]* yylval = strdup(yytext); BEGIN(SEEK_AT);
[[:space:]]+ ; /* ignore whitespace */
/* Could be other rules here */
. return *yytext; /* Let the parser handle errors */
<SEEK_AT>{
[[:space:]]+ ; /* ignore whitespace */
"@" BEGIN(INITIAL); return ID_AT;
. BEGIN(INITIAL); yyless(0); return ID;
<EOF> BEGIN(INITIAL); deferred_eof = 1; return ID;
}
在SEEK_AT
开始条件下,我们只对@
感兴趣。如果我们找到一个,那么ID就是def
的开头,我们返回正确的令牌类型。如果我们找到任何其他内容(除了空格),我们使用yyless
将字符返回到输入流,并返回ID
令牌类型。请注意,yylval
的初始扫描已设置ID
,因此无需担心此问题。
上述代码中唯一复杂的部分是EOF
处理。检测到EOF
后,无法将其重新插入输入流,无论是yyless
还是unputc
。让扫描仪再次读取EOF
也不合法。所以需要充分处理。不幸的是,在SEEK_AT
启动条件下,完全处理EOF
需要发送两个令牌:首先是已检测到的ID
令牌,然后是yyparse
将识别为结束的0输入。如果没有推送解析器,我们就无法通过单个扫描程序操作发送两个令牌,因此我们需要注册已收到EOF
的事实,并在下次调用扫描程序时检查该事件。
在第一个规则插入yylex
函数顶部之前缩进代码,因此它可以声明局部变量并执行扫描开始之前需要完成的任何操作。如上所述,此词法分析器不可重入,但可重新启动,因为if (deferred_eof)
操作中的持久状态已重置。要使其可重入,您只需将deferred_eof
置于yystate
结构中,而不是将其设置为静态本地。
答案 1 :(得分:0)
关注rici有用的评论和回答,以下是我提出的建议:
lex.l:
%{
#include "y.tab.h"
%}
%option noyywrap
%option yylineno
%%
[a-zA-Z][a-zA-Z0-9_]* { yylval.a = strdup(yytext); return ID; }
@ { return OP; }
[ \t\r\n]+ ; /* ignore whitespace */
. { return ERROR; } /* any other character causes parse error */
%%
yacc.y:
%{
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
extern int yylineno;
void yyerror(const char *str);
int yylex();
#define STR_OP " @ "
#define STR_SPACE " "
char *concat3(const char *, const char *, const char *);
struct oplist {
char **ops;
size_t capacity, count;
} my_oplist = { NULL, 0, 0 };
int oplist_append(struct oplist *, char *);
void oplist_clear(struct oplist *);
void oplist_dump(struct oplist *);
%}
%union {
char *a;
}
%define parse.lac full
%define parse.error verbose
%token ID OP END ERROR
%start input
%%
opbase: ID OP ID {
char *s = concat3($<a>1, STR_OP, $<a>3);
free($<a>1);
free($<a>3);
assert(s && "opbase: allocation failed");
$<a>$ = s;
}
;
ops: opbase {
$<a>$ = $<a>1;
}
| ops opbase {
int r = oplist_append(&my_oplist, $<a>1);
assert(r == 0 && "ops: allocation failed");
$<a>$ = $<a>2;
}
| ops ID {
char *s = concat3($<a>1, STR_SPACE, $<a>2);
free($<a>1);
free($<a>2);
assert(s && "ops: allocation failed");
$<a>$ = s;
}
;
input: ops {
int r = oplist_append(&my_oplist, $<a>1);
assert(r == 0 && "input: allocation failed");
}
;
%%
char *concat3(const char *s1, const char *s2, const char *s3) {
size_t len = strlen(s1) + strlen(s2) + strlen(s3);
char *s = malloc(len + 1);
if (!s)
goto concat3__end;
sprintf(s, "%s%s%s", s1, s2, s3);
concat3__end:
return s;
}
int oplist_append(struct oplist *oplist, char *op) {
if (oplist->count == oplist->capacity) {
char **ops = realloc(oplist->ops, (oplist->capacity + 32) * sizeof(char *));
if (!ops)
return 1;
oplist->ops = ops;
oplist->capacity += 32;
}
oplist->ops[oplist->count++] = op;
return 0;
}
void oplist_clear(struct oplist *oplist) {
if (oplist->count > 0) {
for (size_t i = 0; i < oplist->count; ++i)
free(oplist->ops[i]);
oplist->count = 0;
}
if (oplist->capacity > 0) {
free(oplist->ops);
oplist->capacity = 0;
}
}
void oplist_dump(struct oplist *oplist) {
for (size_t i = 0; i < oplist->count; ++i)
printf("%2zu: '%s'\n", i, oplist->ops[i]);
}
void yyerror(const char *str) {
fprintf(stderr, "error@%d: %s\n", yylineno, str);
}
int main(int argc, char *argv[]) {
yyparse();
oplist_dump(&my_oplist);
oplist_clear(&my_oplist);
}
使用A @ B C X @ Y
输出:
0: 'A @ B C'
1: 'X @ Y'